模型输出的 JSON 解析失败怎么办?从提示到约束的五层防线
输出格式不稳定导致解析失败、然后重跑,是很隐蔽的一笔成本:账单上看不出异常,但你在为同一件事付两遍钱。更糟的是它往往在上线后才集中暴露,因为开发时的样本太干净。
这篇按投入从低到高给五层防线,能在第一层解决就别用第五层。
第一层:把格式要求写进提示
最基础也最容易被写砸的一层。有效的写法有几个特征:
明确到字段级。 不要只说「返回 JSON」,要列出字段名、类型、是否必填。模型不知道你的下游解析器期待什么。
明确禁止额外内容。 模型很爱在 JSON 前后加一句「好的,以下是结果:」或者用代码块包起来。明确要求「只输出 JSON,不要任何解释文字、不要 Markdown 代码块」。
明确空值和异常的表示。 抽取不到信息时该返回什么?空字符串、null、还是省略字段?不说清楚,模型会自由发挥,而你的解析器会崩。
把格式要求放在提示末尾。 长提示里,靠近末尾的指令通常更容易被遵守。
第二层:给示例
一到两个完整的输入输出示例,比十行描述有效得多。示例要覆盖典型情况和一个边界情况(比如「找不到信息时长什么样」)。
注意示例是有成本的——它每次请求都要发。如果示例很长,考虑把它放进请求前缀交给提示缓存,这样重复发送的代价很低。见提示缓存是怎么计费的。
第三层:用 schema 强约束
多数厂商提供了结构化输出能力(JSON schema 约束、function calling 的参数 schema 等),由服务端保证输出符合结构。
这是最有效的一层,能把格式错误率压到很低。代价是:
- schema 本身占输入 token(通常不多)
- 各家的支持程度不同,复杂嵌套、联合类型、可选字段的处理有差异
- 换厂商时这部分要重测,见 OpenAI 兼容端点能兼容到什么程度
建议:schema 尽量扁平、字段尽量少。深层嵌套不但更容易出错,也更难在提示里说清楚。
第四层:容错解析
即便有前三层,仍然会有少数漏网的。解析层做几件事能救回大部分:
剥掉 Markdown 代码块包裹。 最常见的污染,一行正则就能处理。
提取第一个完整的 JSON 对象。 前后有解释文字时,从第一个 { 到匹配的 } 截出来。
容忍尾随逗号和单引号。 用宽松的解析器兜一层,别一上来就用最严格的解析。
做字段级降级。 某个非关键字段缺失或类型不对时,用默认值填上继续跑,而不是整条丢弃。
这一层的价值是:把「解析失败」从整体失败降级成局部缺失,避免为了一个可有可无的字段重跑整个请求。
第五层:校验后重试
前面都没救回来,才走重试。重试要讲究方法:
带上错误信息重试。 把上一次的输出和具体的解析错误一起发回去,让模型改。这比原样重发有效得多,成功率高很多。
限制重试次数。 一到两次,再多就该走人工兜底或降级路径了。连续失败通常说明提示本身有问题,不是随机波动。
记录失败样本。 每一条解析失败都是改进提示的素材。攒够一批看规律,往往能发现是某类输入触发的(超长内容、特殊字符、多语言混排)。
schema 设计的四条经验
强约束这一层的效果,很大程度上取决于 schema 本身设计得好不好。
一、扁平优于嵌套。 三层以上的嵌套结构,模型出错率明显上升,而且错了很难定位。能拍平就拍平,实在需要层级就拆成两次调用。
二、字段名要自解释。 is_urgent 比 flag2 好,模型是靠字段名理解语义的。字段名写清楚,甚至能省掉一部分提示词。
三、枚举优于自由文本。 需要分类时给出确定的选项列表,而不是让模型自由填写。自由文本会出现各种同义变体,下游还得再做归一。
四、必填字段尽量少。 每个必填字段都是一个可能失败的点。把「有则更好」的字段设成可选,解析层给默认值,整体成功率会高不少。
另外,schema 也占 token,而且每次请求都发。字段特别多的时候,考虑把它放进请求前缀交给提示缓存,见提示缓存是怎么计费的。
上线前该跑的一批测试
结构化输出的问题几乎都在生产环境才暴露,因为开发时用的样本太干净。上线前建议专门造一批「脏样本」测:
- 超长输入:看模型会不会因为上下文压力而放弃格式
- 含特殊字符的输入:引号、反斜杠、换行、emoji,这些最容易破坏 JSON
- 多语言混排:中英日混合的内容
- 空内容或无关内容:用户输入了一句废话,模型该返回什么
- 信息缺失:要抽取的字段在原文里根本不存在
每类造几条,跑一遍看一次成功率。这批样本留下来,以后每次改提示或换模型都跑一遍,就是你的回归测试集。
把重试成本算清楚
一次解析失败的成本 = 第一次的完整费用 + 重试的完整费用。如果失败率是百分之几,整体成本就多百分之几——听起来不多,但这部分是纯浪费,没有产出任何价值。
更值得关注的是延迟:重试意味着用户要等两倍时间。交互式场景里,这比多花的钱更伤。
所以优化顺序应该是:先把失败率压下来(一到三层),再把失败的代价降下来(第四层),最后才谈重试(第五层)。反过来做的话,你会得到一个稳定但昂贵的重试机器。
监控里加一个指标
结构化输出的一次成功率(不经重试就解析成功的比例)。这个数低于预期时,先查是不是最近改了提示、换了模型、或者输入分布变了。
顺带一提,换模型是这个指标最常见的杀手——不同模型对格式指令的遵守程度差别很大,迁移时务必把这一项列进回归测试,见跨厂商成本实算模板里的迁移成本清单。
三个高频问题
问:schema 约束和提示词约束能一起用吗? 能,而且推荐一起用。schema 保证结构,提示词保证语义正确性——结构对了但内容填错,schema 是拦不住的。
问:为什么开发时好好的,上线就频繁失败? 因为线上输入更脏:超长、特殊字符、多语言混排、信息缺失。上线前用脏样本测一遍能避免大半。
问:解析失败要不要给用户报错? 尽量不要。先走容错解析和一次重试,实在不行再降级到一个可读的提示,而不是抛技术错误。
一句话记住
格式稳定性靠的是约束前置,不是重试兜底。提示写清楚、给示例、上 schema,这三层做扎实,解析失败率能压到很低;反过来,靠重试硬扛的系统,成本和延迟都是双份的,而且失败样本还在持续消耗你的排查时间。