背景:为什么”返回 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 只能是 low、medium、high |
| 必填约束 | 调用链下游必须依赖的字段必须显式声明 |
| 版本边界 | 当字段变更时,需要让调用方知道当前使用的是哪个 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,也建议保留后置校验层。原因有三点:
- 不同模型和推理后端对 schema 特性的支持范围不同
- schema 合规不等于业务规则合规
- 拒答、安全过滤、超时、截断等情况仍需要统一处理
一个典型流程如下:
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 由类型系统生成,模型输出进入类型校验,业务规则在后置逻辑中执行。
参考资料
- OpenAI, Structured model outputs. https://developers.openai.com/api/docs/guides/structured-outputs
- vLLM Documentation, Structured Outputs. https://docs.vllm.ai/en/latest/features/structured_outputs/
- LMSYS, Fast JSON Decoding for Local LLMs with Compressed Finite State Machine. https://www.lmsys.org/blog/2024-02-05-compressed-fsm/
- dottxt-ai, Outlines: Structured Outputs. https://github.com/dottxt-ai/outlines
- Geng et al., JSONSchemaBench: A Rigorous Benchmark of Structured Outputs for Language Models. https://arxiv.org/abs/2501.10868
- Zheng et al., SGLang: Efficient Execution of Structured Language Model Programs. https://arxiv.org/abs/2312.07104
- Le, Schema Key Wording as an Instruction Channel in Structured Generation under Constrained Decoding. https://arxiv.org/abs/2604.14862