运营指南

JSON mode 打开后仍解析失败:结构化输出的约束解码原理、常见坑与调用侧兜底

JSON 结构化输出常被当成一个开关,实际上它涵盖约束解码、schema 设计与调用侧兜底三个层面。本文拆解 JSON mode 的实现原理,梳理截断、前缀污染、类型漂移、流式分片等常见坑,并给出清洗、解析、校验、修复的调用侧处理链条,帮助团队把结构化输出真正落到业务里。

把大模型接进业务系统之后,最先撞上的问题往往不是回答得对不对,而是回答能不能被程序读懂。客服工单分类、订单信息抽取、意图识别、报表字段填充,这些下游环节要的是字段和值,而不是一段语气自然的自然语言。JSON 结构化输出(常被称作 JSON mode)就是为解决这件事而出现的:让模型直接吐出可被 json.loads 解析的对象,而不是让人再补一堆正则去猜。

理解 JSON mode,先要分清两条实现路线。一条是提示词约定,在 system 或 user 消息里写明只输出 JSON、不要加解释、字段如下,靠模型的指令遵循能力自觉遵守。另一条是约束解码,在推理阶段直接干预采样过程,把不符合 JSON 语法的 token 概率压到极低或直接屏蔽。两者稳定性差别很大,前者便宜通用,后者才接近工程意义上的保证。

约束解码的原理并不神秘。服务端把目标 JSON Schema 编译成一台状态机,每生成一个 token,就根据当前所处的语法状态计算哪些 token 合法,合法的保留,非法的分数置为负无穷,采样只在合法集合里进行。于是模型不可能在右花括号之后又多写一句话,也不可能把 key 写成没有引号的形式,因为那些 token 在那一刻根本不可选。

但约束解码只保证语法合法,不保证符合你的业务 schema,这是第一个大坑。如果 schema 写得过于宽松,比如把所有字段都设成可选、类型用 any 兜底、允许额外字段透传,那么状态机的约束力会大幅下降,模型依然会自由发挥,只是碰巧长成了 JSON 的样子。真正有效的做法是把必填补全、把枚举穷举、把类型写死,让约束空间尽可能窄。

第二个坑是截断。JSON 是成对闭合的结构,一旦输出被 max_tokens 或超时切断,拿到的就是半截文本:括号没闭合、字符串引号悬空,解析必然失败。很多线上事故不是模型不听话,而是预算给少了。稳妥的做法是给结构化输出任务单独留足输出长度,并在调用侧显式判断结束原因,遇到长度截断就走重试或改用分段生成。

第三个坑是前缀污染。即使开了 JSON mode,部分模型仍习惯在对象外面包一层代码块围栏,或者加一句好的,以下是结果。这类行为在不同厂商、不同版本之间表现不一致。调用侧如果直接对整段文本做反序列化,就会在第一行报错。工程上常见的兜底是:先剥离 JSON 代码块围栏,再从第一个左花括号截取到最后一个右花括号,最后才送去解析。

第四个坑是类型漂移。模型可能把本该是数字的金额写成字符串 128.00,把布尔值写成字符串 true,把 null 写成空字符串或 N/A,日期格式也可能在同一批请求里出现三种写法。这类问题不会让解析报错,却会在入库或计算时炸掉。解决它不能靠提示词反复强调,而要在调用侧用 schema 校验工具做一次强制转换与校验。

流式场景下的坑更隐蔽。普通流式输出给用户看没有问题,但结构化输出在流式下是分片到达的,任何一个中间片段单独拿出来都不是合法 JSON。正确做法是在调用侧维护缓冲区,边收边累积,收完再整体解析;如果确实需要增量消费,就要用增量 JSON 解析器,按已闭合的字段逐步回调,而不是对每个分片调一次反序列化。

调用侧的处理链条,大致可以归纳成四步:清洗、解析、校验、修复。清洗负责去掉围栏和多余话术;解析负责容错地取出对象;校验负责比对 schema,检查必填、枚举、类型和取值范围;修复则是在校验失败时把具体错误信息回灌给模型,让它只针对出错字段重出一次,而不是整段重来。这套组合拳比单纯依赖模型能力可靠得多。

还有一个容易被忽略的点是重试的代价与幂等。结构化输出失败后重试,模型很可能给出不同的结果,如果这次调用连带写操作,就必须保证重试幂等。同时建议把原始返回、schema、校验错误一起打进日志,形成可回溯的链路。否则线上出现解析失败时,只能看到一句 JSON 解析错误,排查成本极高。

在模型选择这一层,很多团队会同时接多家模型,用同一份 schema 去比对谁的结构化输出更稳。这时统一的中转入口能省不少事。快米兔的模型 API 中转提供统一的调用入口,注册送 5 元测试金、按量计费,适合用来做多模型的结构化输出对比与小流量验证,不必为每家厂商单独写一套接入代码,切换和回退都更省事。

回到最初的问题:JSON mode 怎么用。它不是打开开关就万事大吉,而是一份模型与调用侧之间的契约。约束解码负责让输出在语法上始终成立,schema 设计负责把业务约束表达清楚,调用侧的清洗、校验与修复负责兜住剩下那部分不确定性。三者齐备,结构化输出才算真正落地;只做其中一环,线上迟早会遇到那条解析不了的脏数据。