面试知识库
进阶

结构化输出与约束解码#

一句话答案#

结构化输出要保证 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 ModeAPI 参数强制输出合法 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 次仍失败 → 降级(默认值/兜底模板/转人工)
plaintext

5. 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 强约束,杜绝工具幻觉。