背景:为什么”请输出 JSON”在生产里不够用
大模型进入业务系统后,输出不再只是给人阅读的自然语言。客服工单分流、合同字段抽取、Agent 工具调用、报表生成、风控解释、RAG 答案引用,都需要模型输出可以被程序稳定解析的结构化数据。
早期做法通常是提示词约束:
请严格输出 JSON,不要输出 Markdown,不要解释。
这类提示在 demo 阶段可用,但在生产环境会出现几个典型问题:
| 问题类型 | 表现 | 影响 |
|---|---|---|
| Markdown 包裹 | 模型在 JSON 外包一层 ```json 代码块 | 下游解析失败 |
| 字段名漂移 | riskLevel 变成 risk_level | DTO 反序列化异常 |
| 类型错误 | 必填字段缺失,数字输出成字符串 | Schema 校验失败 |
| 枚举漂移 | high 变成 urgent | 业务分支逻辑断裂 |
| 工具参数不稳定 | Agent 参数看似合理但不符契约 | 工具调用失败 |
Structured Outputs 解决的不是”让模型更会写 JSON”,而是把输出格式从”提示词建议”提升到”解码阶段约束”。 这就是 Constrained Decoding 的价值:在生成每个 token 时,根据 JSON Schema、正则、EBNF 或上下文无关文法,只允许当前状态下合法的 token 进入候选集合。
核心原理:从概率生成到受约束生成
LLM 的普通生成过程可以理解为:模型根据上下文给出下一个 token 的概率分布,然后采样或选择一个 token。普通提示词只能影响概率,但不能从根上禁止非法结构。
Constrained Decoding 在模型 logits 之后、采样之前增加一层 mask:
- 把业务 Schema 编译为一个可执行的语法状态机或解析器。
- 每生成一步,根据当前已生成内容判断下一步哪些 token 合法。
- 将非法 token 的概率屏蔽掉。
- 只在合法 token 集合中继续采样或选择。
- 直到生成完整结构或触发停止条件。
这意味着,如果 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 mode | Structured 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;
这里不要把”重试”作为唯一兜底。对于高风险链路,应明确区分三类失败:
- 结构失败:JSON 不合法、字段缺失、类型错误。
- Schema 失败:结构合法但不符合契约。
- 业务失败:契约合法但业务规则不接受。
适用场景:哪些任务最值得上 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 能完全替代人工审核吗?
不能。它解决的是结构契约问题,而不是事实正确性、合规判断和业务责任问题。对于金融、医疗、保险、法务、支付等高风险场景,即使输出结构完全正确,也应保留人工审核、规则引擎或审批流。
参考资料
- OpenAI:Introducing Structured Outputs in the API — https://openai.com/index/introducing-structured-outputs-in-the-api/
- OpenAI API: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/
- SGLang Documentation:Structured Outputs — https://docs.sglang.io/docs/advanced_features/structured_outputs
- Anthropic Claude Docs:Structured outputs — https://platform.claude.com/docs/en/build-with-claude/structured-outputs
- XGrammar GitHub — https://github.com/mlc-ai/xgrammar
- XGrammar: Flexible and Efficient Structured Generation Engine for Large Language Models — https://arxiv.org/abs/2411.15100
- XGrammar 2: Dynamic and Efficient Structured Generation Engine for Agentic LLMs — https://arxiv.org/abs/2601.04426
- Generating Structured Outputs from Language Models: Benchmark and Studies / JSONSchemaBench — https://arxiv.org/abs/2501.10868