文章

Structured Outputs 生产实战:用 Constrained Decoding 让 JSON Schema 真正可用

本文系统梳理结构化输出在大模型工程中的落地方法,覆盖 JSON Schema、约束解码、校验重试、性能开销、监控指标与常见误区,帮助团队减少解析失败、线上重试和隐性业务风险。

背景:为什么”返回 JSON”仍然经常翻车

在很多大模型应用里,模型输出不再只是给用户阅读的自然语言,而是要继续进入后端流程:创建工单、抽取字段、生成报价、调用工具、更新 CRM、写入数据库或驱动前端 UI。只靠提示词要求”请返回 JSON”,在线上通常不够稳定。常见问题包括:

  • 缺少必填字段
  • 枚举值拼错
  • 数组结构漂移
  • 额外输出解释文本
  • 空值语义不一致
  • 模型在异常输入下输出无法解析的内容

Structured Outputs 的目标,是把”模型尽量按格式输出”升级为”模型输出必须满足结构约束”。OpenAI 文档将 Structured Outputs 描述为让模型响应遵循开发者提供的 JSON Schema,从而降低遗漏必填 key 或生成非法 enum 的问题。vLLM、SGLang、Outlines 等开源推理系统也提供了 JSON Schema、regex、grammar 等形式的结构化生成能力。

本文关注一个具体关键点:如何把 JSON Schema + Constrained Decoding 落到生产系统,而不是停留在 Demo 级别的格式控制。

核心原理:约束解码到底约束了什么

1. JSON Schema 是输出契约,不只是提示词附件

在生产系统中,JSON Schema 应被当作接口契约,而不是 prompt 里的一段说明。它至少承担四类职责:

职责说明
字段结构对象、数组、字符串、数字、布尔值等类型约束
业务枚举risk_level 只能是 lowmediumhigh
必填约束调用链下游必须依赖的字段必须显式声明
版本边界当字段变更时,需要让调用方知道当前使用的是哪个 schema 版本

OpenAI 文档也区分了两类场景:连接工具或系统能力时更适合使用 function calling;只是要求模型响应符合某个结构时,可以使用结构化 response format。这一点在线上很关键,因为”工具调用参数”和”普通结构化结果”应该分开治理。

2. Constrained Decoding 是生成时过滤非法 token

约束解码并不是在模型生成完以后再用正则修补,而是在生成过程中根据 schema、regex 或 grammar 动态限制下一步允许输出的 token。LMSYS 关于 compressed finite state machine 的文章解释了常见实现路径:先把 JSON Schema 转换为正则或有限状态机,在每个状态计算允许转移和可接受 token,然后通过 logits mask / bias 过滤非法 token。

这意味着约束解码能显著减少”语法不合法”的失败,但它也带来新的工程问题:

  • schema 越复杂,状态机或 grammar 处理越重
  • tokenization 边界可能影响解码效率
  • 复杂枚举和深层对象会增加延迟
  • 同一个 schema 在不同后端的支持程度可能不完全一致

3. 有效 JSON 不等于正确答案

JSONSchemaBench 论文强调,结构化输出的评估不能只看是否满足约束,还要同时看效率、约束覆盖和生成质量。生产系统也应采用同样思路:格式合规只是第一层,内容正确、业务可用、安全可控才是最终目标。

例如,下面这个输出完全符合结构,但仍可能是错误的业务判断:

{ "risk_level": "low", "reason": "用户材料完整", "missing_fields": [] }

如果输入材料实际缺少身份证号或授权书,结构化输出不会自动发现事实错误。它只会保证字段存在、类型正确、枚举合法。

工程落地:一条可控的 Structured Outputs 链路

1. Schema 先进入注册表,而不是散落在代码里

建议建立一个轻量级 Schema Registry,哪怕只是一个 Git 目录,也要记录:

schemas/
  claim_triage.v1.json
  claim_triage.v2.json
  customer_intent.v1.json
  invoice_extract.v1.json

每个 schema 至少包含:

  • schema 名称
  • 版本号
  • 适用场景
  • 下游消费者
  • 是否允许新增字段
  • null 与 unknown 的语义
  • 示例输入与示例输出
  • 回滚策略

不要让业务字段名随意变化。字段名本身会影响模型理解,最新研究也讨论了 schema key wording 可能作为隐式指令通道影响结构化生成行为。因此,字段命名应稳定、清晰、可测试,而不是为了节省几个 token 过度缩写。

2. 约束生成后仍要做显式校验

即使使用 Structured Outputs,也建议保留后置校验层。原因有三点:

  1. 不同模型和推理后端对 schema 特性的支持范围不同
  2. schema 合规不等于业务规则合规
  3. 拒答、安全过滤、超时、截断等情况仍需要统一处理

一个典型流程如下:

User Input → Prompt Builder → Schema Selection → LLM Structured Generation
→ JSON Parser → Schema Validator → Business Rule Validator
→ Typed Domain Object → Downstream Workflow

后置校验不应只返回”失败”。它应该输出可观测的错误类型:

{
  "schema_name": "claim_triage",
  "schema_version": "v1",
  "status": "invalid_business_rule",
  "error_code": "MISSING_REQUIRED_EVIDENCE",
  "field": "evidence_list",
  "retryable": false
}

3. Prompt 与 Schema 要分工明确

一个常见错误,是把所有业务说明都塞进 schema description。更稳妥的分工是:

组件职责
Prompt说明任务目标、业务背景、判断标准、边界条件
Schema限定输出结构、字段类型、枚举值、必填项
Validator执行确定性规则,例如金额范围、日期合法性、字段组合关系
Evaluator离线评估内容质量,例如事实一致性、召回率、人工一致率

Schema 不应变成”第二套 prompt”。字段描述可以帮助模型理解,但不能替代业务规则。

4. 复杂任务先拆分,再结构化

如果一个 schema 同时要求模型完成分类、抽取、解释、打分、引用证据和生成建议,失败率和调试难度都会上升。生产中更推荐拆成多段:

  • Step 1:文档分类 → 小 schema
  • Step 2:字段抽取 → 抽取 schema
  • Step 3:风险判断 → 判断 schema
  • Step 4:结果解释 → 可读文本或解释 schema

这样做的好处是每一步都能单独评估,也便于设置不同模型、不同温度、不同重试策略。

适用场景

场景说明
信息抽取合同要素、发票字段、病历摘要、工单标签、客服意图识别。结构化输出可以减少后端解析胶水代码,尤其适合字段稳定、校验规则明确的任务
工具调用参数生成Agent 调用工具时,参数必须可解析、可验证、可审计。Structured Outputs 可以降低参数格式错误,但工具执行前仍要做权限检查和业务确认
前端 UI 数据生成生成表单配置、图表数据、推荐卡片、步骤列表。schema 可以约束前端渲染所需字段,避免 UI 因缺字段而崩溃
批处理和离线数据清洗大批量数据抽取时,结构化输出能降低人工清洗成本。但要监控 parse error、retry rate、字段空值率和人工抽检一致率

常见误区

误区一:用了 Structured Outputs 就不用重试

仍然需要重试,但重试要有边界。建议区分:

失败类型策略
解析失败可短重试
schema 不兼容不可盲目重试,应报警
业务规则失败通常不该重试,应进入人工或降级路径
模型拒答按安全策略处理,而不是强行修复

误区二:schema 越详细越稳定

过深、过宽、枚举过长的 schema 会增加模型和解码后端负担。字段应尽量贴近下游真实需要。能通过业务规则计算出来的字段,不必让模型生成。

误区三:只看 parse success rate

parse success rate 很容易让团队误判。更重要的是:字段准确率、枚举混淆率、空值率、人工一致率、下游回滚率和用户纠错率。

误区四:把 JSON 结果直接执行

特别是在工具调用和 Agent 场景中,结构化参数不能等同于授权。模型生成的 JSON 只是候选决策,涉及支付、删除、发信、修改权限等副作用操作时,必须经过权限边界和确认流程。

上线检查清单

Schema 设计

  • 每个 schema 有唯一名称和版本号
  • 必填字段来自真实下游依赖,而不是”看起来完整”
  • enum 值稳定、短小、含义明确
  • null、unknown、empty 的语义已定义
  • schema 变更有兼容性检查

推理与解析

  • 使用支持 JSON Schema / grammar / regex 的结构化输出能力
  • 后置 JSON parser 和 schema validator 未删除
  • 业务规则校验独立于模型输出
  • 对拒答、截断、超时、空输出有统一错误码
  • 重试次数、重试条件和退避策略已配置

质量评估

  • 有 golden set,覆盖正常、边界、脏数据和对抗样例
  • 评估字段级准确率,而不只看整体成功率
  • 人工抽检与模型评估分开记录
  • 对不同模型、不同 schema 版本做回归比较

性能与成本

  • 监控 TTFT、输出耗时、P95/P99、tokens per second
  • 监控 constrained decoding overhead
  • 复杂 schema 做压缩、拆分或降级策略
  • 批处理任务设置独立并发与成本预算

安全与治理

  • 工具调用参数执行前经过权限校验
  • 输出内容进入下游前做敏感信息和注入检查
  • schema 与 prompt 进入版本管理
  • 出错样本可追踪到输入、模型、schema、prompt 和版本

一个简化的调用结构

下面示例展示了应用层应该关心的结构,而不是绑定某一个厂商 SDK:

from pydantic import BaseModel, Field
from typing import Literal, List

class TriageResult(BaseModel):
    intent: Literal["claim", "renewal", "complaint", "unknown"]
    confidence: float = Field(ge=0, le=1)
    missing_fields: List[str]
    should_handoff: bool
    reason: str

def run_structured_generation(client, model: str, user_text: str) -> TriageResult:
    schema = TriageResult.model_json_schema()
    response = client.generate(
        model=model,
        input=user_text,
        response_schema=schema,
        strict=True,
    )
    parsed = TriageResult.model_validate_json(response.text)
    if parsed.confidence < 0.6:
        parsed.should_handoff = True
    return parsed

这段代码表达的是工程原则:schema 由类型系统生成,模型输出进入类型校验,业务规则在后置逻辑中执行。

参考资料

  1. OpenAI, Structured model outputs. https://developers.openai.com/api/docs/guides/structured-outputs
  2. vLLM Documentation, Structured Outputs. https://docs.vllm.ai/en/latest/features/structured_outputs/
  3. LMSYS, Fast JSON Decoding for Local LLMs with Compressed Finite State Machine. https://www.lmsys.org/blog/2024-02-05-compressed-fsm/
  4. dottxt-ai, Outlines: Structured Outputs. https://github.com/dottxt-ai/outlines
  5. Geng et al., JSONSchemaBench: A Rigorous Benchmark of Structured Outputs for Language Models. https://arxiv.org/abs/2501.10868
  6. Zheng et al., SGLang: Efficient Execution of Structured Language Model Programs. https://arxiv.org/abs/2312.07104
  7. Le, Schema Key Wording as an Instruction Channel in Structured Generation under Constrained Decoding. https://arxiv.org/abs/2604.14862

常见问题

Structured Outputs 和 JSON mode 的区别是什么?
JSON mode 主要保证输出是合法 JSON;Structured Outputs 进一步要求输出遵循指定 JSON Schema,例如必填字段、枚举值和对象结构。
Constrained Decoding 是否能保证内容真实?
不能。它保证的是格式和结构约束,不保证事实正确、业务判断正确或权限安全,仍需要验证、评估和人工兜底。
什么时候不适合使用复杂 JSON Schema?
当 schema 层级过深、字段过多、枚举过长或低延迟要求极高时,需要先压缩 schema、拆分任务或评估约束解码开销。
Structured Outputs 能完全替代人工审核吗?
不能。它主要解决格式可靠性问题。涉及金额、合规、医疗、法律、删除、支付、外发等高风险动作时,仍需要人工审核或确定性业务规则兜底。