跳到主要内容

语法约束生成:大多数团队忽视的输出可靠性技术

· 阅读需 11 分钟
Tian Pan
Software Engineer

大多数需要结构化LLM输出的团队都遵循同一套方案:写一个提示词说"只返回有效的JSON",解析响应,运行Pydantic验证,失败则附上错误信息重试。这种方式在大多数情况下能用,但在生产环境中恰恰会在最糟糕的时刻失效——高负载、边缘用例输入,以及指令遵循不如GPT-4可靠的低成本模型。

语法约束生成是一种根本不同的方法。它不是礼貌地请求模型然后事后检查,而是从数学上让结构无效的输出成为不可能。模型无法输出缺失的括号、不存在的枚举值或遗漏的必填字段——因为这些token在采样前就被过滤掉了。不是不太可能,而是不可能。

大多数团队跳过了这个方法,但不应该。

约束解码的实际工作原理

语言模型逐token生成文本。在每一步,模型在整个词汇表——数万个token——上产生概率分布。通常情况下,从该分布采样后继续。

约束解码在模型的logit计算和采样步骤之间插入了一个环节。在选择任何token之前,约束引擎检查:给定目前已生成的所有内容,词汇表中哪些token能保持输出处于结构有效的路径上?所有会违反约束的token其logit会被设置为负无穷——实际上被清零。剩余token重新归一化,采样正常进行。

约束以形式语法表达,提前编译成状态机。对于正则约束(枚举、日期格式、固定模式),是有限状态机(FSM)。对于像JSON这样具有嵌套结构的上下文无关语法,是下推自动机(PDA)——通过栈追踪开闭括号和嵌套对象的状态机。

JSON的实际流水线如下:将schema表示为Pydantic模型,调用.model_json_schema()获取JSON Schema,将其输入约束生成引擎,引擎将其编译为状态机。推理过程中每个token步骤,引擎查询状态机——"给定当前解析状态,下一个有效token是什么?"——并屏蔽其他所有token。

实际效果:结束括号总是平衡的,必填字段总是出现,枚举值总是在允许集合内。模型无法在JSON外面加Markdown代码围栏、添加说明文字或中途截断对象。这些结果从结构上变得不可能,而不仅仅是不太可能。

你实际上在消除什么

大多数生产代码实现的验证重试循环不仅仅是工程上的臭味——它是一种失败税。每次重试都是以完整成本和延迟进行的完整推理调用。对于具有四个必需嵌套字段和枚举约束的schema,较小的模型在首次尝试时可能失败30%,平均每个输出需要1.5次推理调用。在高吞吐量流水线上,这种影响会快速累积。

一个团队记录了从基于提示词的JSON请求切换到约束解码后,后处理错误从32%降至0.4%。OpenAI自己的基准测试显示,带结构化输出的gpt-4o在复杂JSON schema遵循上得分100%,而同一模型仅用提示词指令时不到40%。

约束解码消除的错误专门属于结构类别:

  • 缺失结束括号和括号不匹配
  • 缺失必填字段(模型"忘记"了)
  • 幻觉的schema中没有的额外字段
  • 错误数据类型(需要整数的地方给了字符串)
  • 无效枚举值(模型发明了新选项)
  • JSON被包裹在Markdown代码围栏中
  • 评论文字混入输出

对结构化输出失败的研究发现,微调模型中65%的schema错误属于两类:幻觉键(发明字段名)和未关闭括号。约束解码从构造上完全消除了这两类问题。

工具及其差异

多个成熟的库实现了约束解码,它们在性能特性上差异显著。

Outlines(dottxt-ai)是最广为人知的开源选项。它支持JSON schema、Pydantic模型、正则表达式和EBNF语法。主要弱点是语法编译时间:复杂schema首次使用时可能需要3–12秒编译。在schema频繁变化的多租户或智能体场景中,这种冷启动成本令人望而却步。

XGrammar(来自卡内基梅隆大学/MLC-AI,2024年11月发布)是当前生产吞吐量的最先进方案。其核心洞察是:词汇表中绝大多数token——超过99%——相对于任何给定语法具有上下文无关的有效性。它们的有效性可以在语法编译期间离线预计算。只有极少数需要运行时评估。这将JSON schema的掩码计算时间降至40微秒以下,比早期库快约100倍。XGrammar现在是vLLM和SGLang的默认后端。

Guidance(微软,由llguidance Rust后端驱动)采用不同的架构方法。它遍历词汇表前缀字典树,使用正则表达式的导数计算掩码。其基准数字令人印象深刻:在约束任务上通常比无约束生成更快,因为当语法唯一确定下一个token时,Guidance可以完全跳过采样步骤。有缓存的平均掩码计算降至50微秒以下。

llama.cpp语法约束使用GBNF(GGML BNF格式)——支持字符类和重复运算符的扩展BNF。它是本地推理和嵌入式部署的正确选择,内置了从JSON Schema到GBNF的自动转换。

对于vLLM用户,可通过请求中的guided_jsonguided_grammarguided_regex参数访问结构化输出功能。引擎根据约束类型自动在XGrammar和Outlines后端之间选择。对于本地模型,Outlines和Guidance都直接与Hugging Face Transformers集成,可用作logit处理器。

性能实际情况

约束解码的"5–15%开销"数字已经过时。实际情况完全取决于你使用哪个引擎。

早期的Outlines实现增加了50–200%的延迟开销,仅编译成本就足以让每个请求schema变化的场景望而却步。这段历史是许多团队放弃这种方法的原因。

现代引擎讲述了不同的故事。JSONSchemaBench论文(2025年1月)测试了近10,000个真实世界JSON schema上的六个主要框架,发现Guidance/llguidance实现了比无约束生成更低的每token延迟——大约6–9ms对比基准的15–16ms。推测执行收益超过了掩码开销。XGrammar在JSON任务上比早期库实现了高达14倍的端到端加速,在复杂上下文无关语法上高达80倍。

剩余的开销问题是语法编译时间,而非推理时间。Outlines每个唯一schema需要几秒钟。XGrammar需要0.12–0.30秒。Guidance在60毫秒内完成编译。对于schema集合小而固定的应用,编译是启动时的一次性成本。对于tool定义动态变化的智能体流水线,XGrammar-2(2025年1月)引入了跨语法缓存,在相关schema间复用已编译的子结构。

实际指导:如果你服务于有限的输出schema集合,语法编译开销不是问题。如果你按请求动态生成schema,使用XGrammar或Guidance,而不是Outlines。

约束解码无法修复的问题

这是许多团队产生虚假自信的地方。语法约束生成提供的是格式保证,而非语义保证。输出将始终在结构上有效,但不会始终正确。

约束解码使必填字段成为强制要求——这意味着当模型实际上不知道该值时,它会产生一个听起来合理的捏造,而不是表达不确定性。需要risk_score字段的schema总会得到一个值,即使正确答案是"我没有足够的信息"。

BAML记录了一个具体的准确性回归:对于收据解析任务,OpenAI结构化输出对香蕉数量返回了1而不是0.46。自由格式输出解析正确提取了0.46。结构化输出约束强制产生了一个schema有效但语义错误的结果。他们的基准测试显示,约束解码的准确率为91.37%,而自由格式解析为93.63%。

EMNLP 2024发表的研究发现,严格的格式约束在数学基准测试上使推理准确率下降了多达27个百分点。机制是:JSON输出强制模型在完成思维链推理之前就输出答案字段,打断了产生正确结果的思考过程。对于模型只需从一组选项中选择的分类任务,约束有帮助。对于需要多步推理的任务,强制在输出上应用schema可能会显著损害准确性。

推荐的混合模式是先给模型一个自由格式的推理草稿,然后只对最终结构化输出步骤应用约束解码。这在保证输出格式的同时保留了推理质量。

还有一个更微妙的失败模式:当约束解码迫使模型走上不寻常的token序列时(因为高概率token违反了语法),产生的token可能是模型在训练中在该上下文中很少生成的token。BPE分词是上下文相关的,不寻常的token路径可能引入难以诊断的质量退化。微软的Guidance库通过token修复解决了这个问题——当提示词在token中间结束时,它会回退一个token并从干净的边界重新约束生成。

何时使用它

语法约束生成在以下情况是正确的默认选择:

  • 你需要从较小或指令遵循能力较弱的模型(7B–13B参数范围)获取结构化输出
  • 你在运行高吞吐量流水线,重试成本是真实存在的
  • 你有带嵌套对象、枚举和必填字段的复杂schema
  • 你在控制推理栈的本地基础设施上部署

在以下情况它的价值较低:

  • 你为语义准确性比格式保证更重要的任务使用前沿API模型(GPT-4o、Claude 3.5 Sonnet),且基于提示词的JSON已经足够可靠
  • 你的输出任务涉及复杂推理,格式约束可能干扰思维链
  • 你使用无法访问logit分布的黑盒API(尽管OpenAI和Gemini都通过其结构化输出功能提供服务端约束生成)

对大多数团队来说工具选择很直接。在vLLM或SGLang上,使用内置的结构化输出功能——XGrammar是默认后端,开销问题实际上已消失。对于使用llama.cpp的本地模型,使用GBNF语法约束并内置JSON Schema自动转换。对于基于Transformers的流水线,Guidance或Outlines都可以作为logit处理器集成。

更深层的启示

大多数团队实现的验证重试循环不是稳健的工程模式——它是一种将失败概率泄漏到尾部延迟和成本中的变通方案。语法约束生成用形式保证替代了概率性格式合规,代价是现代引擎已将其降至接近零。

来自近期研究的更微妙洞察:格式正确性和语义正确性是需要独立解决的独立问题。约束解码解决一个;精心的提示词设计、思维链和输出级验证解决另一个。将它们混为一谈的团队——期望约束解码不仅让输出结构有效,还能使其语义准确——会感到失望。精确地将其用于所保证的范围,并单独构建语义验证的团队,最终会拥有更可靠的系统和更简洁的代码。

验证重试循环不仅仅是慢,它是将格式合规视为概率问题而非工程问题的症状。约束解码将其视为工程问题,这才是正确的框架。

References:Let's stay in touch and Follow me for more thoughts and updates