文章

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 只能是 low、medium、high,模型即使倾向于输出 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 能完全替代人工审核吗?
不能。它解决的是结构契约问题,而不是事实正确性、合规判断和业务责任问题。对于金融、医疗、保险、法务、支付等高风险场景,即使输出结构完全正确,也应保留人工审核、规则引擎或审批流。