结构化输出与约束解码#
一句话答案#
结构化输出要保证 LLM 一定吐出合法、符合 Schema 的 JSON,手段从弱到强是「Prompt 约束 → JSON Mode → Function Calling/Tool Schema → 约束解码(Grammar/正则)」,工程上还要配「校验 + 失败重试」作为保底,约束解码是语法层面强制最强的方案(极强但非绝对,仍受 max_tokens 截断、token 边界等影响)。
核心要点
1. 为什么”让 LLM 输出 JSON”会翻车#
- 多输出解释性文字(“好的,这是结果:{…}”)破坏解析
- 缺逗号/多尾逗号/单引号/未转义换行等非法 JSON
- 字段缺失、类型错(数字写成字符串)、枚举值编造
- Markdown 包裹(
json ...)
2. 四级保证手段(从弱到强)#
| 手段 | 机制 | 保证强度 |
|---|---|---|
| Prompt 约束 | ”只输出 JSON,不要解释” + few-shot 示例 | 弱,靠模型自觉 |
| JSON Mode | API 参数强制输出合法 JSON(保证可解析,不保证结构) | 中,格式合法但字段不保证 |
| Function Calling / response_format=schema | 传 JSON Schema,模型按字段填值 | 强,结构+类型基本保证 |
| 约束解码(Constrained Decoding) | 解码时按语法/正则屏蔽非法 token | 最强,语法层面强制合法 |
3. 约束解码原理#
在每个 decode step,根据已生成前缀计算”下一个合法 token 集合”(由 JSON 语法 / 正则 / GBNF Grammar 决定),把非法 token 的 logits 置为 -inf,使采样在语法层面只能落在合法集合内(注意这是语法层面的强制,实践中仍可能因 max_tokens 截断、tokenizer 与 grammar 终结符边界不对齐等导致非法/不完整输出)。代表实现:llama.cpp GBNF、Outlines、XGrammar、vLLM 的 structured outputs(旧名 guided decoding)、OpenAI Structured Outputs、Anthropic 结构化输出(官方文档说明用的是编译成 grammar 的约束采样)。
代价:构建/查询合法 token 掩码有开销;过严的 Grammar 可能压制模型表达力(被迫填不合理的值)。
4. 工程保底:校验 + 重试链路#
即使用了 Schema,仍要在 Runtime 做防御:
LLM 输出 → JSON 解析
├── 解析失败 → 抽取 ```json 块/截取首个 {..} 再试
├── Schema 校验失败 → 把校验错误回灌 LLM 让其修正(带原输出+错误信息)
└── 重试 N 次仍失败 → 降级(默认值/保底模板/转人工)plaintext5. Schema 设计建议#
- 字段尽量用 enum 限定取值,杜绝自由 string
- 必填字段标 required;可空字段显式允许 null,别让模型猜
- 嵌套别太深(≤2–3 层),深嵌套出错率与 token 成本都升高
- 给字段写 description,等于给模型的”填表说明”
6. 主流 API 的写法(截至 2026-09,以各家官方文档为准)#
| 平台 | 结构化输出参数 | 工具参数严格模式 | 要点 |
|---|---|---|---|
| OpenAI Chat Completions | response_format: {"type": "json_schema", "json_schema": {"name", "strict": true, "schema"}} | function 定义里 strict: true | 严格模式要求每层 additionalProperties: false、所有字段列进 required,可选字段用 ["string", "null"] 这类 null 联合表达;模型拒答时返回单独的 refusal 字段;同一 schema 首次请求有额外延迟 |
| OpenAI Responses API | text: {"format": {"type": "json_schema", "name", "strict": true, "schema"}} | 同上 | 官方把 Structured Outputs 定位为 JSON mode(json_object)的升级,建议能用就不用 JSON mode |
| Anthropic Messages API | output_config: {"format": {"type": "json_schema", "schema"}}(已 GA,beta 期叫 output_format) | tool 定义里 strict: true | 约束采样;首次请求要编译 grammar,编译结果缓存 24 小时;不支持递归 schema、数值范围、字符串长度等约束,官方 SDK 会把这些约束从发送的 schema 里去掉、改到 description,并在本地按原 schema 校验 |
| vLLM(自部署) | OpenAI 兼容的 response_format,或请求体 structured_outputs: {"json" / "regex" / "choice" / "grammar": ...} | — | 旧的 guided_json、guided_regex 等字段已在 v0.12.0 移除;后端可选 xgrammar、guidance、outlines 等 |
不同家的 schema 支持子集不一样:同一份 Pydantic 模型在一家能用,换一家可能因为 minimum、递归引用等被拒,多供应商时要按最小公共子集设计 schema,或者让网关/SDK 做转换。
面试回答(2分钟版)
保证 LLM 一定输出合法 JSON 有四级手段,从弱到强。最弱是 Prompt 约束,写”只输出 JSON 不要解释”加几个示例,但完全靠模型自觉,会翻车。第二级是 JSON Mode,API 层强制输出可解析的 JSON,但只保证格式合法不保证字段对。第三级是 Function Calling 或 response_format 传 JSON Schema,模型按字段填值,结构和类型基本能保证。最强是约束解码,原理是解码时根据语法或正则算出下一个合法 token 集合,把非法 token 的概率置成负无穷,所以采样在语法层面只能落在合法范围内,是最强的方案(不过这是语法层面的强制,实践中仍可能因 max_tokens 截断、token 边界不对齐导致输出不完整,不是绝对的 100%),代表是 Outlines、XGrammar、vLLM 的 structured outputs,云 API 里 OpenAI 的 Structured Outputs 和 Anthropic 的结构化输出也是这类。但光靠模型还不够,工程上一定要加校验加重试:解析失败先尝试抽取 JSON 块,Schema 校验失败就把错误信息回灌让模型修正,重试几次还不行就降级到默认值或转人工。Schema 设计上,能用 enum 就别用自由 string,必填标 required,嵌套别太深,每个字段写 description 相当于给模型填表说明。
追问与易错
追问方向:
- JSON Mode 和约束解码有什么区别? → JSON Mode 只保证”能 JSON.parse”,不保证字段齐全/类型对/枚举合法;约束解码在 token 级按 Schema/Grammar 强制,连结构都保证。前者是 API 便捷开关,后者是解码器层面的硬约束
- 约束解码会不会降低模型质量? → 会有风险:Grammar 过严时模型被迫在合法集合里选,可能填不合理的值(“宁可合法不可正确”)。缓解是 Schema 留出 enum 之外的”other/unknown”出口,别把模型逼到死角
- 模型不支持原生 Schema 怎么办? → 退到 Prompt + 校验重试,或在推理框架侧用 Outlines/Guidance 这类外挂约束解码,与具体模型解耦
- 字段缺失但格式合法怎么处理? → 这正是 JSON Mode 的盲区,必须用 JSON Schema 校验(required)+ 错误回灌重试补齐
易错点:
- ❌ “用 JSON Mode 就万无一失” → 它只保证可解析,字段和类型仍可能错,必须叠加 Schema 校验
- ❌ “解析失败直接报错” → 应先尝试抽取/修复,再回灌重试,最后才降级
- ❌ “Schema 嵌套越细越好” → 深嵌套抬高出错率和 token 成本,扁平化更稳
工程实践要点:
- 工具参数用 JSON Schema 声明,参数上用 enum/required 强约束,工具名做白名单校验,挡住模型编造不存在的工具;
- 工具返回的结构化结果如果要交给下游程序用,可以直接回传给调用方,不要再让 LLM 复述一遍,避免字段在复述时被改写;
- 结合项目时可以讲:约束放在哪一层(Schema / 解码 / 校验)、解析失败率和重试率用什么数据验证。