背景问题:不是所有”能解析”的 JSON 都能上线
很多大模型应用在原型阶段看起来很顺利——提示词里写一句”请返回 JSON”,模型大多数时候也的确会给出类似 JSON 的文本。问题通常出现在接入真实业务之后:
- 字段偶尔缺失
- 类型偶尔变化(
string变number,或反过来) - 枚举值被模型自由改写
- 字符串里混入解释性文本
- 异常输入下模型干脆输出一段自然语言说明
这些问题在人工查看时并不明显,但在工程系统中会直接演变成 JSON 解析失败、字段映射失败、工具调用参数错误、工作流状态机异常。如果系统依赖重试来修复格式,延迟和成本会被成倍放大;如果直接容错吞掉错误,又会把不确定性传导给下游服务。
Structured Outputs 的价值在于:把”请按格式回答”从一句软提示,升级为更接近接口契约的运行时约束。它通常以 JSON Schema、正则表达式、上下文无关文法(CFG)或函数参数 schema 的形式描述输出结构,再通过 Constrained Decoding 在生成过程中限制模型只能选择当前结构允许的 token。
这篇文章讨论的重点不是某个具体 API 的参数,而是生产环境中如何理解、选型、验证和监控结构化输出能力。
核心原理:Constrained Decoding 如何让输出遵守结构
大模型生成文本时,本质是在每一步根据上下文预测下一个 token。普通生成流程会计算整个词表的概率分布,然后根据采样策略选出一个 token。约束解码 在这个流程中插入了一层结构检查:
- 根据 JSON Schema、regex 或 grammar 构建解析状态机;
- 在每一步生成前,根据当前 parser state 推导哪些 token 仍然可能形成合法输出;
- 对不合法 token 做 mask,使其不可被采样;
- 采样合法 token 后,更新 parser state;
- 重复上述过程,直到生成完整结构。
可以把它理解成:模型负责内容,解码器负责边界。模型仍然决定字段值、文本内容和推理结果,但输出的格式外壳被 schema 或 grammar 严格限定。
Structured Outputs 不是 JSON Mode
二者常被混为一谈,但层次不同:
| 能力 | JSON Mode | Structured Outputs |
|---|---|---|
| 保证输出是合法 JSON | ✅ | ✅ |
| 保证字段完整性 | ❌ 不保证 | ✅ 基于 schema 约束 |
| 保证字段类型正确 | ❌ 不保证 | ✅ |
| 保证枚举值合法 | ❌ 不保证 | ✅ |
| 保证对象层级符合业务结构 | ❌ 不保证 | ✅ |
| 保证业务语义正确 | ❌ | ❌ |
OpenAI 官方文档也明确区分了二者:JSON mode 关注合法 JSON,Structured Outputs 关注 schema adherence(对 schema 的遵守程度)。
⚠️ 重要提醒:schema 合规 ≠ 业务正确。例如模型返回的金额字段类型是
number,并不表示金额一定来自真实合同;返回的risk_level是枚举之一,也不表示分类一定准确。生产系统仍然需要业务验证、权限验证和任务级评估。
工程落地:从 Schema 到稳定接口
1. 先定义输出契约,而不是先写提示词
结构化输出应从接口契约开始设计。推荐在动手前写清楚:
- 哪些字段是必须的(
required); - 哪些字段允许为空;
- 哪些字段必须是枚举;
- 哪些字段需要保留原文证据;
- 哪些字段只是模型判断,不能直接执行;
- 哪些字段需要后端二次验证。
一个规模适中、职责单一、可上线的 schema 往往比一个复杂、嵌套极深、包含大量可选字段的 schema 更可靠。字段越多,模型需要同时满足的语义约束越多;结构越深,解码器和验证器的复杂度也越高。
{
"type": "object",
"additionalProperties": false,
"required": ["category", "confidence", "evidence"],
"properties": {
"category": {
"type": "string",
"enum": ["billing", "technical", "account", "other"],
"description": "The primary support category."
},
"confidence": {
"type": "number",
"minimum": 0,
"maximum": 1,
"description": "Model confidence between 0 and 1."
},
"evidence": {
"type": "string",
"description": "Short source phrase from the user request."
}
}
}
2. 在提示词中解释字段语义
约束解码保证结构,但字段值仍由模型生成。vLLM 文档也明确指出:虽然可以直接传入 JSON Schema,但 在 prompt 中说明 schema 及字段如何填充,会显著改善输出质量。
因此,不要只把 schema 丢给推理服务,还要在系统提示词中说明:
- 每个字段的业务含义;
- 不确定时应该填什么(默认值 /
null/ 兜底枚举); - 是否允许猜测;
- 是否必须引用原文证据;
- 输出是否会被后续流程自动执行。
这一步的目标是减少”结构正确但语义错误”的结果。
3. 将结构化输出放进完整验证链路
生产系统中建议把输出处理拆成四层:
- 解码层约束:通过 JSON Schema、grammar 或 regex 限制输出结构;
- 解析层校验:确保结果能被稳定反序列化为目标对象;
- 业务层校验:检查金额范围、日期合法性、权限边界、枚举含义、跨字段逻辑一致性;
- 动作层保护:对会触发外部副作用的字段做人工确认、白名单或二次审批。
type ModelResult = {
category: "billing" | "technical" | "account" | "other";
confidence: number;
evidence: string;
};
function validateBusinessRules(result: ModelResult): string[] {
const errors: string[] = [];
if (result.confidence < 0 || result.confidence > 1) {
errors.push("confidence_out_of_range");
}
if (!result.evidence || result.evidence.length < 3) {
errors.push("missing_evidence");
}
if (result.confidence < 0.6 && result.category !== "other") {
errors.push("low_confidence_requires_safe_category");
}
return errors;
}
4. 自建推理服务要关注解码器后端
使用托管模型时,Structured Outputs 通常是 API 参数层面的选择。如果使用自建推理服务,就要关注运行时是否支持结构化输出,以及底层后端如何实现。
vLLM 支持 choice、regex、json、grammar、structural_tag 等多种结构化输出形式,并支持以 xgrammar 或 guidance 作为后端引擎。Outlines 也强调同一套结构化生成接口可以对接 OpenAI、Ollama、vLLM 等推理入口,支持 JSON Schema、正则和上下文无关文法。
选型时不要只看”是否支持 JSON Schema”,还要压测:
| 关注点 | 说明 |
|---|---|
| 冷启动延迟 | schema 编译是否拖慢首次请求 |
| TPOT 影响 | token mask 计算是否会显著增加每 token 耗时 |
| 流式输出稳定性 | parser state 在流式场景下是否可靠 |
| 高并发缓存 | grammar cache 在高并发下是否命中有效 |
| Tokenizer 差异 | 不同模型的 tokenizer 是否存在边界差异 |
| Schema 兼容性 | 复杂 schema 是否触发后端不支持的关键字 |
适用场景与边界
✅ 最适合的场景
Structured Outputs 最适合那些”输出必须被程序消费”的场景:
- 信息抽取:从邮件、合同、客服对话中抽取结构化字段;
- 分类路由:把用户请求分流到不同队列或工具;
- Agent 工具参数:生成可验证的函数调用参数;
- UI 生成:把答案拆成卡片、步骤、风险提示等结构;
- 数据清洗:将半结构化文本转成统一对象;
- 审核系统:输出风险类型、证据片段和建议动作。
❌ 不太适合的场景
如果输出主要是长篇创作、开放式解释或强主观判断,过强的 schema 可能压缩表达空间,反而降低答案质量。此时可以只结构化关键元数据,把自然语言主体保留为字符串字段。
常见误区
误区一:Schema 越复杂越安全
复杂 schema 会增加约束解码的计算负担和模型语义填充的难度。大量可选字段、深层嵌套、复杂联合类型,在工程上更难测试,也更容易出现”字段存在但含义不稳定”的问题。
💡 更好的做法:按业务阶段拆分 schema——先让模型输出关键决策字段,再由后续步骤补充细节。
误区二:格式合规就可以自动执行
格式合规只是第一道门。任何会影响用户账户、订单、支付、权限、风控、理赔、医疗、法律判断的结果,都必须经过业务规则或人工确认。模型返回的 JSON 不能直接等同于可信事实。
误区三:失败时无限重试
结构化输出失败时,重试可以作为短期兜底,但不能成为唯一方案。无限重试会拉高延迟和成本,还可能掩盖 schema 设计问题。更稳妥的策略:
- 小次数重试(建议 ≤ 3 次)
- 降级到更宽松的 schema
- 回退到 JSON mode
- 转人工或返回可解释的错误
误区四:只看 Parse Failure,不看质量指标
parse failure 下降不代表产品质量提升。还要监控:
- 字段准确率
- 证据命中率
- 分类一致性
- 人工复核通过率
- 用户纠错率
上线检查清单
Schema 设计
- 字段名清晰,不使用模糊缩写
- 关键字段都有
description - 对象默认禁止未声明字段(
additionalProperties: false) - 枚举值稳定,并有
other/unknown兜底 - schema 有版本号,并保留兼容策略
- 大 schema 拆成多个小步骤独立测试
推理与性能
- 记录 schema 编译耗时
- 记录每 token mask 计算耗时
- 对比开启和关闭约束后的 TTFT、TPOT、吞吐
- 流式输出场景单独压测
- 不同模型、不同 tokenizer 单独验证
- 缓存 grammar 或 schema 编译结果
质量与安全
- 保留一套固定评测集
- 同时评估 schema match rate 与任务准确率
- 对高风险字段增加证据要求
- 对自动执行动作增加后端权限检查
- 记录原始输出、解析结果、验证错误和回退路径
- 灰度发布,支持按路由关闭结构化输出
监控指标体系
建议至少监控以下指标,并与模型版本、schema 版本、业务路由、请求长度、输出长度一起打标签,否则问题难以定位:
| 指标 | 含义 |
|---|---|
structured_output_schema_match_rate | Schema 匹配率 |
structured_output_parse_failure_total | 解析失败总数 |
structured_output_validation_failure_total | 业务校验失败总数 |
structured_output_schema_compile_ms | Schema 编译耗时(毫秒) |
structured_output_mask_compute_ms | Token mask 计算耗时(毫秒) |
structured_output_retry_total | 重试总数 |
structured_output_fallback_total | 降级回退总数 |
structured_output_task_accuracy | 任务准确率 |
总结
Structured Outputs 是连接大模型”自由生成”与业务系统”确定性消费”之间的关键桥梁。它不能解决所有问题——幻觉、语义错误、业务逻辑仍需额外的验证机制——但它能把格式层面的不确定性降到最低,让工程团队把精力集中在真正的业务质量上。
实施路径可以概括为:先定义契约 → 选型压测 → 四层验证 → 持续监控 → 灰度上线。遵循这一路径,结构化输出的投产成功率将大幅提升。
参考资料
- OpenAI Structured Outputs
- vLLM Structured Outputs
- Outlines Documentation
- XGrammar: Flexible and Efficient Structured Generation Engine for Large Language Models
- Generating Structured Outputs from Language Models: Benchmark and Studies / JSONSchemaBench
- SGLang: Efficient Execution of Structured Language Model Programs