资讯

JSON mode怎么用才不翻车:结构化输出的约束解码原理与调用侧兜底

JSON结构化输出让大模型直接产出程序可消费的数据,但JSON mode只保证语法合法,并不保证字段符合预期。本文从约束解码的原理解释它是怎么实现的,梳理围栏包裹、类型漂移、schema过长被截断、流式解析失败等常见坑,并给出一套调用侧从解析、校验到重试的兜底思路,顺带说明多模型切换时如何低成本验证。

把大模型接进业务系统,真正让人头疼的往往不是它答得不够聪明,而是答得不够整齐。同一句需求,今天返回带前缀的说明文字,明天把数字写成字符串,后天数组里少一个字段,下游的解析代码就得跟着打补丁。JSON结构化输出要解决的正是这个问题:让模型直接产出程序能消费的数据,而不是一段还需要人工二次提取的自然语言。

从定义上说,JSON结构化输出是指模型按照预先声明的数据结构,生成可被 JSON.parse 直接消费的文本。它通常分三个层次:最弱的提示词约束,只在系统提示里写一句请返回JSON;居中的JSON mode,保证输出是合法JSON但不保证字段符合预期;最严格的schema约束,连字段名、类型、必填项和枚举范围都受控。三者的可靠性差距很大,选错层次往往就是线上事故的起点。

原理上,主流做法是约束解码。模型每一步的输出本质上是词表上的一个概率分布,约束解码在采样之前,把所有会破坏JSON语法的候选token分数压到极低,相当于屏蔽掉。用JSON语法可以构造一个有限状态机,只允许能继续构成合法JSON的token通过;schema再把这个状态机收窄,比如某个位置只接受固定几个字段名,某个值必须是数字。

这套机制带来一个容易被忽略的副作用:约束越紧,模型可选的表达空间越小。当它想说的话落不进schema时,只能退而求其次——字段照填,内容却可能是编的。所以字段齐全不等于数据正确,schema能约束格式,约束不了事实。温度、top_p这类采样参数依然生效,只是作用范围被限定在合法token集合之内。

调用侧的用法本身并不复杂。OpenAI风格的接口里,一般通过 response_format 传 json_object 开启JSON mode,或附带完整的结构定义;也有平台把它挂在函数调用、工具调用上,用参数schema间接约束输出。真正麻烦的是各家国产模型的参数名、schema写法、对嵌套结构的支持程度并不统一,同一份schema换个模型可能就要改一遍,迁移成本集中在这些细碎差异上。

第一个常见坑是多余包装。即使开了JSON mode,仍有模型会输出三反引号加json的代码围栏,或在正文前加一句好的以下是整理结果。稳健的做法不是抱怨模型,而是在解析前做一次容错提取:定位第一个左花括号和最后一个右花括号,截取中间部分再解析。这个几十行的函数,能省掉大量无谓的重试与人工排查。

第二个坑是类型漂移与枚举幻觉。数字被写成字符串,金额带上了货币符号,布尔值变成是或否,本该在枚举里的状态值蹦出一个schema中不存在的选项。这类问题在只有提示词约束时尤其高发。字面校验能拦住它们,但拦截之后是重试、降级还是转人工,需要提前把策略定下来,而不是等出事再补。

第三个坑和schema的体量有关。结构越复杂、字段越多,前置约束越重,首token延迟会明显上升,长列表场景还可能因为 max_tokens 不足而被硬截断。截断后的JSON必然解析失败,而 finish_reason 往往已经说明了原因。调用侧应当把是否完整结束当成一次独立校验,而不是等解析器抛异常才发现问题。

第四个坑出现在流式场景。流式返回的是一段段增量文本,中间任何一刻的缓冲区都不是完整JSON,直接 JSON.parse 必然报错。可选方案有三种:缓冲到结束再解析,适合对首屏速度不敏感的批处理;增量解析器逐步喂入,适合边收边展示;或者干脆约定流式只用于自然语言,结构化结果走非流式通道。

调用侧的兜底建议做成三层。解析层负责容错提取,把非JSON内容剥掉;校验层用jsonschema、pydantic这类工具做字段与类型校验,不通过就拒绝;修复层在失败时做有限次重试,把校验错误信息回灌给模型让它自己修。再配上超时、重试上限和幂等键,整条链路才算可控,失败也失败得清楚。

在多模型之间来回切换时,这些差异会被进一步放大。快米兔的模型API中转提供统一入口,注册送5元测试金、按量计费,用同一套代码就能横向比对不同国产模型在JSON mode下的真实表现:谁的schema遵循度高、谁更容易加围栏、谁的截断更频繁。把验证成本压到几元钱,比对着文档猜要靠谱得多,具体支持范围与计费细节以官方说明为准。

落到实践上,schema设计有几条经验值得先定下来:结构尽量扁平,嵌套别超过两层;必填项只留真正必要的;能枚举就别用自由文本;上线前用小样本压测,重点看空值、超长输入和并发下的表现。结构化输出不是一个可以一开了之的开关,而是一段需要设计与验证的链路,格式的确定性最终来自调用侧的工程约束,而不是模型的一句承诺。