为什么”请返回 JSON”不够用
很多 LLM 应用都会经历同一个阶段:一开始只需要自然语言回答,后来接入了工具、工作流、数据库、RPA 或前端页面,模型输出就必须被机器稳定消费。此时,Prompt 里加一句”请严格返回 JSON,不要输出多余文字”通常只能解决一部分问题。
常见故障包括:
- 模型在 JSON 前后加解释文字,导致解析失败
- 字段名轻微漂移,例如
user_id变成userid - 类型错误,例如应该是数组却返回字符串
- 枚举值不稳定,例如
pending、wait、waiting混用 - JSON 格式合法,但业务语义错误
这也是 Structured Outputs 和 Constrained Decoding 进入工程视野的原因。OpenAI 将 Structured Outputs 描述为让模型响应遵守开发者定义的 JSON Schema,并明确区分了它与 JSON mode:JSON mode 主要保证 JSON 有效,而 Structured Outputs 进一步保证 Schema adherence。官方建议尽可能优先使用 Structured Outputs,而不是只依赖 JSON mode。
核心原理:把”格式要求”变成解码约束
普通生成流程里,模型每一步都会从词表中选择下一个 token。Prompt 里的”请返回 JSON”只是软约束,模型仍然可能生成不符合结构的 token。
约束解码的思路是:在每一步生成时,根据 JSON Schema、正则表达式或语法规则,动态计算”当前状态下哪些 token 仍然合法”,再屏蔽不合法 token。模型不是先自由生成再事后修 JSON,而是在生成过程中就被限制在合法结构内。
可以把它理解成一个状态机:
- Schema 定义目标结构
- 解码器根据当前位置计算可选 token 集合
- 模型只在合法 token 中选择
- 输出完成后天然满足格式规则
这类机制在 Agent、工具调用、信息抽取、代码配置生成、自动表单填充等场景非常有用。vLLM 提供了 Structured Outputs 能力,支持通过 xgrammar 或 guidance 作为后端;llama.cpp 则支持 GBNF grammar,并提供 JSON Schema 到 GBNF 的子集转换;Outlines 定位为结构化文本生成库,支持 JSON Schema、正则和上下文无关文法。
Structured Outputs 适合解决什么问题
1. 工具调用参数稳定
Agent 调用工具时,最怕模型把参数写错。例如:
{
"name": "query_order",
"arguments": {
"order_id": "A20260629001",
"include_refund_status": true
}
}
如果 order_id 缺失,或者 include_refund_status 变成字符串,后端就要额外写大量容错逻辑。使用结构化输出后,可以把参数类型、必填字段、枚举值提前固定下来。
2. 数据抽取结果可入库
从合同、邮件、客服记录中抽取字段时,业务系统通常需要稳定字段。比如:
{
"customer_name": "string",
"policy_no": "string",
"risk_level": "low | medium | high",
"missing_fields": ["string"]
}
这类任务中,格式稳定性直接影响后续入库、检索、统计和人工复核。
3. 前端组件渲染
有些应用会让模型返回 UI 配置,例如卡片、表格、筛选项、图表参数。前端不应该靠正则猜测模型意图,而应该消费一个稳定 Schema。
4. 批处理自动化
批量跑文档处理、摘要、分类、审核时,结构化输出可以降低失败重试率,也更容易做结果比对和质量监控。
但是:格式正确不等于答案正确
这里有一个容易忽视的误区:Schema 合法率不是业务准确率。
约束解码可以让输出满足 JSON Schema,但不能保证模型理解了任务。例如,一个发票抽取任务中,模型可以输出合法 JSON,却把金额、税号或日期填错。结构没错,业务仍然错。
2025 年的 JSONSchemaBench 工作指出,受约束解码已成为结构化输出的重要技术,但系统评估仍然必要。2026 年关于 “constraint tax” 的研究也提醒,小模型在强结构约束下可能出现”格式更合法,但答案准确率下降”的情况。这个结论对生产环境很重要:不能只看 JSON parse 成功率。
因此,生产监控至少要拆成以下几类指标:
| 指标 | 说明 |
|---|---|
| 格式合法率 | 能否被 JSON parser 解析 |
| Schema 合法率 | 是否满足 JSON Schema |
| 字段完整率 | required 字段是否都存在 |
| 业务准确率 | 字段值是否符合人工标注或业务规则 |
| 可执行准确率 | 工具调用是否真的能成功执行 |
| 错误但合法率 | 结构合法,但业务语义错误的比例 |
| 重试率 | 第一次输出失败后需要修复或重新生成的比例 |
Schema 设计的工程建议
字段名要表达业务含义
字段名本身也是模型理解任务的一部分。不要写:
{ "a": "string", "b": "string" }
更好的做法是:
{ "policy_holder_name": "string", "claim_event_date": "string" }
OpenAI 文档也建议使用清晰、直观的 key,并为重要字段提供清楚的 title 和 description。字段名越贴近业务语义,模型越不容易把值放错位置。
Schema 不要一次设计得过大
复杂 Schema 会带来三个问题:
- 模型更难理解每个字段的语义
- 约束解码的状态空间更复杂
- 后续排查时难以定位错误来源
更稳妥的做法是分阶段输出:先分类,再抽取;先判断是否适用,再生成详细结构;先让模型自由推理,再让它把结论封装成结构化结果。
枚举值要少而稳定
枚举值太多会让模型陷入”看起来都差不多”的选择。生产系统中建议把枚举值控制在必要范围内,并在 description 中解释每个值的业务含义。
例如:
{
"risk_level": {
"type": "string",
"enum": ["low", "medium", "high"],
"description": "low 表示无需人工复核,medium 表示建议抽检,high 表示必须人工复核"
}
}
业务校验不要全部交给模型
Schema 适合表达结构规则,但不适合承载所有业务规则。比如”保单生效日期不能晚于终止日期""金额必须等于分项之和""证件号需要通过校验位”,这些规则应该由后端校验。
推荐链路是:
模型结构化输出 → JSON Schema 校验 → 业务规则校验 → 失败原因回传 → 必要时重试或转人工
一套可落地的结构化输出流程
第一步:定义最小可用 Schema
不要一开始就设计全量字段。先找出业务真正需要自动化消费的字段。
{
"type": "object",
"properties": {
"intent": {
"type": "string",
"enum": ["query", "create", "update", "unknown"]
},
"confidence": { "type": "number" },
"reason": { "type": "string" }
},
"required": ["intent", "confidence", "reason"],
"additionalProperties": false
}
第二步:准备失败样例
结构化输出上线前,至少准备这些测试集:
- 正常输入
- 缺字段输入
- 模糊表达输入
- 多意图输入
- 含噪声输入
- 恶意提示注入输入
- 超长上下文输入
不要只用 demo prompt 测试。结构化输出真正出问题,通常不是格式,而是边界输入下的语义判断。
第三步:区分”生成失败”和”业务失败”
建议将错误类型拆开,分别记录:
| 错误类型 | 含义 |
|---|---|
parse_error | JSON 解析失败 |
schema_error | 不符合 Schema |
validation_error | 业务规则不通过 |
execution_error | 工具执行失败 |
semantic_error | 结构合法但语义错误 |
这样线上排障会简单很多。如果只记录”LLM failed”,后面很难判断是 Prompt、Schema、模型、业务规则还是工具接口的问题。
第四步:建立降级策略
结构化输出不是上线后就结束。生产环境至少要有三类兜底:
- 自动重试:仅在格式或轻微字段错误时触发
- 修复模型:将错误 JSON 和错误原因交给模型修复,但限制次数
- 人工复核:涉及金额、合同、理赔、风控等高风险场景时必须保留人工入口
常见误区
误区一:用了 Structured Outputs 就不用写校验
错。Structured Outputs 主要解决结构问题,不替代业务校验。金额、日期、权限、状态流转仍然要由后端规则兜底。
误区二:Schema 越细越好
不一定。Schema 太复杂会降低可维护性,也可能压缩模型表达空间。尤其是小模型,过强约束可能影响答案质量。工程上应该先小范围验证,再逐步扩展。
误区三:只监控 JSON 解析成功率
JSON parse 成功率太表层。更关键的是 Schema 合法率、字段准确率、工具执行成功率和错误但合法率。
误区四:把自然语言解释完全删掉
在某些复杂判断任务中,保留 reason 字段有助于人工复核和排查。但 reason 不应参与强业务决策,只能作为解释材料。
上线检查清单
上线前建议逐项确认:
- Schema 是否只包含当前业务真正需要的字段
- 字段名是否清晰,
description是否能帮助模型理解 -
required字段是否过多 - 是否禁用了不必要的
additionalProperties - 枚举值是否稳定、互斥、数量可控
- 是否有 JSON Schema 校验和业务规则校验
- 是否记录
parse、schema、validation、execution、semantic等错误类型 - 是否建立重试、修复、人工复核策略
- 是否有边界样例和回归测试集
- 是否分别监控格式合法率和业务准确率
结论
Structured Outputs 的价值不在于”让模型看起来更像 API”,而在于把 LLM 从自然语言接口推进到可工程化集成的组件。它能降低解析失败和字段漂移的概率,让 Agent、工具调用、数据抽取和批处理任务更稳定。
但它不是银弹。格式合法只是第一层,真正决定系统质量的是 Schema 设计、业务校验、评估样例、错误分类和兜底策略。一个成熟的结构化输出系统,应该同时追求可解析、可校验、可执行、可追责。
主要参考资料
- OpenAI API Docs — Structured model outputs
- vLLM Docs — Structured Outputs
- llama.cpp Grammars README
- Outlines documentation
- Geng et al., Generating Structured Outputs from Language Models: Benchmark and Studies, 2025 — arXiv:2501.10868
- Ray, The Constraint Tax: Measuring Validity-Correctness Tradeoffs in Structured Outputs for Small Language Models, 2026 — arXiv:2605.26128