最开始注意到这个问题,是因为在看 Mastra 的结构化解析时,发现不同模型、不同厂商、不同 API 对结构化输出的支持并不一样。
有些所谓“结构化输出”,其实只是把要求写进提示词里:请输出 JSON、不要输出解释、字段必须包含哪些内容。模型会更倾向于照做,但它本质上仍然是在自由生成。输出之后,框架再尝试解析、校验,失败了就修复或重试。
这和 OpenAI response_format: { type: "json_schema", json_schema: ... } 这类真正的 Structured Outputs 不完全一样。后者的关键不只是“让模型更听话”,而是在生成阶段就限制模型能输出什么。
普通生成为什么容易破坏 JSON
大语言模型生成文本时,本质上是在逐 token 预测。
给定当前上下文,模型会对整个词表里的所有 token 打分,然后从中选出下一个 token。比如已经生成:
{
"name":下一步模型仍然是在整个 vocabulary 里选择。它可能继续输出字符串、数字、null、左括号,也可能输出不符合当前位置 JSON 语法的内容。
这就是 unconstrained decoding。模型只是“概率上觉得这样像 JSON”,不是“规则上必须生成合法 JSON”。
所以它很容易出现:
- 少括号
- 多逗号
- enum 拼错
- 类型错误
- 字段遗漏
- JSON 前后混入自然语言解释
提示词、few-shot、低 temperature 都能降低这些问题的概率,但不能从机制上禁止非法 token。
自回归会让输出逐渐靠向 JSON
不过,普通生成并不是完全随机乱写。因为 LLM 是自回归生成,前面已经生成的 token 会进入上下文,继续影响后面 token 的概率分布。
如果模型已经生成了:
{
"status":那么后面继续生成 JSON 值、逗号、字段名或右括号的概率会明显升高。模型在训练中见过大量 JSON,因此它知道这种前缀通常应该怎样延续。
所以,提示词里要求 JSON、前几个 token 已经进入 JSON 形态、temperature 又比较低时,模型确实会越来越容易沿着 JSON 格式写下去。
但这仍然只是概率上的靠齐。模型学到的是 JSON 的常见模式,不是像编译器一样严格维护语法栈。对象一长、嵌套一深、字段约束一多,仍然可能出现少括号、错 enum、漏字段或混入解释文本。
因此:
自回归上下文:让合法 JSON 的概率变高
约束解码:让非法 token 的概率变成 0约束解码做了什么
Constrained decoding 的关键,是在模型生成每一个 token 时,动态限制“下一步允许输出哪些 token”。
普通生成大致是:
上下文 -> 模型输出全词表 logits -> 采样一个 token约束解码则是:
上下文 -> 模型输出全词表 logits -> 屏蔽非法 token -> 采样一个合法 token例如 schema 要求:
{
"type": "object",
"properties": {
"status": {
"enum": ["success", "failed"]
}
},
"required": ["status"]
}当模型已经生成:
{"status": "普通生成里,模型可能输出 success、failed,也可能输出 ok、done 或别的内容。
约束解码里,推理引擎会根据 schema/grammar 判断:这里接下来只能生成能组成 success 或 failed 的 token。其它 token 会被 mask 掉,概率变成 0,模型根本选不到。
这就是“生成后校验”和“生成中约束”的区别。
Schema 如何参与生成
真正的结构化输出通常会先把 JSON Schema 转成 grammar。OpenAI 官方文章里提到,他们会把 schema 转成 context-free grammar,再在每一步生成时根据 grammar 计算合法 token。
可以粗略理解成:
JSON Schema -> Grammar -> 每一步合法 token 集合 -> mask 非法 token比如 schema 要求输出对象、字段名、数字、枚举值,推理引擎就会在每个位置判断当前还能接什么。
已经生成:
{"age":下一步就应该允许数字或空白,而不应该允许随便输出 "abc"、[ 或一段自然语言。
所以这里的关键不是模型突然“学会了 schema”,而是推理层在采样前收窄了模型的选择空间。
为什么这和推理层关系很大
Transformer 模型本身仍然只是输出 logits。它并不知道应用层一定要一个合法 JSON 对象。
真正让非法 token 不能出现的,是推理引擎或模型服务在 logits 后面做了控制:根据 schema/grammar 动态 mask token。
所以,同一个模型在不同运行方式下,结构化输出能力可能不同:
普通 generate:自由生成
提示词要求 JSON:更像 JSON,但仍可能坏
生成后 parse + retry:坏了再补救
约束解码:生成时不允许非法 token这也是为什么结构化输出不能只看“模型支不支持”,还要看运行时、推理引擎和 API provider 是否真的支持 constrained decoding。
一句话总结:
模型的自回归能力让输出更容易靠向 JSON;runtime 的约束解码让输出不能偏离 JSON。