结构化输出与受约束解码 本节摘要:让 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 节(子词分词)。
阅读完本节,你应当能够:
一个分类器提示 LLM:「返回 {positive, negative, neutral} 之一」。模型返回:「情感是积极的——这条评论压倒性地正面,因为顾客明确说……」。你的解析器崩了,分类器 F1 是 0。
自由格式生成不是契约,是建议。生产系统需要契约。2026 有三层:
response_format、Anthropic 工具调用、Gemini JSON 模式。在支持的 schema 上可靠。厂商锁定。受约束解码如何工作。 每个生成步,LLM 在整个词表(约 10 万 token)上产出一个 logit 向量。一个 logit 处理器夹在模型与采样器之间:它计算「在目标语法——JSON Schema、正则、上下文无关语法——的当前位置上哪些 token 合法」,把所有非法 token 的 logit 设成负无穷;对剩余 logit 做 softmax 只把概率质量放在合法延续上。
2026 的实现:
guided_json、guided_regex、guided_choice、guided_grammar,后端是 Outlines、XGrammar 或 lm-format-enforcer。受约束解码常常比自由生成更快。原因有二:一,它缩小了下一 token 的搜索空间;二,精巧实现对「被迫 token」(像 {"name": " 这种脚手架,每个字节都已确定)干脆跳过 token 生成。
字段顺序要紧。把 answer 放在 reasoning 前,模型就在思考前承诺了答案。JSON 合法,答案错了,没有任何校验抓得到。
// 坏 {"answer": "yes", "reasoning": "because ..."} // 好 {"reasoning": "... therefore ...", "answer": "yes"}
⚠️ Schema 字段顺序是逻辑,不是排版。
核心想法,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。
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 让非法输出不可达。
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 次)。适任何厂商,但重试加延迟与成本。跨厂商可移植是卖点。
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 + 最便宜模型 |
date: "YYYY-MM-DD" 正则,模型就无法为缺失日期输出「unknown」,转而编造一个日期。允许 null 或哨兵值。保存为 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.
Review(sentiment, confidence, evidence_span),测 100 条评论里多少能解析为合法 JSON。\d{3}-\d{3}-\d{4})的正则约束解码器,在 1000 个样本上验证零非法输出。下一节,我们转向一个最基础的语义判断——进入「自然语言推断」,看如何判定一句前提是否蕴含、矛盾或中立于另一句假设,这是 RAG 忠实度与事实核查的基石。