大模型为什么总“输出不规范的JSON”?该如何优雅接锅并落地?

转载请注明出处❤️

作者:测试蔡坨坨

原文链接:caituotuo.top/db85e15d.html


前言

你好,我是测试蔡坨坨。

最近在使用大模型生成测试用例,流程大概是:先让模型输出结构化的JSON用例,再把JSON转换成XMind文件。

但是在实践过程中发现,模型输出的JSON经常不合法,要么少了括号,要么key值带中文符号,要么多了一堆说明文字,要么双引号嵌套双引号,甚至回答半截就断掉的。最终导致JSON解析失败,影响后续导出流程,整条链路被卡住。

那么问题来了,为什么非要纠结JSON格式输出?

很多人可能觉得输出文字就行,能看懂就好,但其实这远远不够。

原因很简单,在AI开发中,大模型输出的最终结果往往需要被程序继续消费,而不是停留在展示层。

比如你想要从模型回答中抽取一些关键信息,这个时候怎么给你?肯定是结构化的字段信息简洁明了,而不是一段模糊的自然语言。

当然,结构化的输出不止JSON一种,还有很多,但在众多结构化表达方式中,JSON最常见、最轻量、最兼容(前后端都好用;生态丰富,有很多成熟库;天然支持嵌套,能表达树形、列表、字典等复杂结构)。

Anyway,这不重要,让我们回到问题的关键,大家头疼的是,为什么大模型有时候不遵循指令,不按照你约定的格式输出,即使你在prompt中明确说明了要按照JSON格式返回结果,但是它就像一个犟种,就是不听,你能怎么办,总不能不干吧。

本文就带大家把这件事捋清楚:

  • 为什么大模型总是生成不完整或不合法的JSON
  • 常见的坑和错误类型
  • 工程上如何保证输出既合法又健壮(含prompt模板、校验与自愈策略、实战代码片段)

为什么?

从根源上看,大模型本质是预测下一个token的概率机器,它对格式的理解更多来自训练语料,而不是像编程语言解析器那样严格。常见原因包括:

  • 自然语言干扰:模型会“贴心”地加上解析说明,输出时会夹杂评论或解释性语言,导致JSON解析失败。
  • 长文本截断:在流式或长输出时,生成可能中途被截断,缺少 “}” 或 “]”。
  • 符号混乱:中文全角引号、逗号等混入key/value。
  • 结构化推理错误:模型在层级复杂时容易搞错括号匹配、丢字段,尤其是嵌套数组里最常见。

不过话又说回来,模型追求“像人一样写”,而JSON需要“像机器一样严谨”,这两者貌似天然矛盾。

怎么做?

要让大模型生成的JSON可用,通常要从前端约束后端兜底两方面入手。

不要觉得这种很容易,这种结构化大模型输出目前已经成为了很多人的研究课题。

输入即输出

通过JSON in / JSON out的方式,把用户输入本身就设计成一个JSON,直接丢给大模型,按照模型的从众性,模型自然会按照JSON语境来输出。

示例:测试用例生成任务

输入JSON:

task定义目标任务,restriction给出JSON限制,text就是文本输入,format是定义好的输出JSON字段。

1
2
3
4
5
6
7
8
9
10
11
12
{
"task": "根据<text>生成多条测试用例,<title>是用例标题,<steps>是执行步骤,<expected>是预期结果",
"restriction": "输出必须符合<format>的 JSON 结构,不要添加多余说明",
"text": "登录功能",
"format": {
"case": {
"title": "string",
"steps": ["string"],
"expected": "string"
}
}
}

模型输出JSON:

Prompt 约束

在prompt里明确要求模型返回JSON,并且给一个示例JSON作为参考。

还是生成测试用例的例子:

prompt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
你是一个测试用例生成器,请根据下面的需求文本生成测试用例。  
要求:
1. 仅输出合法 JSON,不要添加任何多余说明文字或注释。
2. JSON 格式必须参考下面的示例:

示例 JSON:
```json
{
"case": {
"title": "登录成功用例",
"steps": [
"输入正确的账号",
"输入正确的密码",
"点击登录按钮"
],
"expected": "成功进入系统首页"
}
}
```

需求文本:
系统登录功能

这种方式就是在提示词的基础之上,别简单的写按照JSON格式返回结果,鬼知道能返回什么JSON,所以要按照你的需求,给定对应返回的JSON案例。

模型参数设置

1. 传入response_format参数

如果你用的是OpenAI的API,它提供了response_format参数,可以设置为{"type": "json_object"},使用模型内置的结构化输出能力。

1
2
3
4
5
6
7
8
9
10
11
import openai

openai.api_key = 'your-api-key'

response = openai.Completion.create(
model="",
prompt="",
response_format={"type": "json_object"}
)

print(response)

但是这种方法也不一定有效,最好同时在prompt中按照上面说的加一些输出的JSON格式案例。

2. 调整temperature

将 temperature 设为 0(或极低)以减少随机性。

3. 调整max_tokens

使用 max_tokens 合理上限,避免无意义的长文本或者被截断(注意避免过小导致输出被裁)。

那么问题来了,像上面的这些做法能100%解决格式化输出的问题吗?

那你可真的太小看模型了。

像大规模参数量的模型还好一些,小规模参数量的模型不遵循指令的问题相对明显。

所以,还需要有一些兜底的策略。

第三方库修复(代码)

利用一些第三方库修复工具,比如json-repair,用于修复JSON字符串中语法错误的情况,例如:缺失的逗号、引号不匹配或大括号/中括号未闭合等问题。

安装

1
pip install json-repair

基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from json_repair import repair_json

def repair_json_string(broken_json_str):
"""
修复有问题的JSON字符串并转换为Python对象

Args:
broken_json_str (str): 有问题的JSON字符串(如key没加引号、结尾多逗号等)

Returns:
dict or list or None: 修复后的Python对象,修复失败时返回None
"""
try:
# 修复JSON字符串
fixed_json_str = repair_json(broken_json_str)
# 转换成Python对象
fixed_data = json.loads(fixed_json_str)
return fixed_data
except Exception as e:
# 修复失败,返回None
return None

Badcase1:多了逗号

1
2
输入:"{name: '蔡坨坨', age: 18,}"
输出:{'name': '蔡坨坨', 'age': 18}

Badcase2:未闭合

1
2
输入:'{"name": "test caituotuo", "age": 18'
输出:{'name': 'test caituotuo', 'age': 18}

Badcase3:混入了中文符号

1
2
输入:'{"name": “test caituotuo”, "age": 18'
输出:{'name': 'test caituotuo', 'age': 18}

Badcase4:括号重复

1
2
输入:'{"name": "test caituotuo", "age": 18}}'
输出:{'name': 'test caituotuo', 'age': 18}

更多badcase可以在评论区留言~

需要注意的是,json-repair无法处理字段直接缺失的情况,只能保证现有字段的语法正确性。

JSON片段提取(代码)

提取返回结果中的JSON片段信息,对于非法JSON直接舍弃:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
def extract_json_strings(text):
"""
从文本中提取所有有效的JSON对象
Args:
text (str): 包含JSON数据的文本
Returns:
list: 包含解析后的JSON对象的列表(dict或list对象)
Examples:
输入: 'Here is some data: {"name": "test", "value": 123} and [1,2,3]'
输出: [{'name': 'test', 'value': 123}, [1, 2, 3]]
"""
if not text:
return []
json_objects = []
i = 0
while i < len(text):
# 寻找JSON对象的开始标记
if text[i] in ['{', '[']:
start_char = text[i]
# end_char = '}' if start_char == '{' else ']'
# 使用栈来匹配括号
stack = [start_char]
j = i + 1
while j < len(text) and stack:
char = text[j]
if char == '"':
# 跳过字符串内容,处理转义字符
j += 1
while j < len(text):
if text[j] == '"' and text[j - 1] != '\\':
break
j += 1
elif char in ['{', '[']:
stack.append(char)
elif char in ['}', ']']:
if stack:
last = stack[-1]
if (char == '}' and last == '{') or (char == ']' and last == '['):
stack.pop()
j += 1
# 如果栈为空,说明找到了完整的JSON结构
if not stack:
json_str = text[i:j]
try:
# 解析JSON字符串为Python对象
json_obj = json.loads(json_str)
json_objects.append(json_obj)
except json.JSONDecodeError:
# JSON无效,跳过
pass
i = j
else:
i += 1
else:
i += 1
return json_objects

输入:

1
2
3
4
5
6
7
8
"""
```json
[
{"id": 1, "name": "caituotuo"},
{"id": 2, "name": "测试蔡坨坨",
[{"id": 3,
```
"""

输出:

1
[[{'id': 1, 'name': 'caituotuo'}]]

重试机制

当自动修复失败或数据关键信息缺失时,向模型重新提问,大模型偶尔一时的抽风不代表全部都有问题。

综上

当然了,以上只是一些基础的用法,大模型的格式化输出实际上已经是很多团队和研究者在持续探索的方向,如果你也有类似的case或解决方案,评论区留言。

拜了个拜~