AI

结构化输出的真正边界:从提示词约束到约束解码

结构化输出不只是让模型更会写 JSON,关键在于推理阶段是否限制了模型能选择的 token。

最开始注意到这个问题,是因为在看 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": "

普通生成里,模型可能输出 successfailed,也可能输出 okdone 或别的内容。

约束解码里,推理引擎会根据 schema/grammar 判断:这里接下来只能生成能组成 successfailed 的 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。
Back to Blog

Related Posts

View All Posts »