AI Agent输出JSON总解析失败?三层兜底方案,成功率从70%到95%
本文收录于专栏「一个人用AI做工具矩阵」
做AI应用开发的同学,一定遇到过这个问题:你让AI返回JSON格式,它答应得好好的,结果实际返回的内容让你血压飙升——
• 前面加一句"这是您的表单:"
• 后面跟一句"希望对你有帮助!"
• 用json 代码块包起来
• 返回到一半被截断,括号都没闭合
• 字段名拼错、结构嵌套搞错
如果你只是JSON.parse()一把梭,线上至少30%的请求会报错。
这个问题我在做智枢矩阵(一个AI对话生成表单的工具)时踩了整整一周的坑。今天把最终沉淀的三层兜底方案分享出来,适用所有需要AI输出结构化数据的场景。
一、先看清楚:AI输出JSON到底有哪几种"脏法"
在写方案之前,先搞清楚问题。我把线上抓到的"脏JSON"分了个类:
|
类型 |
示例 |
出现频率 |
|
前后带文字 |
这是您的表单:\n{...}\n希望对您有帮助 |
~25% |
|
Markdown代码块包裹 |
json\n{...}\n |
~15% |
|
被截断(max_tokens) |
{"title":"xxx","fields":[{"label":"姓名"... |
~8% |
|
结构错误 |
字段名拼错、嵌套层级错 |
~5% |
|
干净的JSON |
直接就是合法JSON |
~50% |
注意,这些比例是叠加大模型Prompt优化之前的数据。优化Prompt后"干净的JSON"比例能提升到65%左右,但剩下的35%才是真正让你线上出Bug的元凶。
核心认知:你永远无法100%控制AI的输出格式,只能通过代码兜底。
二、三层兜底方案设计
设计思路很简单:从最理想的情况逐层降级,每一层处理一类问题,尽可能多地提取有效数据。
第一层:清理 → 直接JSON.parse
↓ 失败
第二层:正则提取部分有效字段
↓ 失败
第三层:放弃解析,原样返回当对话处理
下面逐层展开。
第一层:清理 + 直接解析
大部分"脏JSON"其实只差一点点就是合法的——前后多了几个字,或者被代码块包了一层。
import json
import re
def first_layer_parse(raw: str):
"""第一层:清理后直接解析"""
text = raw.strip()
# 1. 去掉 ```json ... ``` 代码块包裹
code_block = re.search(r'```(?:json)?\s*\n?([\s\S]*?)\n?\s*```', text)
if code_block:
text = code_block.group(1).strip()
# 2. 提取 {} 之间的内容(去掉前后多余文字)
first_brace = text.find('{')
last_brace = text.rfind('}')
if first_brace >= 0 and last_brace > first_brace:
text = text[first_brace:last_brace + 1]
# 3. 尝试解析
try:
result = json.loads(text)
return {"success": True, "data": result, "layer": 1}
except json.JSONDecodeError:
return None
这一层能解决约65%的情况。 关键是两个操作:去代码块标记、截取最外层的{}。
有个细节:我用的是find('{')和rfind('}'),取第一个{和最后一个}之间的内容。这比正则更稳,因为有些AI会在JSON内部嵌套花括号,正则容易匹配错。
第二层:正则提取部分字段
第一层失败了,说明JSON结构有问题。最常见的情况是被截断——max_tokens设小了,AI输出到一半就被掐断了。
{
"title": "活动报名表",
"fields": [
{"label": "姓名", "type": "text", "required": true},
{"label": "手机", "type": "phone", "required": true},
{"label": "部门", "type": "select", "options": ["技术部", "产
整个JSON不合法,但前面几个字段对象是完整的。
这时候的思路是:不追求完整JSON,只提取能用的部分。
def second_layer_parse(raw: str):
"""第二层:正则提取可用的字段对象"""
# 提取title(通常在最前面,不会被截断)
title_match = re.search(r'"title"\s*:\s*"([^"]+)"', raw)
title = title_match.group(1) if title_match else "未命名表单"
# 提取完整的字段对象
# 匹配 {"label": "xxx", ... } 的完整结构
field_pattern = re.compile(
r'\{\s*"label"\s*:\s*"[^"]*"[^}]*\}'
)
fields = []
for match in field_pattern.finditer(raw):
field_str = match.group(0)
try:
field = json.loads(field_str)
if field.get("label") and field.get("type"):
fields.append(field)
except json.JSONDecodeError:
# 单个字段也不完整,跳过
continue
if fields:
return {
"success": True,
"data": {"title": title, "fields": fields},
"layer": 2,
"message": f"部分解析成功,提取到 {len(fields)} 个字段"
}
return None
核心逻辑:只收完整的字段对象,不完整的直接丢。
宁可少几个字段,也不能让脏数据污染下游。用户看到"提取到8个字段,部分内容不完整",可以说"继续补全",比直接报错体验好一万倍。
这一层额外拯救了约25%的请求。
第三层:放弃解析,当对话处理
前两层都失败了,说明AI的输出根本没有JSON的意图——可能是在回答用户的问题,或者在"思考"该怎么生成表单。
这时候不要硬解析,直接把AI的原文当对话返回:
def third_layer_fallback(raw: str):
"""第三层:放弃解析,原样返回"""
return {
"success": False,
"data": None,
"layer": 3,
"reply": raw # 原样返回给用户
}
在业务层拿到这个结果后,把AI的回复当作普通对话展示给用户。用户通常会接着说"好的,就按你说的生成",然后下一轮就能正常出JSON了。
这一层兜住剩余约10%的请求。
完整调用链路
def robust_json_parse(raw: str) -> dict:
"""三层兜底解析入口"""
# 第一层
result = first_layer_parse(raw)
if result:
return result
# 第二层
result = second_layer_parse(raw)
if result:
return result
# 第三层
return third_layer_fallback(raw)
三层加起来,我的线上实测成功率从裸调JSON.parse()的约70%提升到了95%以上。
三、几个容易忽略的细节
3.1 Prompt里给Example比写10条规则管用
我试过在System Prompt里写很多规则:"不要输出多余文字"、"必须严格JSON格式"、"不要用代码块"……效果一般。
最后有效的是直接给一个完整示例:
## 示例输出
{
"title": "员工信息表",
"fields": [
{"label": "姓名", "type": "text", "required": true},
{"label": "部门", "type": "select", "options": ["技术部","产品部"]}
]
}
大模型模仿示例格式的能力远强于理解抽象规则的能力。加了这个示例后,"干净JSON"的比例从40%提升到了65%。
3.2 max_tokens不要抠门
一开始我设max_tokens: 500,觉得一个JSON够了。结果复杂表单(20+字段)经常被截断。
现在的做法是根据预估字段数动态调整:
def estimate_max_tokens(field_count: int) -> int:
# 每个字段约50 tokens,基础结构约100 tokens
return min(4096, max(1200, field_count * 50 + 100))
当然,即使max_tokens给够了,第二层兜底仍然有必要——网络超时、模型内部截断等情况都可能发生。
3.3 温度参数的甜蜜点
做结构化输出,temperature太高格式会乱,太低字段类型又会太保守。
我的经验值:0.7。
• 0.3:格式稳定,但用户说"活动报名表"只给姓名+电话
• 0.5:稍好,但字段覆盖不够丰富
• 0.7:格式基本稳定 + 字段覆盖合理
• 0.9:偶尔输出非JSON内容
3.4 未知字段类型要降级,不要报错
AI偶尔会生成你定义之外的字段类型(比如color、password、daterange)。
千万别直接报错。我的做法是降级为text:
KNOWN_TYPES = {"text", "number", "select", "radio", "checkbox", "date", ...}
def normalize_field_type(raw_type: str) -> str:
return raw_type if raw_type in KNOWN_TYPES else "text"
降级后至少能用,用户可以手动改成正确的类型。直接报错的话,整个表单都渲染不出来。
四、实测数据
上线一个月后的统计(约1200次AI生成请求):
|
解析层 |
成功率 |
说明 |
|
第一层(清理+parse) |
65% |
大部分情况,清理后直接成功 |
|
第二层(正则提取) |
25% |
主要是截断场景,提取了部分字段 |
|
第三层(兜底) |
10% |
返回原文,等用户下一轮重新触发 |
|
综合成功率 |
~95% |
第一层+第二层均视为成功 |
剩下5%的失败,主要是:
• AI完全理解错了意图(比如用户输入的内容太模糊)
• 网络超时导致根本没拿到响应
这些就不是JSON解析的问题了,需要在Prompt设计和错误处理层面解决。
五、适用场景
这个三层兜底方案不只适用于表单生成,任何需要AI输出结构化数据的场景都能用:
• AI Agent工具调用:让AI返回函数名+参数JSON
• 数据抽取:从非结构化文本中提取结构化信息
• 配置生成:让AI根据描述生成配置文件
• 对话状态管理:让AI返回当前对话的结构化状态
核心原则都一样:不要信任AI的输出格式,但永远尽力从输出中提取价值。
六、写在最后
做AI应用开发,最大的认知转变就是——AI不是API,它是队友。
API的输出是确定的,你写好了接口规范,它就严格按规范返回。但AI是"概率性"的,即使你Prompt写得再好,它也有概率"不听话"。
所以不能像调API那样调AI。你得做好兜底,接受它的不完美,然后在代码层面把可靠性补回来。
三层兜底方案的核心思路就是:能用就用,能抢救就抢救,实在不行就坦诚告诉用户"这次没搞定",而不是让程序崩掉。
这个思路很朴素,但真的管用。
💬 你在做AI应用时遇到过哪些"AI输出不靠谱"的坑?怎么解决的? 评论区聊聊 👇
⚙️ 我是独立开发者,用AI当队友做工具矩阵。如果你对我的项目感兴趣:
• 智枢矩阵(AI智能表单):https://www.zhishujuzhen.com
• 智播坊(AI口播视频):https://zhibofang.zhishujuzhen.com
• 开源仓库:https://gitee.com/zhang-dongtao/zhishu-matrix-open
更多推荐



所有评论(0)