结构化输出与约束解码#
一句话答案#
结构化输出要保证 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、vLLM 的 guided decoding、OpenAI Structured Outputs。
代价:构建/查询合法 token 掩码有开销;过严的 Grammar 可能压制模型表达力(被迫填不合理的值)。
4. 工程兜底:校验 + 重试链路#
即使用了 Schema,仍要在 Runtime 做防御:
LLM 输出 → JSON 解析
├── 解析失败 → 抽取 ```json 块/截取首个 {..} 再试
├── Schema 校验失败 → 把校验错误回灌 LLM 让其修正(带原输出+错误信息)
└── 重试 N 次仍失败 → 降级(默认值/兜底模板/转人工)plaintext5. Schema 设计建议#
- 字段尽量用 enum 收敛取值,杜绝自由 string
- 必填字段标 required;可空字段显式允许 null,别让模型猜
- 嵌套别太深(≤2–3 层),深嵌套出错率与 token 成本都升高
- 给字段写 description,等于给模型的”填表说明”
面试回答(2分钟版)
保证 LLM 一定输出合法 JSON 有四级手段,从弱到强。最弱是 Prompt 约束,写”只输出 JSON 不要解释”加几个示例,但完全靠模型自觉,会翻车。第二级是 JSON Mode,API 层强制输出可解析的 JSON,但只保证格式合法不保证字段对。第三级是 Function Calling 或 response_format 传 JSON Schema,模型按字段填值,结构和类型基本能保证。最强是约束解码,原理是解码时根据语法或正则算出下一个合法 token 集合,把非法 token 的概率置成负无穷,所以采样在语法层面只能落在合法范围内,是最强的方案(不过这是语法层面的强制,实践中仍可能因 max_tokens 截断、token 边界不对齐导致输出不完整,不是绝对的 100%),代表是 Outlines、vLLM guided decoding、OpenAI Structured Outputs。但光靠模型还不够,工程上一定要加校验加重试兜底:解析失败先尝试抽取 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 成本,扁平化更稳
项目实践(DocMind): DocMind 走 Spring AI 原生 Function Calling,工具参数与返回都绑定 JSON Schema,LLM 按字段填值而非自由生成;工具结果不经 LLM 重新组织,而是通过 AgentToolContext(ThreadLocal 侧信道)直接回传 Agent,从源头规避了”LLM 改写结构化结果”导致的字段污染。对工具名做白名单校验,参数用 enum/required 强约束,杜绝工具幻觉。