为什么结构化输出不是简单的格式化问题
很多 LLM 应用在 Demo 阶段只需要一句提示词:「请用 JSON 返回」。到了生产环境,这种方式会迅速暴露问题:字段缺失、枚举非法、嵌套结构错误、Markdown 包裹 JSON、工具调用参数不稳定、最终结果无法被后端安全解析。
Structured Outputs 的价值在于,它不只是提示模型「像 JSON 一样输出」,而是在生成阶段约束输出必须符合 schema。对表单抽取、订单查询、Agent 工具调用、自动化审批、RAG 结构化答案来说,这比普通 prompt 更接近工程接口。
但新的问题也随之出现:如果系统既要求模型调用工具,又要求模型输出严格 JSON,应该把这两件事放在同一次生成里吗?答案通常是否定的。
核心结论:工具调用和最终结构化输出要分层
一个生产级 Agent 通常有两类输出:
Action output: 模型选择工具、填写参数、触发外部系统
Final output: 模型根据证据生成稳定结构化结果
这两类输出的目标不同。
- 工具调用关注的是 action selection:是否需要查数据库、调用哪个 API、参数怎么填、失败后是否重试。
- 结构化输出关注的是 contract compliance:最终结果是否满足 JSON Schema、字段是否完整、类型是否正确、枚举是否合法。
如果把两者强行塞进同一个 schema-constrained decoding 过程,模型可能为了满足最终 JSON 结构而减少工具调用意愿。近期关于开源模型的研究把这种现象称为 tool suppression:schema 合规率看起来很高,但工具执行能力被压低。
核心知识结构
可以把这个主题拆成 6 个工程模块。
1. JSON Mode
JSON mode 解决的是「输出是不是合法 JSON」。它不能可靠保证 required 字段、enum、嵌套结构和业务规则都符合要求。
2. Structured Outputs
Structured Outputs 解决的是「输出是否符合 JSON Schema」。它更适合直接对接后端接口、数据库写入、UI 渲染和工作流节点。
3. Constrained Decoding
Constrained decoding 会在每一步生成时限制可选 token,使输出只能沿着合法 grammar 或 schema 继续生成。它比事后解析和 retry 更强,但也会引入约束开销。
4. Function Calling
Function calling 是模型与外部系统交互的动作层。它的重点不是最终文本格式,而是工具选择、参数填写、执行顺序和错误恢复。
5. Tool Schema
工具 schema 不是普通 API 文档。它会影响模型如何理解工具边界、参数含义、使用时机和返回结果。
6. Two-Pass Execution
两阶段执行把「工具行动」和「最终结构化回答」拆开:第一阶段允许模型完成工具调用,第二阶段再用 schema 约束最终输出。
为什么单次强约束容易出问题
假设你要做一个订单客服 Agent。用户问:
帮我查一下上个月未按时送达的订单,并按订单号、原因、建议动作返回 JSON。
系统有两个目标:
- 调用订单查询工具,拿到真实订单数据
- 按固定 JSON Schema 输出结果
错误设计通常是这样:
User task + Tool definitions + Final JSON Schema -> One model call under strict schema
这种设计看起来简单,但容易产生 4 类问题。
问题一:模型优先满足最终 schema
当最终 schema 已经要求输出 orders、reason、next_action 等字段时,模型可能直接生成结构化结果,而不是先调用工具获取证据。
问题二:工具调用 token 与 schema token 竞争
在部分 guided decoding 实现中,schema 会被编译成 grammar 或 token mask。如果 mask 只允许最终 JSON 结构继续生成,工具调用相关 token 可能变得不可达或低优先级。
问题三:评测指标会误导
如果只看 JSON 是否 valid,系统可能得到很高分。但这不能说明工具真的被调用,也不能说明结果来自真实数据。
问题四:失败恢复困难
工具调用失败、参数缺失、权限不足、外部 API 超时,都需要 action 层恢复。如果生成过程被最终 schema 强约束,恢复路径会变窄。
推荐架构:Transparent Two-Pass Execution
更稳的方式是把 Agent 分成两个阶段。
第一阶段:工具执行阶段
目标是完成事实获取和动作执行。
- Input: 用户任务 + 可用工具 + 工具使用规则
- Output: 工具调用、工具结果、压缩证据、错误状态
- Constraint: 约束工具参数,但不要强制最终业务 JSON
这一阶段关注:
- 是否需要工具
- 调用哪个工具
- 参数是否完整
- 工具结果是否足够
- 失败是否需要重试或降级
第二阶段:最终结构化输出阶段
目标是把证据转成稳定的业务结果。
- Input: 用户任务 + 第一阶段证据 + 最终 JSON Schema
- Output: 符合 schema 的最终 JSON
- Constraint: 严格 schema-constrained decoding
这一阶段关注:
- required 字段是否完整
- enum 是否合法
- 数组和嵌套对象是否符合结构
- 无证据字段是否明确为空或标记
unknown - 是否可被后端直接解析
一个可落地的伪代码结构:
async function runAgentTask(userInput: string) {
const actionTrace = await runToolPhase({
userInput,
tools: [searchOrders, getShipmentDetail, createSupportTicket],
policy: {
requireEvidenceBeforeFinalAnswer: true,
maxToolCalls: 5,
compressToolResults: true,
},
});
const finalJson = await runStructuredFinalPhase({
userInput,
evidence: actionTrace.compactedEvidence,
schema: LateDeliveryOrderReportSchema,
strict: true,
});
validate(finalJson, LateDeliveryOrderReportSchema);
logTrace({ userInput, actionTrace, finalJson });
return finalJson;
}
这段结构的重点不是具体 SDK,而是边界:工具执行阶段不背最终 JSON 的结构压力,最终输出阶段不再决定是否调用工具。
Schema 设计也会影响模型行为
很多团队以为 schema 只是后端校验规则。实际上,schema 的字段名、description、enum 命名和嵌套方式都会向模型传递隐含指令。
例如下面两个字段,行为暗示并不一样:
{ "status": "string" }
{
"delivery_risk_level": {
"type": "string",
"enum": ["low", "medium", "high", "unknown"],
"description": "Risk level based only on verified shipment evidence. Use unknown when evidence is insufficient."
}
}
第二种写法更适合生产,因为它把业务含义、证据边界和 fallback 规则都写进 schema。
工具定义比工具数量更重要
工具调用失败很多时候不是模型能力问题,而是工具面设计问题。
一个生产工具定义至少应说明:
- 工具做什么
- 什么时候应该用
- 什么时候不应该用
- 每个参数是什么意思
- 参数格式和业务限制是什么
- 返回结果包含什么
- 返回结果不包含什么
- 调用失败后如何处理
工具返回也要控制长度。不要把原始日志、HTML、大段 JSON 全部返回给模型。更合理的是返回高信号结构:
{
"order_id": "O-9182",
"delivery_status": "late",
"promised_date": "2026-05-12",
"actual_date": "2026-05-15",
"evidence": ["carrier_scan_delayed", "warehouse_dispatch_late"],
"recommended_action": "issue_shipping_credit"
}
评测不能只看 schema 合规率
Structured Outputs 很容易让团队产生一个错觉:只要 JSON 通过校验,系统就可靠了。
生产评测至少要拆成 4 层:
| 评测维度 | 检查内容 |
|---|---|
| Format Compliance | 输出是否是合法 JSON,是否通过 schema validation |
| Tool Invocation Quality | 是否在需要时调用工具,是否没有在不需要时滥用工具 |
| Argument Accuracy | 工具参数是否来自用户任务,时间范围、ID、枚举和过滤条件是否正确 |
| Evidence Faithfulness | 最终 JSON 中的结论是否来自工具结果,而不是模型补全 |
一个简化评测样例:
task: "Find May orders that were fulfilled but delivered late."
expected_behavior:
must_call_tools:
- search_orders
- get_shipment_detail
must_not_fabricate_orders: true
final_schema_valid: true
evidence_required_for_each_order: true
metrics:
- schema_valid_rate
- tool_call_recall
- tool_argument_accuracy
- evidence_faithfulness
- end_to_end_success_rate
如果只看 schema_valid_rate,你可能会把一个「会编 JSON 但不会查数据」的 Agent 误判为可靠。
生产落地 Checklist
上线前建议检查:
- 是否区分工具调用阶段和最终结构化输出阶段
- 是否避免把最终业务 JSON Schema 强行套在 action selection 上
- 是否明确每个工具的使用边界
- 是否为复杂参数提供 schema-validated examples
- 是否控制工具返回长度,只保留高信号字段
- 是否对最终输出使用 schema validation
- 是否设计无证据字段的 fallback,例如
unknown、null、evidence_missing - 是否记录完整 trace:用户输入、工具调用、参数、工具结果、最终 JSON
- 是否同时评测 schema 合规率和工具调用召回率
- 是否为 open-weight model 单独评测 tool suppression 风险
- 是否在不同 serving backend 上验证 structured output 行为
- 是否禁用或谨慎使用与 schema 不兼容的并行工具调用模式
结论
Structured Outputs 解决的是「最终结果能不能被系统稳定解析」,Tool Calling 解决的是「模型能不能正确行动并获取证据」。这两个能力都很重要,但它们不应该被粗暴合并成一次强约束生成。
更稳的生产设计是:先行动,再约束,再验证——让 Agent 在工具阶段完成检索、查询、执行和证据收集,让模型在最终阶段按 JSON Schema 输出稳定结果,再用 schema validation、evidence check 和 trace 监控闭环。
对工程团队来说,关键原则是:不要把 Structured Outputs 当成万能可靠性补丁。它应该约束最终接口契约,而不是替代工具规划、证据获取和 Agent 评测。