AI 技术
#约束解码#CFG#正则约束#JSON Schema#vLLM

约束解码如何保证大模型输出格式:从正则到文法约束的机制与工程权衡

本文以自动生成可解析的 JSON 和 SQL 为场景,解释约束解码如何在前向传播时屏蔽非法 Token,对比后处理修复与重试的失败模式,分析对生成质量、延迟和实现复杂度的影响,并给出适用边界。

一个可感知的工程问题

假设你正在构建一个企业知识库问答系统,模型需要返回结构化的 JSON 供前端渲染。你已经在提示词里写明了输出格式,甚至给出了示例。但实际运行时,模型偶尔会多输出一个逗号、漏掉一个引号,或者把布尔值写成字符串。下游的 json.loads() 直接抛异常,整个请求失败。

最常见的应对是后处理修复:把模型输出交给一个正则表达式或一个小模型去修正。但后处理只能等模型生成完毕后才介入,此时 Token 已经消耗,而且修复本身也可能失败。另一种做法是重试:让模型重新生成一次,期望这次运气好。但重试会成倍增加延迟和成本,且无法根治问题。

约束解码(Constrained Decoding)提供了第三条路:在模型逐 Token 生成时,根据预定义的格式(如正则、JSON Schema 或上下文无关文法)动态屏蔽非法 Token,从机制上保证输出结构合法。本文将围绕自动生成可解析的 JSON 和 SQL 这一场景,解释约束解码的工作原理、工程实现、权衡与适用边界。

基线方案为何失效:后处理与重试的失败模式

先看后处理修复。假设模型输出了 {"name": "Alice", "age": 30,},末尾多了一个逗号。一个简单的正则替换可以去掉尾逗号,但如果是嵌套结构呢?比如 {"items": [{"id": 1, "name": "a"}, {"id": 2}]},模型漏掉了内层对象的 name 字段,后处理无从得知该补什么值。更复杂的情况是模型把 null 写成了 None,或者把数字写成了 "30",后处理要么猜,要么直接失败。

后处理修复的另一个问题是它不改变生成过程。模型已经消耗了生成这些非法 Token 的算力,修复只是亡羊补牢。在批量推理中,如果 20% 的请求需要修复,吞吐量会明显下降。

重试策略则更直接:检测到解析失败就重新生成。但重试的代价是成倍的延迟和 Token 消耗。假设一次生成平均需要 500 个 Token,失败率 10%,那么平均每个成功请求要多消耗 50 个 Token。更糟糕的是,重试并不保证成功——模型可能再次犯同样的错误,尤其是在格式复杂或上下文较长时。

后处理和重试的共性问题是:它们把格式保证放在了生成之后,而不是生成之中。约束解码则把格式约束嵌入到解码过程,从源头避免非法 Token 的产生。

约束解码的核心机制:在每一步屏蔽非法 Token

约束解码的基本思路是:在每一步生成时,根据当前已生成的 Token 序列和预定义的约束(正则、JSON Schema 或 CFG),计算一个合法 Token 集合,然后将集合外 Token 的 logits 设为负无穷,再经过 softmax 得到概率分布。这样模型只能从合法 Token 中采样,从而保证输出结构。

以生成 JSON 为例,假设模型已经生成了 {"name": ",此时合法 Token 应该是任何字符串字符,直到遇到转义引号或结束引号。如果约束是 JSON Schema,那么下一个 Token 必须符合 Schema 中 name 字段的类型(字符串)。

约束解码的关键在于如何高效地计算合法 Token 集合。最简单的是正则约束:将正则表达式编译成有限自动机(DFA),在每一步根据当前状态和词汇表,找出能转移到合法状态的 Token。但正则表达能力有限,无法处理嵌套结构,比如 JSON 的递归。

从正则到上下文无关文法:表达能力的跃升

正则表达式可以描述简单的格式,比如 \d{4}-\d{2}-\d{2} 表示日期。但 JSON 和 SQL 是递归结构,正则无法表达。例如 JSON 的数组可以嵌套数组,对象可以嵌套对象,这需要上下文无关文法(CFG)来描述。

CFG 由一组产生式规则组成,例如:

object -> '{' pair (',' pair)* '}'
pair -> STRING ':' value
value -> STRING | NUMBER | object | array
array -> '[' value (',' value)* ']'

约束解码时,需要根据当前已生成的 Token 和文法规则,预测下一步可能出现的 Token。这相当于在运行一个下推自动机(PDA),因为 CFG 需要栈来记录嵌套深度。

资料 2 中的工作(Geng 等人,EMNLP 2023)展示了文法约束解码在信息抽取、实体消歧和成分句法分析等结构化 NLP 任务上的有效性。他们引入了输入相关的文法(input-dependent grammars),允许文法根据输入变化,从而为不同输入生成不同结构。实验表明,文法约束的模型显著优于无约束模型,甚至能超越任务特化的微调模型。

工程实现:XGrammar 与 vLLM 的实践

在实际推理框架中,约束解码需要与高性能推理引擎集成。vLLM 2.1 原生支持了多种约束解码后端,包括 guided_regexguided_jsonguided_grammar 等。默认后端是 XGrammar,由陈天奇团队开发。

XGrammar 的核心优化是:将词汇表中的 Token 分为“上下文无关”和“上下文相关”两类。上下文无关的 Token 可以预先检查,缓存结果;上下文相关的 Token 需要在运行时解释。通过构建持久栈来加速上下文相关 Token 的检查,并与 GPU 执行重叠,XGrammar 实现了近零开销的结构化生成。

资料 3 提到,XGrammar 在 JSON 语法和 JSON Schema 处理上实现了高达 3 倍和 100 倍的速度提升,在 H100 GPU 上端到端服务加速可达 80 倍。这些数字来自资料,具体环境可能不同,但说明优化空间很大。

贯穿场景:从越南企业登记证提取信息

为了具体说明,我们采用一个实际场景:从越南企业的商业登记证中提取关键信息(公司名、成立日期、地址等),并输出为 JSON。这个场景在资料 3 中有真实实验。

假设输入是一张登记证的 OCR 文本,模型需要输出:

{
  "company_name": "...",
  "founded_date": "YYYY-MM-DD",
  "address": "...",
  "tax_code": "..."
}

如果使用无约束解码,模型可能输出非 JSON 文本,或者 JSON 但字段缺失。后处理修复很难补全缺失字段。而使用约束解码,我们可以定义一个 JSON Schema,强制模型生成完整的字段结构。

资料 3 的实验显示,使用 XGrammar 约束解码的 3B 视觉语言模型,在信息提取准确率上比原生模型提升了接近 20 个百分点,接近 Qwen2.5-VL-7B 的水平,甚至超过了 GPT-4o-mini。虽然实验环境是 float32 推理,耗时较长,但准确率提升是显著的。

约束解码的完整流程

下面用 Mermaid 流程图展示约束解码在生成 JSON 时的数据流,与上述场景一致。

flowchart TD
    A[输入提示词与 JSON Schema] --> B[编译 Schema 为文法或自动机]
    B --> C[初始化解析状态]
    C --> D[模型前向传播得到 logits]
    D --> E[根据当前状态计算合法 Token 掩码]
    E --> F[将非法 Token logits 设为 -inf]
    F --> G[softmax 采样下一个 Token]
    G --> H{是否结束?}
    H -- 否 --> I[更新解析状态]
    I --> D
    H -- 是 --> J[输出完整 JSON]

流程的关键转折点在于“计算合法 Token 掩码”。这一步需要根据当前已生成的 Token 和文法状态,确定哪些 Token 是合法的。例如,在生成 {"company_name": " 之后,合法 Token 是字符串字符,直到遇到结束引号。掩码计算的质量直接影响生成速度和准确性。

约束解码与替代方案的对比

下表对比了三种保证输出格式的方案:后处理修复、重试、约束解码。

方案格式保证额外延迟实现复杂度适用场景
后处理修复不保证,可能失败低(修复本身快)低(正则或规则)格式简单、错误率低
重试不保证,可能多次失败高(成倍延迟)低(检测+重试)错误率低、对延迟不敏感
约束解码保证(如果约束正确)低(每 Token 掩码计算)高(需要集成推理框架)格式复杂、错误率高、延迟敏感

约束解码的代价是每步生成都需要计算掩码,增加了少量计算。但相比重试的成倍延迟,约束解码通常更优。实现复杂度上,需要依赖支持约束解码的推理框架(如 vLLM、SGLang),或者自行实现,门槛较高。

约束解码的失败模式与适用边界

约束解码并非万能。首先,约束本身可能过于严格,导致模型无法生成合理内容。例如,如果 JSON Schema 要求某个字段必须是枚举值,而模型想输出一个不在枚举中的值,就会被强制替换为合法值,可能损失语义。

其次,约束解码只保证结构合法,不保证内容正确。模型可能生成结构合法但语义错误的 JSON,比如日期格式正确但年份错误。

第三,约束解码对长文本和复杂嵌套结构的性能仍有挑战。资料 3 提到,vLLM 0.8.2 在长上下文且无直接含义时,即使定义了 backend 和 JSON 格式,仍有一定概率重复输出或截断。这说明约束解码在极端情况下也可能失效。

适用边界包括:

  • 当输出格式固定且复杂(如 JSON、SQL)时,约束解码是首选。
  • 当错误率较高、重试成本大时,约束解码能显著提升效率。
  • 当模型能力较弱(如小模型)时,约束解码能弥补格式遵循能力。

不适用的情况:

  • 输出格式极其简单且错误率极低时,后处理可能更简单。
  • 需要模型自由发挥、格式只是建议时,约束解码会限制创造性。
  • 推理框架不支持约束解码,且实现成本过高时。

可观测性与调试

在生产环境中,需要监控以下信号:

  • 约束解码失败率:即模型生成完成后,解析仍然失败的比率。这通常意味着约束定义有误或模型无法满足约束。
  • 掩码计算延迟:每 Token 掩码计算的时间,如果过高会拖慢生成。
  • 生成质量:约束解码可能降低生成质量(如重复、截断),需要人工评估或自动指标。

调试时,可以先从简单的正则约束开始,逐步增加复杂度。如果发现掩码计算成为瓶颈,可以尝试 XGrammar 等优化后端。

尚未解决的问题

约束解码仍有一些开放问题。例如,如何自动生成高质量的约束?对于 SQL 这类需要理解数据库 Schema 的语言,约束需要根据数据库结构动态生成,这增加了复杂度。另外,约束解码与采样策略(如温度、top-p)的交互尚未完全研究,过强的约束可能使采样退化为贪心。

总之,约束解码通过在前向传播时屏蔽非法 Token,从机制上保证了输出格式,相比后处理和重试有本质优势。但它并非银弹,需要根据场景权衡实现复杂度和性能。对于需要可靠结构化输出的生产系统,约束解码是值得投入的方向。

资料来源

  1. Guidance: A Guidance Language for Controlling Large Language Models
  2. Grammar-Constrained Decoding for Structured NLP Tasks without Finetuning
  3. 大模型如何保证输出json格式? - 53AI-AI知识库|企业AI知识库|大模型知识库|前线部署工程师|FDE|AIHub