一文讲透大模型格式化输出:从预训练先验到约束解码的五层保障

为什么"请只输出 JSON"的哀求会失灵,而 response_format 一开就稳?沿训练到推理的完整链路拆解格式保障的五层机制:预训练给先验、后训练给服从、提示接口表达意图、约束解码给硬保证、系统校验兜底。结合 PICARD、Outlines、XGrammar、SGLang 与 Let Me Speak Freely 等论文,讲清 FSM 掩码的原理、tokenizer 错位难题、约束解码的分布扭曲代价,以及生产系统为什么五层全都要。

每个写过 LLM 应用的人都经历过这个瞬间:prompt 里写了三遍”请只输出 JSON,不要包含任何其他文字”,模型回了一句”好的,以下是您要的 JSON:” —— JSON.parse 当场爆炸。可是把 API 参数换成 response_format={"type": "json_schema", ...},同一个模型忽然就百发百中了。哀求和开关之间,差的不是运气,是机制:前者在提高一个概率,后者在物理上删除所有非法选项。

这篇文章回答一个完整的问题:大模型的格式化输出到底是怎么被保障的——从预训练、后训练、提示接口,到解码时刻、再到系统兜底,每一层各自做了什么、能保证什么、保证不了什么。

先给全文的一句话主线:

格式保障是一条从”概率”走向”确定性”的光谱:训练和提示只能提高输出合法的概率,唯一的硬保证发生在解码时刻——把非法 token 的概率置零;而硬保证有自己的代价,所以生产系统把五层全部叠着用。

flowchart TB
    L1["第 1 层 · 预训练<br/>格式先验:见过海量 JSON/代码,括号配对成为统计本能"]
    L2["第 2 层 · 后训练<br/>格式服从:SFT/RLHF 让模型愿意听话(InstructGPT、ToolLLM)"]
    L3["第 3 层 · 提示与接口<br/>意图表达:few-shot、prefill、response_format 开关"]
    L4["第 4 层 · 约束解码<br/>硬保证:FSM/CFG 掩码,非法 token 概率 = 0(Outlines、XGrammar)"]
    L5["第 5 层 · 系统兜底<br/>语义校验:validate → retry/repair,文法管不到的归它"]
    L1 --> L2 --> L3 --> L4 --> L5
    L1 -.- P1["P(合法) ≈ 高"]
    L2 -.- P2["P(合法) ≈ 更高"]
    L4 -.- P4["P(语法非法) = 0"]
    L5 -.- P5["语义错误也能兜住"]

第 0 层认知:为什么自回归模型天然不保证格式

在讲保障之前,先讲清楚为什么需要保障。这是全文的第一性约束。

自回归语言模型每一步做的事是:给定已生成的前缀 x<tx_{<t},在整个词表 VV 上算一个 softmax 分布 p(xtx<t)p(x_t \mid x_{<t}),然后从中采样一个 token。两个致命的性质:

  1. softmax 不产生零。 词表里每个 token 都有非零概率——包括那个会毁掉你 JSON 的多余逗号。温度、top-p 收得越紧,破格概率越低,但永远不为零(采样机制的细节可参考站内《采样不是玄学:从 logits 到 temperature、top-k、top-p》)。
  2. 格式是全局性质,采样是局部决策。 “整个输出是一个合法 JSON”是对完整序列的约束,但模型每步只做一个局部的 token 选择,没有任何内建机制回头检查括号是否配对。生成 5000 个 token,每步 99.9% 正确,全对的概率也只有 0.99950000.7%0.999^{5000} \approx 0.7\%……好在错误不独立、格式 token 占比有限,实际没这么惨——但这个粗算足以说明:指数衰减的全局正确率,靠局部概率是撑不住的。

于是问题变成:在哪些环节、用什么手段,把这个概率往 1 推——以及在什么环节可以直接把它钉死在 1。

第 1 层:预训练——格式先验从哪来

模型并非从零学格式。预训练语料里有海量代码、JSON、HTML、Markdown,“{ 之后终将出现 }”这类配对规律是语料里最强的统计信号之一。这就是为什么裸的 base model 补全 JSON 也大体像样——括号配对、引号闭合已经是它的”肌肉记忆”。

但要看清这层给的是什么:一个先验分布,不是承诺。它决定了后面所有层的起点高度——先验越好,后训练要掰的越少,约束解码需要”干预”的步数越少。也仅此而已:base model 会在 JSON 后面顺手续写一段解释、会模仿语料里”JSON 嵌在 Markdown 代码块里”的样子——因为语料里的 JSON 本来就长这样。

第 2 层:后训练——从”会写 JSON”到”愿意只写 JSON”

预训练给了能力,后训练解决服从。这层有三条线:

通用指令跟随。 InstructGPT (arXiv:2203.02155) 确立的 SFT + RLHF 配方,本质是把”用户说只输出 JSON,就真的只输出 JSON”这类行为,从语料里的低概率路径变成对齐后的高概率路径。你感受到的”新模型越来越听话”,大头来自这里。

面向格式的专项训练。 工具调用(function calling)是格式要求最苛刻的场景,也催生了专门的训练数据构造:Gorilla (arXiv:2305.15334) 用 API 文档构造调用样本做检索感知微调,让模型学会输出准确的 API 调用;ToolLLM (arXiv:2307.16789) 更进一步,用 16000+ 真实 API 自动生成多步调用轨迹做 SFT。今天各家模型的 function calling 能力,背后都是这类”格式即任务”的专项数据。厂商的 tool-use 微调通常还配合特殊 token(如工具调用的开始/结束标记),把”进入格式模式”变成词表层面的显式状态切换。

可验证的度量。 训练要有标尺。IFEval (arXiv:2311.07911) 的思路是只测程序可验证的指令(“不少于 400 词""提到 AI 至少 3 次”这类),约 500 条 prompt、25 类约束,正确与否一行代码就能判断(我在《IFEval 学习笔记》里拆过细节)。格式服从率从此可以进训练看板、做发布门禁。

这一层的天花板也要说透:后训练把服从率推到 95%+,但推不到 100%。 而且失败不均匀——schema 越深、字段越多、上下文越长、温度越高,崩得越快。对一个每天跑一百万次的流水线,99% 的服从率意味着每天一万次解析失败。这就是为什么还需要第 4 层。

第 3 层:提示与接口——表达意图,而不是获得保证

这层是大多数人最熟悉的,也是最容易高估的。快速过一遍从弱到强:

  • 写清楚 + few-shot 示例:把目标 schema 和一两个完整示例放进 prompt。有效,因为你在用上下文把第 1、2 层的先验”对准”到目标格式上。
  • 预填充(prefill):直接替模型把回答开头写成 {,等于把”要不要说客套话”这个分叉从采样空间里剪掉了。四两拨千斤,但只管开头,管不了结尾。
  • response_format / structured_outputs 参数:注意,这不是一条更响亮的提示,而是一个开关——它告诉推理引擎”启用第 4 层”。“请输出 JSON”和 response_format=json_schema 的区别,不是措辞强弱,是机制类别(vLLM 里这个参数如何一路变成 StructuredOutputsParams,我在《从 vLLM ChatCompletionRequest 看懂大模型请求参数》第 8 节从源码走过一遍)。

记住这层的定位:提示负责把”你想要什么”表达清楚,保证要去下一层拿。

第 4 层:约束解码——唯一的硬保证

这是全文的重头戏。思路一句话就能说完,工程却演化了四代。

原理:在采样之前删掉非法选项

每一步解码时,用一个跟踪格式状态的自动机算出”当前位置哪些 token 是合法的”,把非法 token 的 logit 置为 -\infty,再归一化采样:

p~(xtx<t)  =  p(xtx<t)1 ⁣[xtVlegal(st)]xVlegal(st)p(xx<t)\tilde{p}(x_t \mid x_{<t}) \;=\; \frac{p(x_t \mid x_{<t})\cdot \mathbb{1}\!\left[x_t \in V_{\text{legal}}(s_t)\right]}{\sum_{x' \in V_{\text{legal}}(s_t)} p(x' \mid x_{<t})}

其中 sts_t 是自动机当前状态,Vlegal(st)V_{\text{legal}}(s_t) 是该状态下的合法 token 集。模型依然自由地在合法集合内按自己的偏好采样,但非法 token 被物理移除。语法错误的概率不是”很小”,是——这就是”哀求”和”开关”的本质区别。

四代演化:从”每步跑解析器”到”约束反而加速”

第一代:验证式(每步问解析器)。 PICARD (arXiv:2109.05093) 在 text-to-SQL 上做增量解析:每步取 top-k 候选 token,逐个问解析器”接上它还合法吗”,非法的拒掉。有效但贵——每个 token 都要跑解析。Synchromesh (arXiv:2201.11227) 把思路推广成”补全引擎”(completion engine),除语法外还能检查作用域、类型这类语义约束。

第二代:编译式(预处理换运行时)。 转折点是 Outlines 背后的 Willard & Louf (arXiv:2307.09702):把正则表达式/JSON schema 编译成有限状态机(FSM),并离线为词表建立索引——每个 FSM 状态直接映射到它的合法 token 集合。运行时每步只需查表,掩码开销与词表大小近乎无关。“约束解码太慢”从此不再成立。Geng et al. (arXiv:2305.13971) 则证明了用上下文无关文法(CFG)做约束,不需要任何微调就能让通用模型胜任封闭信息抽取、实体消歧等结构化任务——约束解码是微调的替代品这个视角,对资源有限的团队非常实用。

第三代:工程极致化。 JSON schema 用正则逼近会丢失递归结构(嵌套对象要 CFG 才能精确表达),而 CFG 约束需要运行时维护栈,比 FSM 贵得多。XGrammar (arXiv:2411.15100) 的解法是把 token 分成两类:上下文无关的(仅看自动机状态就能判合法,占绝大多数)离线缓存进自适应掩码,上下文相关的(要看栈)才在运行时用持久化执行栈检查——把 CFG 级别表达力的掩码开销压到接近查表。这也是它成为 vLLM 等推理引擎默认结构化后端之一的原因。

第四代:约束反而加速。 SGLang (arXiv:2312.07104) 的压缩 FSM 观察到一个漂亮的事实:格式里有大量确定性片段——当 FSM 走到 {"name": " 这段路径上,下一串 token 根本没有选择。那为什么还要一个一个 token 地”生成”?直接把整段拼进序列,跳到下一个分叉点(jump-forward decoding)。约束从”每步的额外开销”变成了”跳过解码步的加速器”——论文报告整体最高 6.4× 的吞吐提升(其中结构化解码是贡献之一)。顺带一提,“一次前向多个 token”这个动作和投机解码神似,只不过这里的”草稿”来自文法的确定性,连验证都不需要(投机解码那条线可看《Medusa 深读》)。

被低估的难题:tokenizer 错位

约束解码最阴险的坑不在算法,在词表。语法定义在字符上,模型却按 BPE token 生成——两者边界不对齐。比如 ]} 在很多词表里是一个 token:它一次跨过了两个语法单元;反过来一个语法单元(如字符串里的转义序列)也可能被劈进多个 token。所以自动机不能建在字符上,必须建在 token 词表上——把每个 token 视为”一串字符的复合转移”。这正是 Willard & Louf 的核心贡献(词表索引本质上就是把字符级 FSM 提升为 token 级 FSM),也是 XGrammar 大量工程复杂度的来源。自己手搓约束解码的团队,九成的 bug 出在这里。

硬保证的代价:两笔隐性账

灰度时刻到了。第 4 层不是免费午餐,有两笔账要算:

第一笔:格式挤压思考。 Let Me Speak Freely? (arXiv:2408.02442) 系统测量了格式限制对能力的影响:在推理类任务上,越严格的格式约束,性能下降越明显——直觉上,模型没有了”打草稿”的空间,被迫把第一个 token 就落在 schema 里。工程上的标准缓解是两阶段:先让模型自由推理(CoT),再把结论转成结构化输出;或者干脆在 schema 里留一个 reasoning 字段,把草稿纸画进表格里。

第二笔:分布扭曲。 逐步掩码采出来的分布,不等于模型在”所有合法输出”上的真实条件分布。Grammar-Aligned Decoding (arXiv:2405.21047) 把这个问题讲透了。用一个手算例子看清它:

设输出两个 token,每个是 ab,文法只禁止 aa。模型的真实分布:P(a)=0.9P(a)=0.9,且第二步仍然 P(a)=0.9P(a\mid\cdot)=0.9

  • 模型在合法集上的真实条件分布P(ab)=0.09, P(ba)=0.09, P(bb)=0.01P(ab)=0.09,\ P(ba)=0.09,\ P(bb)=0.01,归一化后 ab:47%, ba:47%, bb:5%ab: 47\%,\ ba: 47\%,\ bb: 5\%
  • 逐步掩码的实际结果:第一步 ab 都还”有救”,不掩码,于是 90% 走 a;走了 a 之后第二步被迫掩掉 a,强制出 b。最终 ab:90%, ba:9%, bb:1%ab: 90\%,\ ba: 9\%,\ bb: 1\%

ab 从 47% 被抬到 90%——贪心的局部掩码把”死路前的最后一步”的概率错误地转移给了幸存者,模型等于被文法拖着走了一条它本来不太想走的路。GAD 提出的 ASAp 用采样反馈迭代逼近真实条件分布,代价是更多采样。大多数生产场景可以忍受这点扭曲,但如果你在做数据合成或评测,这个偏差会系统性污染你的分布——知道它存在,比什么都重要。

第 5 层:系统兜底——文法管不到的,校验来管

约束解码保证的是语法合法,不是语义正确。这几类错误它无能为力:

  • schema 之外的约束:user_id 必须真实存在于数据库、end_date 必须晚于 start_date
  • 数值幻觉:格式完美的 JSON 里装着编造的数字;
  • 截断:输出长度触顶,JSON 在半空中断掉(语法自动机到 EOS 前都合法,它不知道”没写完”)。

所以最外层永远是应用代码:parse → validate(JSON Schema/Pydantic)→ 失败则带着错误信息 retry,或用修复器(如 json-repair 类工具)打补丁。第 5 层的存在不是前四层失职,而是职责边界:它是唯一能理解”业务对不对”的层。工程直觉:前四层做得越好,第 5 层的重试率越低——但它的代码一行都不能省。

全链路串讲:当你设置 response_format=json_schema 时发生了什么

把五层串起来走一遍(以 vLLM + XGrammar 后端为例,OpenAI 的 Structured Outputs、llama.cpp 的 GBNF 文法同理):

  1. 请求进入response_format 被解析成结构化输出参数,你的 JSON schema 被编译成文法/自动机(第 3 层交棒给第 4 层);
  2. 预填充阶段:prompt(可能含 few-shot 和 schema 说明)建立上下文先验(第 1、2、3 层就位);
  3. 逐 token 解码:每步 forward 得到 logits → 自动机给出合法掩码 → 非法 token 置 -\infty → 采样 → 自动机状态推进;确定性片段直接 jump-forward(第 4 层工作中);
  4. EOS:只有自动机处于接受状态时 EOS 才合法——保证不会生成半个 JSON(除非撞上长度上限);
  5. 返回后:你的代码 parse + 业务校验,不通过则重试(第 5 层收尾)。

速查表:五层保障一览

手段保证强度成本典型失效
1 预训练语料里的格式先验概率性·弱沉没成本续写多余解释、模仿语料噪声
2 后训练SFT/RLHF/工具调用专训概率性·强(~95%+)训练侧一次性深嵌套、长上下文、高温下崩格
3 提示与接口few-shot、prefill、开关参数概率性·看写法几乎为零开头客套话、结尾画蛇添足
4 约束解码FSM/CFG logit 掩码确定性(语法层)推理时掩码开销(已近乎归零)格式挤压思考、分布扭曲、截断
5 系统兜底validate → retry/repair确定性(语义层)重试延迟与费用无——它就是最后一道

结语:带走一个心智模型

“大模型如何保障格式化输出”这个问题,答案不是某一项技术,而是一条分层递进的保障链

  • 训练给先验:预训练让格式成为高概率路径,后训练让服从成为默认行为——它们把问题变简单,但给不了保证;
  • 解码给保证:FSM/CFG 掩码是全链路唯一的确定性环节,语法错误概率精确为零——代价是思考空间与分布保真度,用两阶段生成和对代价的清醒认知去平衡;
  • 系统给兜底:语义正确性超出文法的表达力,永远需要最外层的校验与重试。

下次再看到”请只输出 JSON”失灵,你知道那不是模型不听话,是你在第 3 层要一个只有第 4 层能给的东西。


参考来源

arXiv 论文

  • 指令对齐:Ouyang et al., Training language models to follow instructions with human feedbackarXiv:2203.02155
  • 工具调用训练:Patil et al., Gorilla: Large Language Model Connected with Massive APIsarXiv:2305.15334
  • 工具调用训练:Qin et al., ToolLLM: Facilitating Large Language Models to Master 16000+ Real-world APIsarXiv:2307.16789
  • 格式遵循评测:Zhou et al., Instruction-Following Evaluation for Large Language Models (IFEval) — arXiv:2311.07911
  • 增量解析约束:Scholak et al., PICARD: Parsing Incrementally for Constrained Auto-Regressive Decoding from Language ModelsarXiv:2109.05093
  • 语义约束解码:Poesia et al., Synchromesh: Reliable code generation from pre-trained language modelsarXiv:2201.11227
  • FSM 约束解码:Willard & Louf, Efficient Guided Generation for Large Language Models (Outlines) — arXiv:2307.09702
  • CFG 约束替代微调:Geng et al., Grammar-Constrained Decoding for Structured NLP Tasks without FinetuningarXiv:2305.13971
  • 高效文法引擎:Dong et al., XGrammar: Flexible and Efficient Structured Generation Engine for Large Language ModelsarXiv:2411.15100
  • 压缩 FSM 与跳跃解码:Zheng et al., SGLang: Efficient Execution of Structured Language Model ProgramsarXiv:2312.07104
  • 格式约束的能力代价:Tam et al., Let Me Speak Freely? A Study on the Impact of Format Restrictions on Performance of Large Language ModelsarXiv:2408.02442
  • 分布扭曲与矫正:Park et al., Grammar-Aligned DecodingarXiv:2405.21047

工程实践

站内相关