文章

Structured Outputs 生产实战:用 Constrained Decoding 稳定生成 JSON Schema

本文系统讲解 Structured Outputs 与 Constrained Decoding 的核心原理、Schema 设计、服务端接入、性能成本、异常处理、上线检查与常见误区,帮助 LLM 应用稳定生成可解析 JSON。

背景:为什么”请输出 JSON”在生产里不够用

大模型进入业务系统后,输出不再只是给人阅读的自然语言。客服工单分流、合同字段抽取、Agent 工具调用、报表生成、风控解释、RAG 答案引用,都需要模型输出可以被程序稳定解析的结构化数据。

早期做法通常是提示词约束:

请严格输出 JSON,不要输出 Markdown,不要解释。

这类提示在 demo 阶段可用,但在生产环境会出现几个典型问题:

问题类型表现影响
Markdown 包裹模型在 JSON 外包一层 ```json 代码块下游解析失败
字段名漂移riskLevel 变成 risk_levelDTO 反序列化异常
类型错误必填字段缺失,数字输出成字符串Schema 校验失败
枚举漂移high 变成 urgent业务分支逻辑断裂
工具参数不稳定Agent 参数看似合理但不符契约工具调用失败

Structured Outputs 解决的不是”让模型更会写 JSON”,而是把输出格式从”提示词建议”提升到”解码阶段约束”。 这就是 Constrained Decoding 的价值:在生成每个 token 时,根据 JSON Schema、正则、EBNF 或上下文无关文法,只允许当前状态下合法的 token 进入候选集合。


核心原理:从概率生成到受约束生成

LLM 的普通生成过程可以理解为:模型根据上下文给出下一个 token 的概率分布,然后采样或选择一个 token。普通提示词只能影响概率,但不能从根上禁止非法结构。

Constrained Decoding 在模型 logits 之后、采样之前增加一层 mask

  1. 把业务 Schema 编译为一个可执行的语法状态机或解析器。
  2. 每生成一步,根据当前已生成内容判断下一步哪些 token 合法。
  3. 将非法 token 的概率屏蔽掉。
  4. 只在合法 token 集合中继续采样或选择。
  5. 直到生成完整结构或触发停止条件。

这意味着,如果 Schema 要求字段 priority 只能是 lowmediumhigh,模型即使倾向于输出 urgent,解码器也会把该路径排除。结构正确性不再完全依赖模型”听话”。

但这也带来一个容易被忽略的事实:约束解码保证结构,不保证事实与业务判断正确。模型可以稳定输出:

{ "priority": "high", "reason": "用户表达了紧急诉求", "confidence": 0.82 }

这个 JSON 可能完全符合 Schema,但 priority 是否判断正确,仍然需要业务规则、人工标注集、离线评估或线上抽检验证。


三种模式的对比:Structured Outputs、JSON mode 与 Function Calling

JSON mode

JSON mode 的目标通常是让输出成为合法 JSON。它能减少明显的格式错误,但不保证字段集合、字段类型、嵌套结构和枚举值符合你的 Schema。

维度JSON modeStructured Outputs
约束层级输出合法 JSON输出严格匹配 Schema
字段保障不保证保证 required 字段存在
类型保障不保证保证类型匹配
枚举约束不约束严格限缩可选值
额外字段可能出现additionalProperties: false 可杜绝
  • 适合场景:轻量抽取、内部脚本、非关键链路。
  • 不适合场景:支付参数、数据库写入、自动审批、工具调用参数、需要强契约的 API 返回值。

Structured Outputs

Structured Outputs 要求开发者提供 Schema,并在生成阶段让模型输出匹配该 Schema。OpenAI 在 2024 年 8 月发布 Structured Outputs 时明确区分了 JSON mode 与严格 Schema 跟随:JSON mode 只提高有效 JSON 的概率,Structured Outputs 才是真正面向开发者提供的 JSON Schema 进行约束。

  • 适合场景:结构化抽取、前后端契约、LLM 生成业务对象、Agent 中间状态、复杂表单生成。

Function Calling / Tool Calling

Function Calling 更关注”模型决定调用哪个工具,以及传入什么参数”。当工具参数启用严格 Schema 时,它本质上也是 Structured Outputs 的重要使用方式。

  • 适合场景:Agent 工具调用、搜索、数据库查询、订单查询、CRM 更新、代码执行前参数校验。

工程落地:推荐的三层架构

生产环境不要把 Structured Outputs 当成单个 API 参数,而应该设计成三层能力。

第一层:Schema Contract

Schema 是业务契约,不是提示词的一部分。建议将它纳入版本管理,并与下游服务的 DTO、数据库写入结构或事件格式对齐。

一个工单分流 Schema 可以这样设计:

{
  "type": "object",
  "additionalProperties": false,
  "required": ["category", "priority", "summary", "confidence"],
  "properties": {
    "category": {
      "type": "string",
      "enum": ["billing", "technical", "account", "other"]
    },
    "priority": {
      "type": "string",
      "enum": ["low", "medium", "high"]
    },
    "summary": {
      "type": "string",
      "description": "A concise summary of the customer issue."
    },
    "confidence": {
      "type": "number",
      "minimum": 0,
      "maximum": 1
    }
  }
}

Schema 设计要遵守四个原则:

  • 字段少而稳定:不要把所有潜在字段一次性塞进去。
  • 枚举明确:后端需要分支处理的字段优先用枚举。
  • 禁止额外字段:关键链路使用 additionalProperties: false
  • 版本可演进:新增字段、字段废弃、枚举扩展都要有版本策略。

第二层:Generation Adapter

不同推理平台的接口不同。OpenAI、Claude、vLLM、SGLang、TensorRT-LLM、XGrammar、Guidance、Outlines 的参数和支持范围并不完全一致。因此建议在业务代码中增加一层 adapter,把业务 Schema 转成目标后端支持的格式。

type StructuredBackend = "openai" | "vllm" | "sglang";

interface StructuredRequest {
  model: string;
  messages: Array<{ role: string; content: string }>;
  schemaName: string;
  schema: Record<string, unknown>;
  temperature?: number;
}

async function generateStructured(
  backend: StructuredBackend,
  request: StructuredRequest
): Promise<unknown> {
  if (backend === "openai") {
    return callOpenAIStructuredOutputs(request);
  }
  if (backend === "vllm") {
    return callOpenAICompatibleServer(request, {
      structured_outputs: { json: request.schema }
    });
  }
  if (backend === "sglang") {
    return callOpenAICompatibleServer(request, {
      response_format: {
        type: "json_schema",
        json_schema: request.schema
      }
    });
  }
  throw new Error(`Unsupported backend: ${backend}`);
}

这层 adapter 的价值是:业务代码只关心”我要一个 TicketTriageResult”,不关心底层是 OpenAI 原生 Structured Outputs,还是 vLLM 的 structured_outputs,还是 SGLang 的 json_schema

第三层:Validation & Recovery

即使使用约束解码,也必须保留后置校验。原因包括:

  • 模型可能因为拒答、长度限制、服务端错误导致输出不完整。
  • 不同平台支持的 JSON Schema 子集不同。
  • 某些约束只能保证语法,不保证业务语义。
  • 下游系统还需要做权限、范围、状态机校验。

推荐流程:

const raw = await generateStructured(backend, request);
const parsed = schemaValidator.safeParse(raw);

if (!parsed.success) {
  logStructuredOutputError({ raw, errors: parsed.error });
  return fallbackToHumanReview(raw);
}

const businessCheck = validateBusinessRules(parsed.data);
if (!businessCheck.ok) {
  return fallbackToManualQueue(parsed.data, businessCheck.reason);
}

return parsed.data;

这里不要把”重试”作为唯一兜底。对于高风险链路,应明确区分三类失败:

  1. 结构失败:JSON 不合法、字段缺失、类型错误。
  2. Schema 失败:结构合法但不符合契约。
  3. 业务失败:契约合法但业务规则不接受。

适用场景:哪些任务最值得上 Structured Outputs

场景典型任务核心收益
信息抽取邮件、合同、聊天记录、工单字段提取下游直接入库、进入审批流
Agent 工具调用搜索、查询、CRM 操作参数始终符合服务端契约
RAG 引用与证据答案 + 引用 ID + 置信度 + 复核标记输出可审计、可追溯
前端动态表单生成表单字段、筛选条件、图表配置前端按 Schema 渲染
批量离线处理大规模结构化抽取减少解析失败和人工修复

1. 信息抽取

从邮件、合同、聊天记录、工单中提取字段,是 Structured Outputs 的典型场景。输出结构稳定后,下游可以直接进入数据库、审批流或 BI。

2. Agent 工具调用

Agent 最容易出事故的地方不是”不会回答”,而是”调用工具时参数不稳”。Structured Outputs 可以让工具参数始终符合服务端契约,降低错误调用、注入式参数和无效请求。

3. RAG 引用与证据结构

RAG 答案可以要求输出:答案、引用片段 ID、置信度、不确定原因、是否需要人工复核。这样比让模型自由写一段解释更容易审计。

4. 前端动态表单或 UI 状态

当模型需要生成表单字段、筛选条件、图表配置时,Structured Outputs 能让前端按 Schema 渲染,而不是解析自然语言。

5. 批量离线处理

在批处理任务中,失败样本会放大运维成本。强结构输出可以显著减少解析失败、人工修复和重复运行。


常见误区

误区一:有了 Structured Outputs 就不需要提示词

约束解码负责结构,但模型仍然需要理解任务。Schema 中的字段描述、系统提示和少量示例仍然重要。SGLang 文档也建议在提示中明确说明期望格式,以改善输出质量。

误区二:Schema 越细越好

复杂 Schema 会增加编译、缓存和解码成本。Claude 文档明确提到 Structured Outputs 会把 JSON Schema 编译成约束语法,复杂 Schema 会产生更大的 grammar,并带来更长编译时间。

工程上建议先把 Schema 控制在”业务必须稳定”的字段上,避免为了追求完整性引入大量可选字段、深层 anyOf、复杂正则和长枚举。

误区三:结构正确等于答案正确

JSONSchemaBench 等研究关注的不仅是约束合规,还包括效率、覆盖范围和生成质量。生产环境也应同时观察结构成功率、字段准确率、业务命中率和人工复核率,而不是只看 JSON parse 成功率。

误区四:所有模型和后端支持一致

OpenAI、Claude、vLLM、SGLang、XGrammar 等系统都支持结构化生成能力,但可用参数、Schema 子集、语法后端、限制条件和性能表现不同。迁移后端时必须跑回归测试,而不是只改 base_url

误区五:出错后无限重试

重试能缓解偶发失败,但不能修复 Schema 设计不合理、字段描述不清、max_tokens 太小、模型能力不足或业务规则冲突。高风险场景应进入人工队列或降级流程。


性能成本:约束解码不是免费的

Constrained Decoding 需要在每一步生成时计算合法 token 集合。对于简单 JSON,开销通常可控;对于复杂递归结构、深层嵌套、大量枚举、复杂正则或动态工具集合,开销会明显增加。

XGrammar 的研究方向正是降低这类开销。XGrammar 通过预检查上下文无关 token、持久化栈、与推理引擎协同等方法降低结构化生成成本;2026 年的 XGrammar 2 又面向 Agent 动态结构生成,引入动态分发、JIT 编译和跨 grammar 缓存,用来减少动态工具与条件结构的编译成本。

生产环境要重点监控:

指标说明
Schema compile latency首次使用某个 Schema 的编译耗时
Time per output token (TPOT)结构化输出是否拉高每 token 耗时
Cache hit rate相同 Schema 是否复用编译结果
Decode failure reason长度截断、拒答、服务端限制或 Schema 不支持
Fallback rate进入人工或普通 JSON 模式的比例

上线检查清单

Schema 检查

  • 每个 Schema 有唯一名称和版本号。
  • 禁止把用户隐私、客户名称、身份证号等敏感信息写进字段名、枚举值或正则。
  • 关键链路设置 additionalProperties: false
  • 所有枚举值都有明确业务含义。
  • Schema 变更有兼容策略:新增字段、废弃字段、枚举扩展、字段改名必须有迁移计划。

推理后端检查

  • 明确当前后端支持的 JSON Schema 子集。
  • 跑过最小 Schema、复杂 Schema、异常输入、空输入、超长输入测试。
  • 验证拒答、max_tokens 截断、服务超时、内容安全拦截时的返回结构。
  • 验证不同 temperature 下结构稳定性。
  • 验证批量请求、流式输出、工具调用并行开关是否影响结构保证。

业务校验检查

  • 使用 Zod、Pydantic、JSON Schema Validator 或服务端 DTO 做二次校验。
  • 区分结构错误、Schema 错误、业务错误。
  • 对关键字段建立人工标注评估集。
  • 记录模型原始输出、解析结果、校验错误和最终处理路径。
  • 对低置信度、规则冲突和高风险结果进入人工复核。

观测与回归检查

  • 记录结构成功率、字段准确率、人工复核率、重试率。
  • 每次 Schema 变更都跑离线回归集。
  • 每次模型升级都跑同一批结构化输出用例。
  • 对不同后端做兼容性测试,避免”OpenAI 可用,vLLM 不可用”的迁移风险。
  • 将失败样本沉淀为回归用例,而不是只在日志中查看。

一个可落地的选型建议

如果你正在做 SaaS 或企业内部系统,可以按下面方式选择:

  • 使用闭源 API 时,优先使用官方 Structured Outputs 或 strict tool calling。
  • 使用自部署模型时,优先评估 vLLM 或 SGLang 的结构化输出能力,并关注 XGrammar、Guidance、Outlines、llguidance 等后端支持。
  • 简单抽取任务先用扁平 JSON Schema,不要一开始设计复杂嵌套。
  • Agent 工具参数必须走严格 Schema,并在工具执行前做服务端校验。
  • 高风险任务不要直接自动执行,应加人工复核或业务规则二次确认。

FAQ

Structured Outputs 和 JSON mode 有什么区别?

JSON mode 通常只约束输出是合法 JSON,不能保证字段、类型、枚举和嵌套结构严格符合业务 Schema;Structured Outputs 会把 Schema 转成生成约束,在解码阶段限制可选 token,从而提升结构一致性。

Constrained Decoding 会不会影响回答质量?

它能提升格式可靠性,但不能自动保证语义正确。Schema 过细、枚举过多或字段设计不合理时,模型可能生成结构正确但业务含义较弱的结果,因此仍需要校验、评估与回归测试。

生产环境应该优先使用提示词、重试还是约束解码?

稳定接口、表单抽取、工具调用参数等强结构场景应优先使用约束解码;开放式总结和解释类任务可以用轻量格式提示加后置校验。两者可以组合,但不要只依赖重试。

Structured Outputs 能完全替代人工审核吗?

不能。它解决的是结构契约问题,而不是事实正确性、合规判断和业务责任问题。对于金融、医疗、保险、法务、支付等高风险场景,即使输出结构完全正确,也应保留人工审核、规则引擎或审批流。


参考资料

常见问题

Structured Outputs 和 JSON mode 有什么区别?
JSON mode 通常只约束输出是合法 JSON,不能保证字段、类型、枚举和嵌套结构严格符合业务 Schema;Structured Outputs 会把 Schema 转成生成约束,在解码阶段限制可选 token,从而提升结构一致性。
Constrained Decoding 会不会影响回答质量?
它能提升格式可靠性,但不能自动保证语义正确。Schema 过细、枚举过多或字段设计不合理时,模型可能生成结构正确但业务含义较弱的结果,因此仍需要校验、评估与回归测试。
生产环境应该优先使用提示词、重试还是约束解码?
稳定接口、表单抽取、工具调用参数等强结构场景应优先使用约束解码;开放式总结和解释类任务可以用轻量格式提示加后置校验。两者可以组合,但不要只依赖重试。
Structured Outputs 能完全替代人工审核吗?
不能。它解决的是结构契约问题,而不是事实正确性、合规判断和业务责任问题。对于金融、医疗、保险、法务、支付等高风险场景,即使输出结构完全正确,也应保留人工审核、规则引擎或审批流。