结构化输出与受约束解码


文档摘要

结构化输出与受约束解码 本节摘要:让 LLM 输出 JSON,大多数时候能拿到 JSON——生产里「大多数」就是问题。受约束解码靠在采样前编辑 logits,把「大多数」变成「总是」。一个分类器提示 LLM「返回 {positive, negative, neutral} 之一」,模型却返回「情感是积极的——这条评论压倒性地正面,因为顾客明确说……」。你的解析器崩了,F1 是 0。自由格式生成不是契约,是建议;生产系统需要契约。

结构化输出与受约束解码

本节摘要:让 LLM 输出 JSON,大多数时候能拿到 JSON——生产里「大多数」就是问题。受约束解码靠在采样前编辑 logits,把「大多数」变成「总是」。一个分类器提示 LLM「返回 {positive, negative, neutral} 之一」,模型却返回「情感是积极的——这条评论压倒性地正面,因为顾客明确说……」。你的解析器崩了,F1 是 0。自由格式生成不是契约,是建议;生产系统需要契约。2026 有三层:提示(「只返回 JSON 对象」,前沿模型约 80% 命中)、原生结构化输出 API(OpenAI response_format、Anthropic 工具调用、Gemini JSON 模式,可靠但厂商锁定)、受约束解码(在每个生成步修改 logits,让模型不可能吐非法 token,按构造 100% 合法,适任何本地模型)。本节为三层建立直觉,点明何时取用何者,并揭示那个让你吃亏的陷阱:字段顺序是逻辑而非排版——把 answer 放在 reasoning 之前,模型就先承诺答案再思考,JSON 合法但答案错了,没有任何校验抓得到。

对应原课程:Phase 5 · Lesson 20 · structured-outputs-constrained-decoding(原英文 phases/05-nlp-foundations-to-advanced/20-structured-outputs-constrained-decoding/docs/en.md)。前置依赖:第 17 节(聊天机器人)、第 19 节(子词分词)。

学习目标

阅读完本节,你应当能够:

  1. 区分三层结构化输出(提示、原生 API、受约束解码)的可靠度与适用边界
  2. 讲清受约束解码的机理:logit 处理器在每个生成步屏蔽非法 token,使非法输出不可达。
  3. Outlines(FSM)、Instructor(Pydantic + 重试)、vLLM 引导解码三种工具落地。
  4. 识别字段顺序陷阱与递归 schema、巨型 enum、过严语法等生产坑。

一、问题与直觉

一个分类器提示 LLM:「返回 {positive, negative, neutral} 之一」。模型返回:「情感是积极的——这条评论压倒性地正面,因为顾客明确说……」。你的解析器崩了,分类器 F1 是 0。

自由格式生成不是契约,是建议。生产系统需要契约。2026 有三层:

  1. 提示:客气地请求。「只返回 JSON 对象。」前沿模型约 80% 命中,小模型更差。
  2. 原生结构化输出 API:OpenAI response_format、Anthropic 工具调用、Gemini JSON 模式。在支持的 schema 上可靠。厂商锁定。
  3. 受约束解码:在每个生成步修改 logits,让模型不可能吐非法 token。按构造 100% 合法。适任何本地模型。

受约束解码如何工作。 每个生成步,LLM 在整个词表(约 10 万 token)上产出一个 logit 向量。一个 logit 处理器夹在模型与采样器之间:它计算「在目标语法——JSON Schema、正则、上下文无关语法——的当前位置上哪些 token 合法」,把所有非法 token 的 logit 设成负无穷;对剩余 logit 做 softmax 只把概率质量放在合法延续上。

2026 的实现:

  • Outlines:把 JSON Schema 或正则编成有限状态机(FSM),每个 token 有 O(1) 的「下一合法 token」查表。FSM 式,故递归 schema 需摊平。
  • XGrammar / llguidance:上下文无关语法引擎,能处理递归 JSON Schema,解码开销近乎零。OpenAI 2025 结构化输出实现里致谢了 llguidance。
  • vLLM 引导解码:内建 guided_jsonguided_regexguided_choiceguided_grammar,后端是 Outlines、XGrammar 或 lm-format-enforcer。
  • Instructor:基于 Pydantic、跨任意 LLM 的封装。校验失败就重试。跨厂商,但不改 logits——它靠重试 + 结构化输出感知的提示。

反直觉结果

受约束解码常常自由生成更快。原因有二:一,它缩小了下一 token 的搜索空间;二,精巧实现对「被迫 token」(像 {"name": " 这种脚手架,每个字节都已确定)干脆跳过 token 生成。

让你吃亏的陷阱

字段顺序要紧。把 answer 放在 reasoning 前,模型就在思考前承诺了答案。JSON 合法,答案错了,没有任何校验抓得到。

// 坏 {"answer": "yes", "reasoning": "because ..."} // 好 {"reasoning": "... therefore ...", "answer": "yes"}

⚠️ Schema 字段顺序是逻辑,不是排版。

二、从零实现

第 1 步:从零写正则约束生成

核心想法,30 行:

def mask_logits(logits, valid_token_ids): mask = [float("-inf")] * len(logits) for tid in valid_token_ids: mask[tid] = logits[tid] return mask def generate_constrained(model, tokenizer, prompt, fsm): ids = tokenizer.encode(prompt) state = fsm.initial_state while not fsm.is_accept(state): logits = model.next_token_logits(ids) valid = fsm.valid_tokens(state, tokenizer) logits = mask_logits(logits, valid) tok = sample(logits) ids.append(tok) state = fsm.transition(state, tok) return tokenizer.decode(ids)

FSM 追踪「我们到目前为止满足了语法的哪些部分」。valid_tokens(state, tokenizer) 计算哪些词表 token 能在不离开接受路径的前提下推进 FSM。

第 2 步:用 Outlines 跑 JSON Schema

from pydantic import BaseModel from typing import Literal import outlines class Review(BaseModel): sentiment: Literal["positive", "negative", "neutral"] confidence: float evidence_span: str model = outlines.models.transformers("meta-llama/Llama-3.2-3B-Instruct") generator = outlines.generate.json(model, Review) result = generator("Classify: 'The wait staff was attentive and the food arrived hot.'") print(result) # Review(sentiment='positive', confidence=0.93, evidence_span='attentive ... hot')

永远零校验错误。FSM 让非法输出不可达。

第 3 步:用 Instructor 跨厂商跑 Pydantic

import instructor from anthropic import Anthropic from pydantic import BaseModel, Field class Invoice(BaseModel): vendor: str total_usd: float = Field(ge=0) line_items: list[str] client = instructor.from_anthropic(Anthropic()) invoice = client.messages.create( model="claude-opus-4-7", max_tokens=1024, response_model=Invoice, messages=[{"role": "user", "content": "Extract from: 'Acme Corp $420. Widget, Gizmo.'"}], )

机制不同。Instructor 不碰 logits:它把 schema 排进提示、解析输出、校验失败就重试(默认 3 次)。适任何厂商,但重试加延迟与成本。跨厂商可移植是卖点。

第 4 步:原生厂商 API

from openai import OpenAI client = OpenAI() response = client.responses.create( model="gpt-5", input=[{"role": "user", "content": "Classify: 'The food was cold.'"}], text={"format": {"type": "json_schema", "name": "sentiment", "schema": {"type": "object", "required": ["sentiment"], "properties": {"sentiment": {"type": "string", "enum": ["positive", "negative", "neutral"]}}}}}, ) print(response.output_parsed)

服务端受约束解码,在支持的 schema 上与 Outlines 可靠度持平。无需本地模型管理,但锁定厂商。

三、框架对比

2026 的栈:

情形
OpenAI/Anthropic/Google 模型,简单 schema 原生厂商结构化输出
任意厂商,Pydantic 工作流,能容忍重试 Instructor
本地模型,要 100% 合法,扁平 schema Outlines(FSM)
本地模型,递归 schema XGrammar 或 llguidance
自托管推理服务 vLLM 引导解码
批处理,可接受重试 Instructor + 最便宜模型

陷阱

  • 递归 schema:Outlines 把递归摊平到固定深度。树结构输出(嵌套评论、AST)要 XGrammar 或 llguidance(CFG 式)。
  • 巨型 enum:一万选项的 enum 编译慢或超时。改用检索器:先预测 top-k 候选,再约束到那几个。
  • 语法过严:强制 date: "YYYY-MM-DD" 正则,模型就无法为缺失日期输出「unknown」,转而编造一个日期。允许 null 或哨兵值。
  • 过早承诺:见上面的字段顺序陷阱。总把 reasoning 放第一。
  • 无 schema 的厂商 JSON 模式:纯 JSON 模式只保证合法 JSON,不保证对你的用例合法。总提供完整 schema。

四、可复用产物

保存为 outputs/skill-structured-output-picker.md:

--- name: structured-output-picker description: Choose a structured output approach, schema design, and validation plan. version: 1.0.0 phase: 5 lesson: 20 tags: [nlp, llm, structured-output] --- Given a use case (provider, latency budget, schema complexity, failure tolerance), output: 1. Mechanism. Native vendor structured output, Instructor retries, Outlines FSM, or XGrammar CFG. One-sentence reason. 2. Schema design. Field order (reasoning first, answer last), nullable fields for "unknown", enum vs regex, required fields. 3. Failure strategy. Max retries, fallback model, graceful `null` handling, out-of-distribution refusal. 4. Validation plan. Schema compliance rate (target 100%), semantic validity (LLM-judge), field-coverage rate, latency p50/p99. Refuse any design that puts `answer` or `decision` before reasoning fields. Refuse to use bare JSON mode without a schema. Flag recursive schemas behind an FSM-only library.

五、练习

  1. 基础:在没有受约束解码的情况下,提示一个小开源权重模型(如 Llama-3.2-3B)产出 Review(sentiment, confidence, evidence_span),测 100 条评论里多少能解析为合法 JSON。
  2. 进阶:同一语料用 Outlines JSON 模式,对比合规率、延迟、语义准确率。
  3. 挑战:从零实现一个电话号码(\d{3}-\d{3}-\d{4})的正则约束解码器,在 1000 个样本上验证零非法输出。

本节要点回顾

  1. 自由生成是建议不是契约:生产需要把「大多数」变成「总是」。
  2. 三层递进:提示(~80%)→ 原生 API(可靠但锁厂商)→ 受约束解码(按构造 100%,适本地模型)。
  3. logit 处理器夹在模型与采样器之间:每步把非法 token 的 logit 设负无穷,softmax 后只剩合法延续。
  4. Outlines 把 schema 编成 FSM:O(1) 下一合法 token 查表,递归需摊平。
  5. XGrammar/llguidance 是 CFG 引擎:处理递归 schema,解码开销近乎零,OpenAI 2025 实现致谢了 llguidance。
  6. Instructor 改提示不改 logits:Pydantic + 重试,跨厂商可移植是卖点,但加延迟成本。
  7. 反直觉:受约束解码常更快:搜索空间缩小,被迫 token 干脆跳过生成。
  8. 字段顺序是逻辑:把 answer 放 reasoning 前,模型先承诺后思考,JSON 合法但答案错。
  9. 巨型 enum 改检索:先 top-k 候选再约束,免得编译超时。
  10. 语法过严逼模型编造:强制日期格式会逼出假日期,允许 null/哨兵。

下一节,我们转向一个最基础的语义判断——进入「自然语言推断」,看如何判定一句前提是否蕴含、矛盾或中立于另一句假设,这是 RAG 忠实度与事实核查的基石。


发布者: 作者: Rohit Gupta 转发
评论区 (0)
U