约束解码的双刃剑——完美 JSON 背后的推理税
结构化输出(Structured Output)让你告别 JSON 解析失败,但它有个没人提的代价——约束解码会在推理任务上偷走你 10-30% 的准确率。这篇文章拆解这个 tradeoff 的底层原理,以及生产中怎么用好这把双刃剑。
上周我在做多 Agent 系统时加了一个功能:让 Agent 输出结构化数据,直接作为下游服务的入参。开箱即用的
strict: true,完美 JSON,零解析错误。我心想,这东西太香了,早该用。
然后我顺手跑了一轮 eval。准确率掉了 12%。
不是 JSON 解析的问题——解析成功率 100%。是 Agent 的决策质量下降了。它输出了格式完美的 JSON,但选错了工具、填错了参数、推理链断了。这篇就聊聊这个反直觉的事实:结构化输出在消灭一种问题的同时,可能制造另一种问题。
约束解码到底在干什么
先说清楚机制。你调用 API 传一个 JSON Schema,模型返回符合这个 schema 的输出。这不是靠 prompt 里写 "请返回 JSON" 就能做到的——那叫 prompt-and-pray,失败率 5-20%。
真正的结构化输出走的是 约束解码(Constrained Decoding)。
每一步生成时,推理引擎维护一个有限状态机(FSM),追踪当前生成位置在 schema 中的状态。然后遍历词汇表,挑出所有"在当前状态下合法"的 token,把不合法的 mask 掉。模型只能从合法 token 里选。
听起来靠谱。也确实靠谱——解析失败率从 5-20% 降到 <0.1%。OpenAI 的 Strict Structured Outputs、Anthropic 的 tool use strict 模式、vLLM 的 XGrammar 后端,底层都是这个逻辑。
XGrammar 的优化做得尤其漂亮。词汇表里大部分 token 是"上下文无关"的——不管你在 JSON 的什么位置,它们是否合法都不受影响。这些 token 可以预计算一次就缓存起来。每步只需要实时检查那几百个"上下文相关"的 token。实测下来,约束解码的开销只有总推理时间的 1-5%,基本可以忽略。
好,机制讲完了。问题出在哪?
当模型被强制走二线路径
约束解码的代价不在性能,在质量。
每步生成时,模型原本有一个"最想选择的 token"——概率最高的那个。如果这个 token 在当前 schema 状态下不合法,被 mask 掉了,模型只能从次优的候选里选。一次次的"被迫次优选择"累积起来,推理链就会发生偏移。
想想一个数学推理的例子。模型原本想输出 "Step 1: calculate the derivative of x^2",但 schema 强制它先填一个 "answer" 字段。它被迫在没推完的情况下,先输出一个结论。然后才能回头补 reasoning。这种顺序颠倒对推理质量是致命打击。
NeurIPS 2024 的一项研究发现,在复杂推理任务上,约束解码相比自由文本输出再解析,准确率下降了 10-15%。而 2026 年 CRANE 等人的最新研究把这个数字更新到了 10-30%,取决于任务类型和 schema 复杂度。
最关键的发现是:schema 字段顺序就是生成顺序。如果你把 answer 字段放在 reasoning 前面,模型会在推理完成之前就承诺一个答案。之后所有的 reasoning 都是在"论证已选的答案",而不是自由探索。
不同 provider 的取舍差异
这里有个有意思的区别。
OpenAI 的 Strict Structured Outputs 做的是真正的硬约束解码。schema 一旦编译成 FSM,模型就不可能输出不合法的 token。代价就是上述的质量损耗。它的底层引擎已经从自家实现换成了微软的 llguidance(Rust 写的,每 token 约 50μs 的开销),覆盖了更完整的 JSON Schema 子集。
Anthropic Claude 走的是另一条路。它的 structured output 底层用的是 tool use 调用——模型被训练得"很擅长遵循工具 schema",但没有强制的 token masking。它的失败率是 0.5-5%(不是 <0.1%),但在复杂推理任务上,语义质量明显更好。
Gemini 的 response_schema 走的是偏 OpenAI 路线的硬约束,但对微调模型有个有趣的 caveat:严格 schema 下微调模型的质量可能下降明显,因为 SFT 过程本身已经改变了模型的 token 分布,再叠一层 masking,偏移更严重。
这不是谁优谁劣的问题,是一个 engineering tradeoff:
- 硬约束:100% 合法的输出,可能 10-30% 的推理断点
- 软约束:95-99% 合法的输出,推理质量几乎无损
你用哪一种,取决于你的场景。
自托管的选择:XGrammar vs Instructor vs Outlines
如果你自己部署模型,选择就不是 API 参数,而是库。
XGrammar(MLC-AI)是现在 vLLM 和 SGLang 的默认约束解码后端。它的核心优化是词汇表分区:大部分 token 是"上下文无关"的,可以被预计算和缓存。每步只需要检查几百个"上下文相关"的 token,开销小于 40μs。XGrammar-2 在 2026 年 5 月刚发布,进一步做了基于 token ID 区间的快速过滤。
Instructor 走的是另一条路线:不修改推理过程,而是在应用层做 Pydantic 验证 + 自动重试 + 反馈闭环。第一次输出 parse 失败,把错误信息塞回 prompt 让模型重新生成。好处是支持所有 provider,坏处是额外消耗 token 和延迟。对于非关键路径,这是个很务实的方案。
Outlines 做的是纯粹的语法约束,支持正则表达式和完整 CFG(上下文无关文法)。如果你的 schema 超出了 JSON Schema 的表达能力——比如需要 field A 的值约束 field B 的可选性——Outlines 能做到。代价是编译时间,尤其大 enum 场景下可能分钟级。
选哪个取决于你的部署方式。用 API 的:直接 provider 的 native strict mode。自托管的:XGrammar 默认,Instructor 兜底。需要复杂语法约束的:Outlines。
生产中怎么用
过去几个月踩了不少坑,总结出三条实用规则:
规则一:低推理任务,放心硬约束。
数据提取、分类、格式化输出——这些任务不需要模型"想",只需要模型"填"。严格 schema 是纯收益,没有质量代价。金融数据提取的案例验证了这一点:验证失败率从 27% 降到 2%,内容质量没下降。
规则二:高推理任务,分两步走。
先让模型自由输出推理过程(不限格式),再用第二次调用提取结构化结果。代价是两次 API 调用、双倍 token 消耗,但推理质量几乎不受影响。CRANE 提出的方案更进一步——在同一个生成过程中用 tag 标记"自由窗口"和"约束窗口",推理部分不约束,结构化部分再约束。实测比全程约束高 10 个百分点的准确率。
规则三:永远验语义,不只验语法。
schema 合法 ≠ 输出正确。一个路由 Agent 返回 {"target": "memory_agent"},schema 完美合法,但正确答案是 "search_agent"。你需要加语义断言:不是说"这个字段存在且类型对",而是"这个值在业务上是对的"。
还有一条容易被忽略的:预编译 schema 缓存。 如果你的系统用了 10 种不同的 schema,不要在每次请求时重新编译。XGrammar 和 llguidance 的冷启动开销都在毫秒级,但 Outlines 在某些复杂 schema 下可能分钟级。启动时预热所有 schema 的 grammar,能避免流量高峰时的雪崩效应。
所以结构化输出到底要不要用
要。但要知道代价。
对 Agent 系统来说,结构化输出是不可或缺的基础设施——tool calling 本身就是结构化输出。没有它,你的 Agent 每三步就 parse 失败一次,根本没法跑。
但迷信"strict: true 解决一切"的人,会在推理任务上吃亏。最危险的场景是:生产环境跑了一周,parse 成功率 100%,团队欢呼。一个月后发现准确率比预期低,但谁也说不清是哪掉的——因为 evals 只测了格式合规,没测语义正确。
解决方案很简单:为你的任务跑一次对比 eval。 同一组 prompt,跑一遍 strict mode,跑一遍 free text + 后处理。数值会告诉你该不该用硬约束。
我测完之后,现在我对不同的 tool 用不同策略——数据类 tool 硬约束,推理类 tool 软约束。这不是完美的方案,但至少是有意识的选择。
做 Agent 系统最吸引我的地方就在于:每个 architectural decision 都有两面性。没有银弹,只有对 tradeoff 的理解和测量。
你要决定你的 Agent 是输出得"好看但笨"还是"有点丑但聪明"。这取决于你更怕哪种失败。
评论
发表评论