大模型输出格式约束原理

0 阅读7分钟

引言

使用大模型时,我们常常希望它返回结构化的数据——JSON、表格、枚举值等,而不是一段自由文本。但在实际使用中,模型输出格式混乱、JSON 解析失败、字段遗漏等问题层出不穷。

为什么?因为大模型的本质是下一个 Token 预测器,它天生不会遵循你定义的格式。那业界是怎么解决这个问题的?这个思路的演进过程,就是输出格式约束的完整技术地图。

一、为什么需要输出格式约束

使用场景

  • 数据提取:从非结构化文本中提取实体、关系、结构化信息
  • API 调用:模型输出作为下游系统的输入,必须符合约定 Schema
  • Agent 工具调用:Function Calling 需要严格的参数格式
  • 多步工作流:链式调用需要中间结果结构一致

核心挑战

大模型本质是概率语言模型

P(w_1,w_2,...,w_n)=prod_i=1nP(w_iw_1,w_2,...,w_i1)P(w\_1, w\_2, ..., w\_n) = \\prod\_{i=1}^{n} P(w\_i | w\_1, w\_2, ..., w\_{i-1})

它不知道什么是 JSON,什么是 Schema。它的"学习"来自于训练语料中见过的格式模式——这意味着格式保证是概率性的,不是确定性的

二、输出格式约束的技术演进

第一阶段:Prompt 引导

最原始的方法:在 System Prompt 里描述格式要求。

请以 JSON 格式输出,格式如下:
{
  "name": "...",
  "age": 25
}

优点:零额外成本 缺点:靠模型"自觉",中小模型经常不遵守;JSON 中嵌入 Markdown(json);字段名不一致

第二阶段:后处理验证 + 重试

模型生成自由文本后,用正则/JSON Schema 验证,失败则重试。

def safe_json_parse(text, schema, max_retries=3):
    for i in range(max_retries):
        try:
            data = json.loads(extract_json_from_text(text))
            jsonschema.validate(data, schema)
            return data
        except (json.JSONDecodeError, ValidationError):
            text = llm.generate(prompt + "\n前次格式错误,请修正。")
    raise ValueError("格式始终不符合要求")

优点:简单可靠,任何模型都可用 缺点:重试消耗 Token,延迟不定;改模型也改不好格式时就死循环了

第三阶段:约束解码(Constrained Decoding)—— 核心突破

这是真正的硬约束方案。不是在生成后检查,而是在生成过程中就确保每个 Token 都合法。

核心原理:Token Masking

大模型的生成流程:

输入 → 模型前向推理 → Logits(词汇表概率分布)→ 采样 → 下一个 Token

约束解码在这个流程中插入一个 Mask 步骤:

输入 → 模型推理 → Logits → [Token Mask 过滤非法 Token] → 采样 → 合法 Token

具体来说,在每个 generation step:

  1. 根据当前已生成的 Token 序列和约束规则,计算当前状态下合法的 Token 集合
  2. 将非法 Token 对应的 Logits 设为 infty-\\infty(使其概率为 0)
  3. 在合法 Token 中采样

实现机制:有限状态机(FSM)

约束规则通过**有限状态机(Finite State Machine)**来实现。以 JSON Schema 约束为例:

JSON Schema → 正则表达式 → 确定性有限状态机(DFA)

DFA 的每个状态代表已生成内容的语法位置。在每个 step:

  • 当前状态 S_tS\_t 已知
  • 对于词汇表中的每个 Token,检查从 S_tS\_t 出发是否可以通过该 Token 到达下一状态
  • 能到达的 Token 标记为合法,不能的标记为非法

例子:约束 role 字段只能是 "admin""user""guest"

当模型输出到 role: " 时,FSM 状态表明接下来必须枚举 role 字段。非法 Token 如 xyz 被 Mask 掉,只有 adminuserguest 对应的 Token 允许通过。

支持多种约束类型

约束类型描述示例
JSON Schema结构+类型约束{"type": "object", "properties": {...}}
正则表达式字符串模式匹配r"\w+@\w+\.com"
枚举选择可选列表["positive", "negative"]
EBNF 文法上下文无关文法SQL 查询、自定义 DSL

第四阶段:API 原生结构化输出

OpenAI 在 2024 年正式推出 Structured Outputs,将约束解码内置到推理引擎中。

response = client.beta.chat.completions.parse(
    model="gpt-4o",
    messages=[...],
    response_format=UserSchema,  # Pydantic model
)

Gemini API 也支持 JSON Schema 约束,并且保持了 Schema 中字段的书写顺序。

Anthropic 没有提供原生的 Constrained Decoding,而是利用 Tool Calling(Function Calling)的参数约束来"曲线救国"——将输出 Schema 包装成 Tool 定义,强制模型以 Tool Call Arguments 的形式输出。

关键区分

  • OpenAI/Gemini:推理引擎层做约束(硬保证)
  • Anthropic:Tool Schema 只是提示,不是强制(软保证)

三、主流方案对比

框架对比

维度OutlinesGuidanceXGrammarvLLM
原理JSON→Regex→FSMToken 修复+CFG编译 JSON Schema→确定性自动机集成 Outlines/XGrammar 作为后端
支持格式JSON, Regex, Choice, CFGJSON, Regex, Select, CFGJSON Schema, CFGJSON, Regex, Choice, CFG
性能中等(Python 层 FSM)中等高性能(C++ 层 FSM)高性能(集成 XGrammar)
集成Transformers, vLLM, llama.cppTransformers, llama.cppTransformers, vLLMvLLM 内置
使用门槛第三方库第三方库需编译开箱即用

推理性能对比

引入约束解码必然带来额外开销。根据业界测试数据:

  • Outlines:Python 层实现 FSM,每个 Token 需要约 0.1-0.5ms 的 Mask 计算
  • XGrammar:C++ 层实现,Mask 计算时间可忽略不计,相比无约束场景 TPOT(Time Per Output Token)增加约 5-10%
  • vLLM + XGrammar:官方宣称可实现 5x 的 TPOT 提升(相比 Outlines)

各平台 API 支持对比

平台Structured Output约束类型保证程度
OpenAI✅ 原生JSON Schema硬约束
Gemini✅ 原生JSON Schema硬约束
Mistral✅ 原生JSON Schema硬约束
Anthropic❌ 无原生(Tool Calling Workaround)Tool Schema软约束
Ollama✅ 原生JSON Schema硬约束
vLLM 服务✅ 原生JSON/Regex/Choice/Grammar硬约束

四、实践示例

示例:用 Python 实现一个简易的约束解码

以下示例展示约束解码的核心思想——通过 Logits Mask 实现 Token 级别的约束:

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-7B-Instruct")
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-7B-Instruct")

def apply_logits_mask(logits, allowed_token_ids):
    """将非法 Token 的 Logits 设为 -inf"""
    mask = torch.full_like(logits, float("-inf"))
    mask[:, allowed_token_ids] = logits[:, allowed_token_ids]
    return mask

# 约束:必须输出 "positive" 或 "negative"
allowed_tokens = tokenizer.encode(["positive", "negative"], add_special_tokens=False)
inputs = tokenizer("Classify: This movie is great!", return_tensors="pt")

with torch.no_grad():
    outputs = model(**inputs)
    next_token_logits = outputs.logits[0, -1, :]
    # Mask 掉非法 Token
    masked_logits = apply_logits_mask(
        next_token_logits.unsqueeze(0),
        [token_id for ids in allowed_tokens for token_id in ids]
    )
    predicted_id = torch.argmax(masked_logits, dim=-1).item()

print(tokenizer.decode(predicted_id))  # 输出 "positive"

示例:使用 vLLM 的结构化输出

from openai import OpenAI

client = OpenAI(base_url="http://localhost:8000/v1", api_key="dummy")

# JSON Schema 约束
response = client.chat.completions.create(
    model="Qwen/Qwen2.5-7B-Instruct",
    messages=[
        {"role": "user", "content": "生成一个用户信息"}
    ],
    extra_body={
        "structured_outputs": {
            "json": {
                "type": "object",
                "properties": {
                    "name": {"type": "string"},
                    "age": {"type": "integer"},
                    "role": {"type": "string", "enum": ["admin", "user", "guest"]}
                },
                "required": ["name", "age", "role"]
            }
        }
    }
)
print(response.choices[0].message.content)

示例:使用 Ollama 的结构化输出

from ollama import chat
from pydantic import BaseModel

class User(BaseModel):
    name: str
    age: int
    role: str

response = chat(
    model='llama3.1',
    messages=[{'role': 'user', 'content': '生成一个用户信息'}],
    format=User.model_json_schema(),
)
user = User.model_validate_json(response.message.content)
print(f"name={user.name}, age={user.age}, role={user.role}")

五、约束解码的影响

结构化输出对模型的影响因任务类型而异,这一点需要特别注意。

推理任务(GSM8K、Last Letter 等):

  • JSON 模式可能导致模型放弃 CoT,直接输出答案而非逐步推理
  • 原因是 JSON 的 Key 顺序改变了模型的输出顺序,破坏了思维链
  • 性能下降可达 38%

分类任务

  • JSON 模式通常表现更好,因为答案空间被严格限制
  • 解析失败率接近零(主流模型 < 1%)

实践建议

  • 推理密集型任务:使用宽松的格式约束,或采用两阶段法(先生成自然语言推理过程,再转为结构化格式)
  • 分类/提取任务:使用严格的 JSON Schema 约束

六、总结

大模型输出格式约束的技术演进可以概括为:

Prompt 引导 → 后处理验证 → 约束解码 → API 原生结构化输出

这个过程中,核心思想从生成后检查转变为生成中约束——从概率保证走向了确定保证。

对于开发者,选型建议如下:

场景推荐方案
简单格式要求Prompt 引导 + 后处理验证
严格 SchemaOpenAI/Gemini/Mistral API + JSON Schema
自部署模型vLLM + XGrammar(推荐)
复杂 DSL/文法Guidance / Outlines
多平台兼容AI SDK + 适配层

约束解码代表了 LLM 应用开发的一个重要方向:不再把模型当作文本生成器,而是把它当作受控的数据生成器。当模型的输出格式可以预期,构建可靠的应用系统就不再是运气活。

参考文献