有一个问题,很多人用过 LLM 但从来没细想过:同一个模型,写自由文本的时候可能跑题、可能语无伦次,但一旦让它输出 JSON,几乎每一次都能通过 json.loads()。是模型的 JSON 能力比写作能力更强吗?不是。是因为 JSON 的格式约束,把模型的搜索空间压到了一个极小范围。

抽签游戏:模型是怎么选下一个词的
大语言模型生成文本,本质上是逐 token 抽样。token 是模型眼中的“词”,常见的词表大小在 3 万到 10 万之间。每生成一个 token,模型都要在词表上算一个概率分布,然后根据这个分布抽一个 token。这个概率分布,就是模型对“接下来该说什么”的全部理解。
如果没有任何外部约束,下一个 token 可以是词表里任何一个。模型只是把概率最高的那个挑出来。看起来模型很“懂”,其实它是在做一个天文数字规模的选择题。
把词表大小设为 3.2 万,生成 100 个 token 的文本,可能的序列数量是 32000^100。这个数大约 10^449。而宇宙里的原子总数大约是 10^80。换句话说,自由文本的搜索空间,比宇宙的原子数还多几百个数量级。
从宇宙原子数到单选题:JSON 如何压缩搜索空间
现在把同样的任务换成“生成一个符合 JSON 格式的对象”。JSON 的语法是严格定义的:对象以 { 开头,键必须是双引号字符串,键和值之间必须有 :,对象元素用 , 分隔,最后一个键值对后面不能有逗号。
这意味着,模型在生成 JSON 的过程中很多位置上,合法 token 集合缩小到个位数。举个例子:当模型刚输出一个 {,下一个合法 token 只有两种可能:"(键开始)或者 }(空对象)。如果加上 JSON Schema 约束,可能性甚至只剩一种。而当它输出完 : 之后,下一个 token 只能在字符串引号、数字、[、{、true、false、null 里面选。也就是说,模型在每一步面临的选择从数万骤降到十几种。
这个差距可以量化。假设自由文本平均每一步有 5 万种合法 token,而 JSON 约束下平均只有 20 种。生成长度 50 的输出,总搜索空间缩小了 (50000/20)^50 = 2500^50 ≈ 10^170 倍。没错,170 个数量级。这就是为什么模型“更擅长”生成 JSON:它不是更聪明了,而是每一道选择题的选项被你砍掉了三个数量级。
真正的格式保证来自 mask,而不是 prompt
但这里有一个至关重要的区分:格式约束到底来自哪?很多人以为是 prompt。你在 prompt 里写“请输出 JSON”,模型确实会更守纪律——但那只是模型根据训练数据学出来的“倾向”,不是保证。如果上下文变长、任务变难、或者 temperature 调得过高,模型照样会给你一个残缺的大括号。
真正可靠的 JSON 生成,靠的是解码时的硬约束。做法是:在每一步采样之前,用一个状态机(由 JSON 语法或 JSON Schema 编译而来)算出当前哪些 token 是合法的,然后把非法 token 的概率全部设为负无穷,再做归一化。模型连“犯错”的机会都没有。
这件事已经有代表性实现。Outlines 把 JSON Schema 编译成状态机,在生成时过滤 token,见Outlines 官方文档;Hugging Face 写过专门的约束生成博客,见constraining LLM generation;研究上,这篇Grammar-Constrained Decoding for Structured NLP Tasks without Finetuning描述了通用方法。这些实现输出的 JSON,语法错误率可以做到 0——注意,是 0,而不是 1%。
我最早读到这个实现时非常惊讶。我一直以为 ChatGPT 的 response_format 只是把 JSON 示例塞进 prompt。后来才意识到,真正可靠的结构化输出,靠的都是这种 hard constraint。OpenAI 的 structured outputs 也在做类似的格式保证,见官方文档。
一张表看清自由文本和约束 JSON 的本质区别
下面这张表总结了最重要的差异。
| 对比维度 | 自由文本 | 约束 JSON |
|---|---|---|
| 每一步合法 token 数量 | 约 3.2 万到 10 万 | 通常 1 到几十 |
| 50 步后的可能序列数 | 约 10^235 | 约 10^65 |
| 错误表现 | 跑题、语法错、逻辑漂移 | 语法错误率 0,但可能语义错误 |
| 格式保证来自 | 模型的概率倾向 | 解码阶段的硬约束 |
| 适合任务 | 写作、翻译、对话 | 工具调用、数据提取、API 输出 |
表格里的 10^235 和 10^65 是按上述假设估算的。它说明了一个反常识的结论:JSON 更容易正确生成,不是因为模型更擅长它,而是因为我们把模型生成每个字的自由度砍掉了。
约束会不会让模型变蠢?我踩过一次坑
我也担心过这个问题。最早用 Outlines 生成 JSON 时,我总觉得输出会比自由格式干瘪。于是我做了一个小实验:同一个产品描述任务,一半让模型自由写,另一半约束成 JSON 里的一个字符串字段。结果内容长度和包含信息量几乎一致。我才明白,约束的是结构,不是语义。模型的发挥空间依然在字段内部完整保留。
真正会“变蠢”的是另一种情况:你在 JSON Schema 里塞过强的语义约束,比如 maxLength: 10,然后抱怨模型表达不了复杂意思。这是约束放错了位置——格式约束解决的是“长得像不像 JSON”,不是“内容够不够好”。
那用 prompt 限制和用 Schema 限制到底差在哪?
Prompt 约束是软约束,等于在游戏开始前告诉玩家规则,然后完全信任玩家。JSON Schema 约束是硬约束,等于在游戏进行中每一步都检查玩家的选项,不允许选规则外的选项。两者最大的差别是失败概率:软约束下模型会“偶尔犯规”;硬约束下模型“想犯规都难”。
为什么我用 OpenAI 的 structured outputs 仍然见过 JSON 报错?
structured outputs 解决的是格式约束问题,但官方文档也明确:它保证输出的 JSON 语法合法,但不保证内容语义正确。同时,如果你在应用层没有做严格校验和重试,任何一次网络异常或采样偏差都可能让下游解析失败。要 100% 的语法合法,最可靠的做法是在本地用开源解码器做硬约束,或者至少加入一层 json.loads 失败重试。
是不是所有任务都应该让 LLM 输出 JSON?
不是。JSON 适合机器解析的接口;但如果你是给人看的内容,JSON 会摧毁可读性。约束解码也有额外计算成本,每次采样前要跑状态机;对于超长输出,这个开销会更明显。
JSON 不是万能药:它保证格式,不保证事实
最后说限制。约束解码保证的是语法正确,不是内容正确。模型在硬约束下可以生成完全合法、但字段值胡编的 JSON。这是另一个问题——幻觉,跟格式约束无关。
另一个实际代价是性能。每次采样前都要运行状态机来计算合法 token 集合,这比自由文本生成多了额外开销;对于流式输出和超长 JSON,延迟会更明显。所以 JSON 约束适合机器解析的场景,不适合给人读的内容。
我的判断是:JSON 比自由文本容易生成,是工程上的结构约束带来的红利。它把一个无边无际的开放生成,变成了连续的单选填空。这个红利值得开发者充分用足——但你得清楚地知道,它降低的是语法错误率,不是幻觉率。如果你想让它不撒谎,那是另一个维度的难题,任何格式约束都替代不了。
原创文章,作者:guanweilu,如若转载,请注明出处:https://guanweilu.cn/article/2042.html