AI 博客

Outlines、XGrammar、llguidance 与 Instructor:合法 JSON 从来不是难的那一部分

这四者中有三个约束采样器,让非法输出根本产生不出来,而它们之间的选择坍缩成一个问题:你的 schema 会重复吗?第四个做的是另一类事,也是唯一能强制那些真正搞垮智能体的规则的——因为语法只保证枚举值是五个当中的一个,却对"是哪一个"只字不提。

作者 智能体 AI 维基 25 分钟读完

一个语法引擎能保证你的模型吐出 {"action": "refund", "amount": 4200},一个多余的反引号都不会有——却没法告诉你这笔退款究竟该不该退。结构化输出项目走偏,正是偏在这道缝上:团队去挑最快的受约束解码器、上线,然后发现故障率几乎没动,因为把他们搞垮的从来就不是格式不合法的 JSON。这里的四个工具,有三个解决的是"格式良构",彼此的区别只在于何时为语法买单。第四个解决的是另一个问题,而多数对比把它归错了栏。

要点速览

四个都挂在"结构化输出"名下、却并不都做同一件事的项目。

项目它是什么跑在哪里保证什么
Outlines语法引擎;从 schema 预计算令牌索引你自托管的服务引擎内部输出不可能格式不合法
XGrammar语法引擎;即时编译,带持久缓存你自托管的服务引擎内部输出不可能格式不合法
llguidance语法引擎;惰性构建自动机,掩码即时计算你自托管的服务引擎内部输出不可能格式不合法
Instructor客户端封装;按 Pydantic 校验并重试你的应用里,对任何 API 都行输出满足你的校验器——否则抛异常
Where each approach intervenes in generation Two rows. The upper row shows constrained decoding: the schema is compiled to a grammar, and at every decoding step a token mask zeroes out any token that would break the grammar before sampling, so invalid output is never produced. The lower row shows validate-and-retry: the model generates freely, the result is parsed and validated against a schema, and on failure the error is sent back as a new request. Constrained decoding — Outlines, XGrammar, llguidance JSON Schema or CFG Grammar compile once, or per request Token mask applied every step Sample Output always valid next step Validate and retry — Instructor Pydantic model to a prompt or tool Free generation any provider Parse + validate after the fact Retry error fed back Output or exception another full generation Semantic rules live here — and only here cross-field invariants a grammar cannot express
这条分类的裂缝。上面一行防止错误发生;下面一行检测错误——而只有下面一行看得见语义。
Capability matrix across the four structured-output tools A four-by-five grid scoring Outlines, XGrammar, llguidance and Instructor on well-formedness guarantee, recursive schema support, per-request unique schemas, semantic and cross-field validation, and whether the tool works against a hosted API you do not run. Each is strong where its design premise points and weak against the grain. Where each tool leans hardest Well-formed by construction Recursive schemas Unique schema per request Semantic / cross-field rules Works on a hosted API Outlines Guaranteed Bounded depth or rejected Precompute cost on every new one Out of scope Self-hosted only XGrammar Guaranteed Full CFG JIT + cache; repeats are free Out of scope Self-hosted only llguidance Guaranteed Full CFG Lazy build; no startup cost Out of scope Self-hosted only Instructor Only via the provider, if offered Whatever Pydantic expresses No compile step Arbitrary Python validators Any provider Strong Conditional Weak or not applicable
每一个都恰好强在它的设计前提所指的方向,逆着纹路就帮不上忙。

受约束解码到底怎么工作

机制比项目数量所暗示的要简单。每一个解码步骤,模型都会产出一个覆盖整个词表的分布。语法引擎针对当前的解析器状态计算出哪些令牌可以合法地接下去;其余每个令牌的 logit 在采样之前都被置为负无穷。非法输出不是被纠正的,而是根本够不着——而且因为掩码作用在采样之前,这个保证在任何温度下都成立。

于是只剩下一个真正的工程难题:把掩码算得足够快,快到不至于主导整个步骤。词表有十万多个令牌,而一步只有个位数毫秒,所以掩码必须在微秒级产出。这三个引擎之间的每一处差别,都是对这个难题的一个不同答案,而每一个答案都是"前期做多少功"与"每令牌做多少功"之间的一次取舍。

反直觉的那个结果

人们常假定受约束生成要付出延迟代价。跨越大量真实世界 JSON schema 的基准测试反复发现,总体上恰恰相反:一个实现良好的引擎,每令牌延迟可以低于无约束生成。原因并不神奇——受约束的模型更早收尾。它没法东拉西扯、没法先来一段开场白、没法把已经闭合的对象再打开,而且经常只有一个合法令牌可选——有些引擎会为此走快速通路,压根不去问模型。如果你反对结构化解码的理由是"它会拖慢我",请先测量再相信。

三个语法引擎,以及区分它们的那一个问题

What happens when every request carries a different schema Four columns describing behaviour under unique per-request schemas. Outlines pays a precompute cost for each new schema before the first token. XGrammar compiles just in time with a persistent cache, so repeated schemas are free and novel ones cost a compile. llguidance builds its automata lazily and pays essentially no startup cost. Instructor has no compile step at all because it never constrains sampling. Cost before the first token, on a novel schema Outlines Precompute the index over the vocabulary Highest startup cost; cheapest per token after XGrammar JIT compile with a persistent cache Repeat schemas free; novel ones pay once llguidance Lazy automata, masks computed on the fly Essentially no startup cost Instructor No compile step; no mask at all Cost arrives later, as a retry One question decides the first three: do your schemas repeat?
启动成本对每令牌成本。你的 schema 更替率,决定你想站在这笔取舍的哪一侧。

Outlines——编译一次,之后近乎免费

Outlines 让这条路线流行起来:把 schema 变成一个有限自动机,并为每一个状态预计算出允许的令牌集合。结果是采样时只做一次查表,便宜到差不多是这件事的下限。代价则是镜像对称的——对一个没见过的 schema,构建那个索引要花实打实的时间和内存;而且有限自动机表达不了无界递归,所以深度递归或自引用的 schema 要么被拒绝,要么被压平到一个固定深度。如果你服务的是一小组固定的 schema,这两笔成本都在启动时付一次,之后再不发生。

XGrammar——即时编译,靠缓存干活

XGrammar 把问题重构到上下文无关文法之上,并配一个快到可以按需运行的编译步骤,背后是持久缓存。重复的 schema 命中缓存、成本为零;真正新出现的则付一次相对于一次生成而言很小的编译。它还能处理那些让基于自动机的路子栽跟头的递归 schema,而它公布的每令牌开销低到可以消融进正常的步骤时间里。这个组合正是它成为 vLLM 与 SGLang 默认结构化输出后端、而不是一个需要你主动选上的选项的原因;而 2026 年的后续工作,瞄的正是智能体所在的动态 schema 场景。

llguidance——前期什么都不付

微软 Guidance 底下的引擎 llguidance 走的是相反的立场:惰性构建自动机、掩码即时计算,于是几乎完全没有启动成本。在固定 schema 集下,论纯每令牌吞吐它输给预计算索引。在真正每请求都不同的 schema 下它赢,因为对手在付一笔 llguidance 根本不产生的编译成本。在服务引擎上做的独立对比恰恰显示了这个交叉点——简单且重复的 schema 上 XGrammar 领先,每个请求都带来新东西时 llguidance 领先。

所以:你的 schema 会重复吗?

这就是那个决策。一个有二十个固定抽取 schema 的产品,缓存几乎全命中,应该直接用服务引擎的默认值——今天这意味着 XGrammar,也意味着不必为此费心。一个由租户自带 schema 的平台,或者一个工具集按会话拼装的智能体,属于缓存高失效的负载,应该拿 llguidance 对着自己的流量测一测。Outlines 仍然是预计算这一思路的参考实现,也是"你完全掌控 schema 集合、且想要尽可能低的每令牌成本"时的正确选择。

Instructor 不在同一个类别里

Instructor 根本不碰采样器。它封装提供方的客户端,把你的 Pydantic 模型作为 schema 或工具定义发出去,解析返回的内容、做校验,失败时把校验错误作为一次新请求发回给模型——默认最多三次。这是基于重试的,不是基于约束的,而这个差别不是程度问题。

它的代价一目了然:一次失败就是一整次额外生成、尾部延迟无界,而且根本没有任何保证——只有重试耗尽时的一个异常。它换来的东西才是被低估的那部分。语法能强制 discount_percent 是 0 到 100 之间的整数。它没法强制"除非 customer_tier 是 gold,否则 discount_percent 必须为零",没法强制 end_date 晚于 start_date,也没法强制响应里的每一个 ID 都出现在检索到的上下文中。这些都是对已解析对象的任意谓词,它们才是生产中真正会失败的约束,而一个 Pydantic 校验器一行就能表达全部。

它换来的第二样东西是覆盖面。语法引擎跑在服务引擎里面,这意味着你得自己在跑模型。面对一个托管 API,你只能拿到提供方给的那个结构化输出特性,别无其他。Instructor 到处都能用;而在提供方确实提供了受约束解码的地方,它会用上,并在其上保留校验层。

诚实的建议是两个都要

它们是可组合的,而组合起来才是真正管用的配置:语法引擎保证对象能解析,校验层强制语法看不见的那些语义。这样你就恰好只有一条重试路径,而且它只在那些本来就非重试不可的错误上触发。只挑一个工具的团队,通常最后都在手工重建另一半。

没人做基准的那个失败模式

schema 合规与正确是两种不同的性质,而这个领域里的每一个基准只测其中一种。一个被塞进 schema 的模型会把它填满。你要一个必填的 root_cause 字段,你就会拿到一个——不管证据支不支持存在一个根因;你要一个五值枚举,模型就会挑一个,而不会告诉你一个都不适用。这道约束把"我不知道"——模型能说出的最有用的一句话——变成了一个笃定、良构、无从证伪的取值。

  • 给"弃答"一个合法的编码方式。在答案确实可能不存在的地方把字段设为可选,并给枚举加上明确的 insufficient_evidence 变体。如果 schema 没有任何办法表达不确定性,你并没有消除不确定性;你只是把它藏了起来。
  • 不要约束推理过程。对整个响应强加结构,就是在约束模型用以思考的那些令牌。让它用自由文本推理,再把结构化对象作为一个单独的受约束片段、或一次单独的调用吐出来。
  • 字段顺序是一种提示词。生成是从左到右的,所以放在前面的字段是在更少上下文下产出的,并且会条件化它之后的一切。把结论放在证据之后,别放在之前。
  • 命名承载指令。is_fraudulentrisk_flag 是同一个布尔值,却不会产生同一个分布。schema 的键名措辞是一处提示词工程的作用面,近期已有研究把它当作一处来测量。

这些都不是对工具的批评。这是一个提醒:它们把问题的句法那一半解决得干干净净,语义那一半则完全没碰——而你的事故报告来自第二半。

什么时候选哪个

情形为什么
托管 API,自己没有服务引擎Instructor,加上提供方的结构化输出模式语法引擎对你不可得;校验加重试就是全部工具
自托管,schema 集小且固定你的引擎的默认值,也就是 XGrammar每个请求都命中缓存;这里没有可供调优的余量
自托管,每请求 schema 都不同拿 llguidance 与默认值做对比测试"没有编译成本"就是它的全部优势,而这恰好是你的负载
递归或自引用的 schemaXGrammar 或 llguidance有限自动机表达不了无界递归
跨字段或需接地于证据的规则在你所用之物之上叠一层 Instructor没有任何语法能表达对整个已解析对象的谓词
智能体的工具调用引擎默认值,并检查它在满 batch 下还撑得住厂商的工具调用本已受约束;风险在负载下的吞吐

有一条运维层面的提醒,重要性盖过整份对比:无论你用哪个引擎,都要验证它在生产批量下依然表现正常。结构化解码是按序列施加的,一个在 batch 1 下看着免费的引擎,在一个大 batch 里每个请求都带着自己的语法时,可能就现形了。这是服务引擎对比那篇当作一等区分项来处理的一个维度,而且值得你拿自己的流量去测,而不是从一篇博客里继承结论。

常见问题

受约束解码会让模型变笨吗?

如果你约束错了令牌,会。对整个响应强加结构,会限制模型用来推理的那些令牌,在需要一步步想的任务上会有可测量的损害。只约束自由文本推理之后的最终输出片段,就能规避掉其中绝大部分。

如果我的提供方已经提供结构化输出,我还需要这些吗?

就格式良构而言,不需要——提供方在服务端做的是同一件事。就语义与跨字段规则而言,需要,因为没有哪家提供方会校验它看不见的谓词。那正是 Instructor 所占的那一层,而它不会消失。

XGrammar 总是正确的默认值吗?

在字面意义上它是正确的默认值——vLLM 与 SGLang 已经把它设成默认了,而对多数负载没有理由去覆盖它。例外是真正每请求都不同的 schema:那里缓存不再帮忙,而 llguidance 的惰性构建是一项实打实的优势。

语法能保证取值是对的吗?

不能。语法约束的是输出的形状——类型、结构、枚举里哪些字符串合法。至于合法取值中被选中的是哪一个,那是个建模问题;而一个没有任何办法说"我不知道"的 schema,得到的将是一个笃定的答案,而不是一个诚实的答案。

那工具调用呢——是同一套机制吗?

底层通常是的:一个工具 schema 就是一份 JSON Schema,而受约束解码正是提供方让工具调用可靠解析的手段。区别在于,工具调用时你很少能选引擎,而你真正会见到的失败是——模型带着完全合法的参数挑错了工具。

延伸阅读

本站相关:

项目来源: