结构化输出与约束解码:让大模型稳定吐出 JSON

在很多真实业务里,我们并不想让模型输出一段散文,而是希望它返回可以被程序直接消费的结构化数据:一段 JSON、一组字段、一个枚举值。本文讲清楚为什么需要结构化输出,以及从提示技巧到约束解码逐层递进的几套方案。

为何需要结构化输出

把大模型接入后端系统时,最朴素的做法是用提示词要求模型「按 JSON 返回」。但当请求量上来后,你会发现即便写了很详细的格式说明,模型偶尔还是会:

  • 在 JSON 前后多说几句废话,比如先写「好的,这是结果」再输出对象;
  • 字段名拼写不一致,有时叫 user_name,有时叫 username
  • 漏掉你要求的必填字段;
  • 把枚举值写成不在你允许范围内的内容。

这些不稳定会让下游解析逻辑频繁报错,于是人们开始寻找比「靠提示词祈祷」更可靠的机制。本质上,让大模型稳定吐出结构化数据有三条路线:

  1. 平台提供的格式化能力,例如 JSON Mode 与 Structured Outputs;
  2. 借由函数调用(Function Calling / Tool Use)把输出变成一次受 schema 约束的工具调用;
  3. 在解码阶段做语法约束,从 token 层面禁止模型生成非法内容。

JSON Mode:OpenAI 与各家实现

JSON Mode 是最早被广泛支持的「格式保证」能力:它保证模型返回的是合法可解析的 JSON,但不保证 JSON 的内容符合某个特定 schema。

在 OpenAI 的接口中,开启 JSON Mode 需要设置 response_format,并把 type 设为 json_object

{
  "model": "gpt-4o-mini",
  "messages": [
    { "role": "system", "content": "你是一个只输出 JSON 的助手。" },
    { "role": "user", "content": "提取这段文本里的人物姓名与年龄。" }
  ],
  "response_format": { "type": "json_object" }
}

这里有一个硬性要求:使用 JSON Mode 时,你必须在对话里的某条消息(通常是系统消息)中出现字符串「JSON」,否则 API 会直接报错。这是因为模型只有在被明确提示输出 JSON 时才会收敛到 JSON 格式。

需要特别注意:JSON Mode 只保证「能解析」,不保证「符合你的字段定义」。如果业务要求严格字段,官方也建议优先改用 Structured Outputs(即 response_format 设为 json_schemastrict: true),它会在生成阶段约束模型遵循你提供的 JSON Schema,保证不漏必填字段、不产生非法枚举值。

在开源与本地模型生态中,许多推理框架(如 vLLM、Ollama、llama.cpp)也提供了类似的 JSON Mode 或 grammar 约束选项,命名可能不同,但思路一致:在采样时限制输出为合法 JSON。

Function Calling 作为结构化输出

当你的目标本来就是「让模型填好一张固定结构的表」,函数调用往往比 JSON Mode 更自然。原理是:你向模型声明若干个函数的签名(含输入参数的 JSON Schema),模型不直接写散文,而是返回一个符合签名的 arguments 对象。

以 Anthropic Claude 为例,结构化输出通常通过 Tool Use 实现。你定义一个工具并给出 input_schema,再用 tool_choice 强制模型必须调用该工具:

{
  "model": "claude-opus-5",
  "max_tokens": 1024,
  "tools": [
    {
      "name": "extract_person",
      "description": "从文本中抽取人物结构化信息",
      "input_schema": {
        "type": "object",
        "properties": {
          "name": { "type": "string" },
          "age": { "type": "integer" }
        },
        "required": ["name", "age"]
      }
    }
  ],
  "tool_choice": { "type": "tool" },
  "messages": [
    { "role": "user", "content": "张三今年 28 岁。" }
  ]
}

tool_choice 设为 { "type": "tool" } 时,模型被强制发起一次工具调用,返回的 tool_use 块里的 input 就是结构化结果。若希望进一步保证严格符合 schema,Anthropic 还提供 strict tool use,在工具定义里加 strict: true 即可要求调用参数精确匹配 schema。

OpenAI 一侧同样可用函数调用承载结构化输出:把 tools 中的函数签名写成目标结构,tool_choice 设为 { "type": "function", "function": { "name": "..." } } 即可强制对应输出。函数调用之所以稳,是因为模型在训练阶段就被大量对齐到「按 schema 填参数」,比纯提示词更可靠。

温度设为 0 的技巧

无论用 JSON Mode 还是函数调用,把采样温度设为 0 都能显著提升结构化输出的稳定性。

温度控制输出的随机性。温度越高,模型越倾向于探索多样性,越可能在字段顺序、措辞、是否额外加注释等方面产生波动;温度设为 0 会让模型在每一步选择概率最高的 token,输出更确定、更可复现。对于「抽取」「分类」「格式化」这类有标准答案的任务,温度 0 几乎总是更优选择。

需要注意两点:

  • 温度 0 只降低随机性,并不能单独保证 schema 合规。它应作为「结构化能力 + 低温度」组合拳的一部分。
  • 部分平台对温度为 0 的调用会做去重或缓存优化,既更稳也更省成本。

约束解码与语法约束

前面几类方案大多依赖模型「愿意配合」,而约束解码(constrained decoding)走的是另一条更彻底的路:在每一步解码时,根据文法或 schema 计算「当前允许出现的下一个 token 集合」,把不在集合内的 token 概率直接置零。这样模型在物理上无法生成非法内容,输出天然合法。

以下是几个经过核实的开源约束解码库:

Outlines

Outlines(仓库 dottxt-ai/outlines)是一个专注于结构化生成的库,支持多选、正则表达式、JSON / Pydantic 模式以及上下文无关文法等多种约束方式,并且宣称可对接 OpenAI、Ollama、vLLM 等不同后端。它的口号是「在生成过程中保证结构化输出」。

Guidance

Guidance(仓库 guidance-ai/guidance)提供一套引导语言,用 system()user()assistant()gen() 等 Pythonic 写法编排提示与生成,并通过 gen(regex=...)select([...]) 等方式做约束生成,还支持完整上下文无关文法(CFG)与离线语法调试。

LM Format Enforcer

LM Format Enforcer(仓库 noamgat/lm-format-enforcer)通过字符级解析器结合分词器前缀树,在每一步只允许符合格式的 token 通过,从而强制输出 JSON Schema、正则等格式。它可与 transformers、LangChain、llama.cpp、vLLM 等集成。

约束解码的优势是「不靠运气」,且在模型能力较弱(如小尺寸本地模型)时尤其有价值;代价是需要在推理侧接入对应库,且对生成速度有少量开销。

常见失败与排错

即便用了上述方案,仍可能遇到以下问题,这里给出对应的排查方向。

输出被截断

表现:JSON 不完整,解析时报 Expecting value 或括号未闭合。

原因通常是命中了 max_tokens / max_output_tokens 上限,或模型在长字段上「写嗨了」。排查与处理:

  • 调大输出 token 上限;
  • 精简 schema,去掉非必要字段;
  • 若使用平台结构化能力,优先改用 Structured Outputs / strict tool use,它们会尽力在限额内给出完整对象,但仍需为复杂结构预留足够 token。

格式不符,前后多了自然语言

表现:模型在 JSON 前加了「这是结果:」,或在结尾补了一段解释。

处理:

  • JSON Mode 下确认系统消息包含「JSON」字样,并明确写「只输出 JSON,不要任何额外文字」;
  • 优先使用 Structured Outputs 或函数调用,从根本上让模型以结构化块返回,而不是散文;
  • 解析前可做一次「截取首个 { 到最后一个 }」的容错清洗(仅作兜底,不能作为主方案)。

少字段或字段名漂移

表现:期望 5 个字段,实际只来 3 个;或 user_nameusername 混用。

处理:

  • 把 schema 中所有字段标为必填(required),并在对象上设置 additionalProperties: false
  • 用 Structured Outputs / strict tool use 替代普通 JSON Mode;
  • 解析后做一层 schema 校验(如用 Pydantic / jsonschema),对缺失字段给出明确报错而非静默通过。

示例代码

下面给出两段可落地的示例。第一段演示如何用 OpenAI 的 Structured Outputs 让模型稳定返回规定字段;第二段演示如何用 Outlines 在本地模型上做约束生成。

示例一:OpenAI Structured Outputs

from openai import OpenAI
from pydantic import BaseModel

client = OpenAI()


class Person(BaseModel):
    name: str
    age: int
    occupation: str


# 借助 SDK 的 parse 能力,直接拿到校验后的对象
response = client.responses.parse(
    model="gpt-4o-mini",
    input=[
        {"role": "system", "content": "抽取文本中的人物信息。"},
        {"role": "user", "content": "李雷今年 30 岁,是一名软件工程师。"},
    ],
    text_format=Person,
)

person = response.output_parsed
print(person.name, person.age, person.occupation)

这段代码的关键在于 text_format=Person:SDK 会把 Pydantic 模型转成 JSON Schema,并以 strict: true 的形式提交给 Structured Outputs,保证返回对象不缺字段、类型正确。

示例二:Outlines 约束生成

import outlines
from transformers import AutoTokenizer, AutoModelForCausalLM
from typing import Literal

model_name = "microsoft/Phi-3-mini-4k-instruct"
model = outlines.from_transformers(
    AutoModelForCausalLM.from_pretrained(model_name, device_map="auto"),
    AutoTokenizer.from_pretrained(model_name),
)

# 把输出约束为固定枚举,模型在解码时无法写出范围外的内容
sentiment = model(
    "评价:这家餐厅的菜品让人失望。",
    Literal["正面", "负面", "中性"],
)
print(sentiment)

这里 Literal["正面", "负面", "中性"] 会在每一步把下一个 token 限制在这三个候选内,因此输出一定是其中之一,无需再做后处理判断。

小结

让大模型稳定吐出结构化数据,不是一个「写好提示词」就能解决的问题,而是需要按可靠度递进地选型:

  • 最基础是 JSON Mode,只保证合法 JSON,不保证字段;
  • 更稳的是 Structured Outputs 与函数调用(Tool Use),它们以 schema 约束模型,显著减少漏字段与格式漂移;
  • 把温度设为 0 能进一步降低随机性,作为所有结构化任务的默认设置;
  • 最彻底的是约束解码(Outlines、Guidance、LM Format Enforcer),在 token 层面禁止非法输出,适合本地模型与高可靠场景。

实际落地时,建议优先采用平台自带的结构化能力,在需要极致稳定或自托管模型时再引入约束解码库,并在解析后保留一层 schema 校验作为兜底。

参考与延伸阅读

  1. OpenAI 官方文档:Structured Outputs — 说明 response_format 设为 json_schemastrict: true 时的保证与限制。 https://platform.openai.com/docs/guides/structured-outputs

  2. OpenAI 官方文档:JSON Mode — 说明 response_format 设为 json_object 的启用方式与「上下文须含 JSON」的硬性要求。 https://platform.openai.com/docs/guides/text-generation/json-mode

  3. Anthropic 官方文档:Tool Use — 说明用 toolstool_choice 强制结构化调用,以及 strict tool use 的 schema 合规保证。 https://docs.anthropic.com/en/docs/build-with-claude/tool-use

  4. Outlines 开源仓库(dottxt-ai/outlines)— 支持多选、正则、JSON / Pydantic 与文法约束的结构化生成库。 https://github.com/dottxt-ai/outlines

  5. Guidance 开源仓库(guidance-ai/guidance)— 提供 gen(regex=...)select([...]) 等约束生成能力的引导语言库。 https://github.com/guidance-ai/guidance

  6. LM Format Enforcer 开源仓库(noamgat/lm-format-enforcer)— 通过字符级解析器与分词器前缀树做约束解码的库。 https://github.com/noamgat/lm-format-enforcer

本文累计阅读