Appearance
结构化输出与函数调用专题
版本:
v1.3最后更新:
2026-07-09
很多团队第一次接入大模型时,会先让模型自由输出一段自然语言。
这个阶段往往能快速看到效果,但一旦系统开始进入工程落地,很快就会遇到一组重复出现的问题:
- 输出格式不稳定
- JSON 经常缺字段或字段类型漂移
- 下游系统无法直接消费
- 工具调用链路不清晰
- 高风险动作和普通建议混在一起
这篇专题聚焦两个核心能力:
- 如何让模型稳定输出程序可消费的结构化结果
- 如何通过函数调用把模型接入真实工具和真实系统
它们不是“高级功能”,而是 Agent、RAG、审批流、评测流和生产工作流的基础地板。
1. 为什么结构化输出是工程分水岭
很多看起来像“模型不稳定”的问题,本质上并不是模型不会做,而是系统没有定义稳定的输出契约。
结构化输出真正解决的是三件事:
- 模型输出能被程序直接解析
- 输出字段有稳定边界,方便校验
- 输出可以成为工作流节点之间的中间状态
这对下面这些场景尤其关键:
- 信息抽取
- 分类与标签
- 审批结果
- 工单路由
- 风险评分
- Agent 中间状态传递
如果没有结构化输出,团队通常会被迫:
- 用正则和字符串拆文本
- 大量补解析容错
- 在工作流层不断写 if-else 补丁
这些补丁短期能跑,长期几乎一定会拖垮系统可维护性。
2. Structured Outputs、JSON mode、函数调用,分别在解决什么
这是最容易被混淆的一组概念。
2.1 JSON mode
根据 OpenAI 官方文档的定义,JSON mode 的目标是让模型返回合法 JSON。
它主要回答的是:
- 输出是不是 JSON
但它不保证:
- 字段是否完整
- 字段名是否正确
- 枚举值是否符合预期
- 深层结构是否满足你的业务 schema
2.2 Structured Outputs
Structured Outputs 是比 JSON mode 更严格的一层。它的价值在于:
- 不只要求输出是 JSON
- 还要求输出符合你给定的 schema
这带来一个很重要的工程结论:
- 如果系统依赖稳定字段、严格类型和固定结构,优先考虑 Structured Outputs
2.4 Structured Outputs 也要区分“给用户的结构”和“给工具的结构”
OpenAI 当前 Structured outputs 文档已经很清楚地区分了两类使用方式:
- 用
response_format/text.format约束模型回复给用户的结构 - 用 function calling 约束模型调用工具时的参数结构
这意味着工程上不要把两类 schema 混成一套:
- 面向用户的 schema 更关注展示和消费
- 面向工具的 schema 更关注执行安全和参数边界
如果两类结构不分开,常见问题是:
- 给用户看的字段被迫背负执行语义
- 给工具的参数结构又被展示层拖得很复杂
2.5 Structured Outputs、工具参数 schema 和工具结果 schema 最好分三份
很多团队走到生产后会发现,真正需要稳定的不只是:
- 模型最后吐出来的结构
还包括:
- 模型调用工具时写入的参数
- 工具返回后再喂回模型的结果结构
更稳的做法通常会显式维护三份 schema:
user-facing schema给下游页面、工单、审批结论或 API 消费。tool-input schema约束模型调用工具时能提交哪些参数。tool-output schema约束工具结果进入下一轮推理前该长成什么样。
这样你后面在演进时才更容易回答:
- 是展示层要变
- 还是工具契约要变
- 还是结果清洗层要变
2.3 函数调用
函数调用并不是“让模型自己执行代码”,而是让模型在受控边界内提出一个工具调用建议:
- 你向模型暴露工具定义和参数 schema。
- 模型决定是否调用工具以及填写参数。
- 你的应用在外部执行工具。
- 工具结果再回传给模型继续推理或生成结果。
函数调用真正解决的是:
- 模型如何受控地接入外部能力
所以这三者的边界可以这样记:
- JSON mode:输出至少像 JSON
- Structured Outputs:输出必须长成你规定的结构
- Function calling:模型不只是说话,还能受控地请求外部能力
3. 什么时候该用结构化输出,什么时候该用函数调用
3.1 更适合结构化输出的场景
- 结果只是“产出数据”
- 不需要外部副作用
- 只想稳定得到字段化结论
例如:
- 意图分类
- 摘要字段抽取
- 风险标签判定
- 审核结论输出
- 中间状态封装
3.2 更适合函数调用的场景
- 需要外部系统能力
- 需要查询真实数据
- 需要写入真实系统
- 需要多步工具协同
例如:
- 查订单
- 搜知识库
- 创建工单
- 调数据库
- 发消息
- 发起审批
3.3 两者经常一起使用
在真实系统里,这两个能力经常组合出现:
- 先用结构化输出做分类、意图识别或路由判断
- 再用函数调用执行工具
- 最后再用结构化输出产出稳定结果
这比“所有环节都自由文本”更可控,也更适合评测和审计。
4. 先定义 schema,再写 prompt
很多团队会先写一大段 prompt,然后再想“最后能不能顺便给个 JSON”。这个顺序经常会反。
更稳妥的顺序通常是:
- 先定义这个节点到底要输出什么结构。
- 再定义哪些字段必填、哪些字段可选。
- 再定义字段类型、枚举和边界。
- 最后再写 prompt,告诉模型该如何填这份结构。
结构化输出真正的起点不是 prompt,而是 schema。
一个实用的 schema 至少应明确:
- 必填字段
- 字段类型
- 枚举范围
- 长度和数值边界
- 是否允许额外字段
示例:
json
{
"type": "object",
"properties": {
"intent": {
"type": "string",
"enum": ["refund", "policy", "billing", "other"]
},
"confidence": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"needs_human_review": {
"type": "boolean"
}
},
"required": ["intent", "confidence", "needs_human_review"],
"additionalProperties": false
}如果没有这些约束,模型就容易:
- 发明字段
- 把解释性文本塞进参数里
- 输出看起来“差不多对”但程序无法稳定消费的结构
4.1 schema 版本演进最好定义兼容性等级
很多团队一开始只有一个 schema 文件,后面随着需求增长不断改字段,最后最容易出的问题是:
- 老链路还能不能消费新结果
- 旧模型会不会继续生成过期字段
- 下游存储和评测脚本会不会一起坏
更稳的做法通常是给 schema 变更标一个兼容性等级:
backward-compatible例如新增可选字段、放宽说明文字。forward-compatible risky例如新增枚举、改变默认行为但不删字段。breaking change例如删字段、改字段类型、改核心枚举语义。
这样结构化输出和函数调用的发布流程才能决定:
- 是直接灰度
- 还是必须双写、双读或保留别名期
4.2 breaking schema 最好准备 alias 字段和迁移窗口
很多团队第一次做 breaking schema 时,最容易直接:
- 改字段名
- 改枚举值
- 改嵌套结构
这样上线后最常见的后果是:
- 老回放样本跑不通
- 老工具结果无法复用
- 下游解析脚本一起坏
更稳的做法通常是为 breaking change 准备一个短期迁移窗口:
- 保留 alias 字段
- 保留旧枚举映射
- 在 trace 和评测里同时记录新旧字段
这样你才能平滑判断:
- 新 schema 是否真的稳定
- 哪些旧链路还没迁完
5. schema 合法,不等于业务允许
这是很重要的一层区分。
即使参数完全符合 JSON schema,也不代表这次动作就应该执行。因为 schema 只能回答:
- 参数结构是不是合法
但它回答不了:
- 这个人有没有权限
- 这个动作是否超额
- 这个请求是否需要审批
- 当前状态下是否允许执行
所以成熟的系统通常至少分两层校验:
5.1 结构层校验
回答:
- 这份输入能不能被解析
5.2 业务与策略层校验
回答:
- 这次动作该不该执行
也就是说:
- schema 解决的是“能不能读”
- policy 解决的是“能不能做”
5.3 高风险函数最好拆成“建议参数”和“最终执行参数”两阶段
很多系统会直接让模型给出最终执行参数,然后交给后端判断能不能做。
更稳的做法通常是拆成两层:
proposed_action模型先给出建议动作和候选参数。approved_execution通过权限、审批、额度、幂等和对象状态校验后,才生成最终执行参数。
这对高风险场景很重要,因为它能明显降低:
- 模型一次性越过人审边界
- 执行参数和审批参数不一致
- 后续补偿时说不清“到底批准了什么”
6. 工具定义不是后端接口照搬
很多团队把现有后端 API 原样暴露给模型,最后常见问题是:
- 工具太大
- 参数太多
- 名字太抽象
- 模型很难选对
更适合给模型使用的工具,通常应该满足这些原则:
6.1 单一职责
不要把多个动作塞进一个“万能工具”里。
不推荐:
manage_customerprocess_caseoperate_order
更推荐:
lookup_orderdraft_refund_recommendationcreate_refund_approvalissue_refund
6.2 命名体现动作和副作用
工具名最好一眼能看出:
- 它做什么
- 是读还是写
- 是否可能产生真实副作用
6.3 参数对模型友好
字段名要清晰、含义要稳定,尽量避免:
- 模糊缩写
- 一参多义
- 让模型猜业务语义
6.4 输出对下游友好
工具返回值不应只是大段自由文本,而应能直接支持下一个判断节点。
6.5 工具 schema 最好带版本号、稳定别名和弃用策略
OpenAI 当前 Function calling 和 Migrate to the Responses API 的思路,都在提醒一个现实:
- 工具契约本身会演进
更成熟的做法通常是给工具定义保留这些元信息:
tool_nametool_versionstable_aliasdeprecated_afterreplacement_tool
这样你在升级 schema 或改参数时,才能更平滑地处理:
- 老模型还在引用旧函数名
- 某些工作流还没来得及切新参数
- 回放和评测仍然需要旧契约重现
6.6 读工具和写工具最好分不同暴露层
很多系统把所有工具一次性暴露给模型,结果是:
- 读工具、写工具、审批工具混在一起
这会明显放大误调用风险。
更稳的做法通常是按阶段或风险层级拆开暴露:
read-only tool set允许查数据、搜知识、取状态。draft / recommendation tool set允许生成建议、草稿、候选动作。approved write tool set只在权限、审批和上下文满足后开放。
这样模型在大多数时间根本看不到高风险执行工具,错误空间会小很多。
7. 函数调用链路到底怎么理解
一个典型的函数调用链路可以抽象成:
text
User Request
-> Model chooses a tool
-> App validates tool name and args
-> App executes the tool
-> Tool result is cleaned and summarized
-> Model continues reasoning or returns final answer这里最容易被忽视的是中间两步:
- 参数验证
- 工具结果清洗
7.1 工具结果清洗最好分成原始层、白名单层和模型消费层
很多团队知道“不要把原始结果整包塞回模型”,但没有把清洗流程结构化。
更稳的做法通常是拆三层:
raw result原始后端返回,便于审计和排障。sanitized result经过脱敏、白名单字段过滤、异常值规整后的结果。model-facing summary只保留模型下一步推理真正需要的字段和摘要。
这样你能同时满足:
- 可追溯
- 可脱敏
- 可控 token 成本
- 可稳定推理
7.2 tool result 清洗层最好有固定的 rejection code
很多工具结果虽然不该继续传播,但如果只返回一段自由文本说明,后续工作流还是很难稳定消费。
更适合生产的做法通常会给清洗层返回结构化拒绝码,例如:
RESULT_EMPTYRESULT_NOT_AUTHORIZEDRESULT_PARTIALRESULT_SCHEMA_MISMATCHRESULT_SENSITIVE_REDACTED
这样后面的模型、工作流和人工接管逻辑就能更稳定地区分:
- 是继续推理
- 进入补查
- 直接升级人工
很多线上问题恰恰不是工具本身失败,而是:
- 模型给了勉强可解析但业务不合法的参数
- 工具返回太脏,污染后续推理
8. 工具结果不一定要原样回灌给模型
很多系统会把工具的原始返回结果整包喂回模型,这经常带来三个问题:
- 噪音太大
- token 成本太高
- 敏感数据泄漏风险变大
更稳的做法通常是:
- 先清洗
- 先脱敏
- 只保留必要字段
- 对大对象做摘要
例如查询客户资料后,不一定要把整份 CRM 原始 JSON 回传给模型,而可以只返回:
- 客户状态
- 关键标签
- 最近工单摘要
- 是否命中高风险规则
这既省 token,也更不容易被工具噪音带偏。
8.1 工具结果回灌前最好先做“拒绝传播”判断
有些工具结果不是“脏”,而是根本不该继续传播给模型或下游节点。
例如:
- 明显越权查询结果
- 空对象但状态异常
- 后端错误页被包装成 200
- 含高敏字段但当前角色不该看
更稳的做法通常是在清洗前先判断:
- 这份结果能不能进入下一轮推理
- 只能进入审计
- 还是必须直接中断并升级人工
这一步能有效避免:
- 后端脏结果把模型带偏
- 敏感字段在模型上下文里继续扩散
9. 高风险工具必须显式暴露风险
函数调用一旦接入真实系统,风险就会从“说错话”升级为“做错事”。
高风险工具通常包括:
- 删除
- 转账
- 发消息
- 改权限
- 发起退款
- 修改生产配置
这类工具在定义层就应明确表达:
- 它是写工具
- 它可能不可逆
- 它是否需要审批
- 它失败后能否补偿或回滚
不要把高风险动作包装成看起来无害的名字,例如:
sync_customer
如果它实际会:
- 发信
- 改状态
- 扣费
那就应该拆开,让副作用可见。
9.1 高风险工具最好默认两阶段调用:draft_* 和 execute_*
高风险动作非常适合从工具命名层就体现两阶段边界,例如:
draft_refund_recommendationcreate_refund_approvalexecute_refund
这种拆法的价值不只是好看,而是能把:
- 生成建议
- 创建审批
- 真正执行
明确拆成不同控制点,方便:
- 权限矩阵设计
- trace 审计
- 回滚和补偿
- 高风险动作评测
9.2 execute 工具最好要求显式确认字段,而不是只靠函数名
很多团队以为只要工具名带了 execute_,风险边界就足够清楚。
但真实生产里更稳的做法通常还会要求执行参数里显式带上:
approval_idconfirmed_intentidempotency_keyobject_snapshot_id
这样你后面才能判断:
- 执行是不是基于已批准对象
- 是否真的是同一次动作
- 是否发生了对象漂移
10. 幂等性和补偿是写工具的底线
只要工具会产生外部副作用,就要思考:
- 这次动作能不能被安全重放
建议至少为这些动作设计幂等键:
- 创建工单
- 发通知
- 提交退款
- 创建审批单
- 更新外部记录
否则一旦出现这种情况:
- 工具执行成功,但本地状态没及时写回
后续系统就很难判断:
- 该不该重试
- 重试会不会造成重复写入
这类问题最终会从“工具设计问题”演变成“业务事故”。
10.1 并发、重试和超时边界最好提前写进工具契约
很多团队会讨论幂等键,但忽略另一个同样关键的问题:
- 这类工具能不能并发调用
- 超时后是否允许自动重试
- 重试前是否必须重新查状态
更稳的工具契约通常还会明确:
concurrency_policyretry_policytimeout_policystate_recheck_required
这样工作流引擎和模型编排层才不会对写工具做出过度乐观的假设。
10.2 工具最好定义“可安全自动重试”和“必须人工确认重试”两类
不是所有失败都适合自动重试。
更稳的分类通常是:
safe retry读查询、幂等草稿、无副作用状态检查。manual retry required退款、发信、改权限、提交审批、写 CRM。
如果这层不分开,模型或工作流引擎就可能在最不该重试的地方自动补发第二次动作。
11. 审批和人机协同要前置到工具设计里
高风险工具不应该等到工作流最后才想起要审批。
更成熟的做法是从工具契约开始就表达清楚:
- 是否需要审批
- 谁来审批
- 什么条件下必须升级人工
- 审批拒绝后如何收束
因此函数调用设计天然会和这些专题联动:
11.1 审批型函数最好把“审批对象快照”单独留存
很多团队会保存审批结论,但不保存当时真正被批准的参数快照。
这会带来一个很实际的问题:
- 审批通过的是 A
- 执行时却跑成了 B
更稳的做法通常是在审批型函数链路里显式保存:
- draft 参数
- 审批对象快照
- 审批结论
- 最终执行参数
这样你后面才能判断:
- 是审批错了
- 还是执行前对象漂移了
11.2 审批链最好消费结构化理由,而不是只看通过 / 拒绝
高风险函数如果最终要接审批,人审阶段最有价值的往往不是一个布尔结果,而是:
- 为什么建议执行
- 为什么建议拒绝
- 哪些字段最关键
- 哪些风险被接受了
所以更成熟的设计通常会在草稿阶段就输出:
decision_rationalerisk_flagsrequired_review_scope
这样审批人和后续审计看到的就不只是:
- “批准了”
而是一个可解释、可回放的结构化判断。
12. Structured Outputs 和函数调用怎么评测
12.1 结构化输出评测
建议至少看:
- schema 命中率
- 必填字段完整率
- 枚举值正确率
- 数值范围合法率
- 额外字段违规率
12.2 工具调用评测
建议至少看:
- 工具选择正确率
- 参数正确率
- 非必要工具调用率
- 高风险动作误触发率
- 审批触发率
- 审批绕过率
12.3 过程级评测
还应该看:
- 工具结果是否被正确消费
- 空结果和异常结果是否被正确处理
- 超时和重试是否可控
- 多工具循环是否进入低价值回合
如果只评最终答案,很容易漏掉“工具路径错了但最后看起来还能说通”的问题。
12.4 schema 兼容和工具版本切换也要单独评测
很多团队会测:
- 工具选得对不对
- 参数填得对不对
但不会专门测:
- 新 schema 是否打坏旧链路
- 新工具版本和旧别名是否还能共存
- 老回放样本是否还能被新工作流解释
更成熟的评测通常还会补两类:
compatibility eval验证 schema 演进前后是否还兼容。migration eval验证工具别名、版本切换、Responses API 迁移后链路是否稳定。
12.5 迁移评测最好覆盖“老样本 + 新样本 + 高风险样本”三层
很多团队做迁移评测时,只会挑最新的一批 happy path 样本。
这通常不够,因为真正会暴露问题的经常是:
- 老 schema 时代留下的历史样本
- 新功能刚引入的新样本
- 高风险写工具和审批链样本
更稳的迁移评测通常至少要覆盖:
legacy setcurrent sethigh-risk set
这样你才更容易发现:
- 向后兼容哪里坏了
- 新能力哪里没跟上
- 风险边界哪里松了
13. 常见失败模式
13.1 schema 过于复杂
schema 太深、分支太多,模型更容易偏离。
13.2 字段语义模糊
字段含义不清时,模型迟早会误填。
13.3 工具描述过长
描述过长会抬高 token 成本,也会把关键信号淹没在噪音里。
13.4 工具输出过脏
后续节点会越来越不稳定。
13.5 只测 happy path
空结果、部分成功、重试、超时、审批拒绝,这些场景才最像真实生产。
13.6 让模型同时看到太多可写工具
这也是一个非常常见的失败模式:
- 工具很多
- 读写混杂
- 说明词又很像
最后模型即使“选对了大类”,也很容易:
- 提前走执行函数
- 跳过草稿或审批函数
- 在两个相近写工具之间来回误选
更稳的做法通常是:
- 把同一阶段只暴露该阶段需要的最小工具集
- 通过路由或工作流节点逐步解锁高风险工具
13.7 用自然语言补 schema 漏洞,而不是回头修契约
这也是很常见的反模式:
- 字段本身定义不清
- 工具参数边界不够严
- 结果 schema 太松
但团队不去修 schema 或工具契约,而是往 prompt 里继续堆说明文字。
短期看似能缓解,长期通常会导致:
- prompt 越来越长
- 模型越来越难稳定遵守
- 同类问题反复出现
更稳的处理顺序通常是:
- 先修 schema / tool contract
- 再修 prompt
- 再补评测
14. 一个客服 Agent 的工具设计示例
假设系统需要支持:
- 查询订单
- 查询政策
- 生成退款建议
- 发起退款
- 给客户发邮件
更合理的拆法通常是:
lookup_ordersearch_refund_policydraft_refund_recommendationcreate_refund_approvalissue_refunddraft_customer_emailsend_customer_email
这里最关键的不是“工具数量”,而是边界清晰:
- 建议和执行分开
- 草稿和外发分开
- 审批创建和真实退款分开
这样模型就不容易跨越本不该跨越的边界。
15. 推荐搭配阅读
16. 重点官方资源
以下入口已按 2026-07-09 的 OpenAI 官方资料重新整理:
- OpenAI Structured outputs guide:https://developers.openai.com/api/docs/guides/structured-outputs
- OpenAI Function calling guide:https://developers.openai.com/api/docs/guides/function-calling
- OpenAI Tools guide:https://developers.openai.com/api/docs/guides/tools
- OpenAI Responses API migration guide:https://developers.openai.com/api/docs/guides/migrate-to-responses
- OpenAI Guardrails and human review:https://developers.openai.com/api/docs/guides/agents/guardrails-approvals
- OpenAI MCP and Connectors:https://developers.openai.com/api/docs/guides/tools-connectors-mcp
- OpenAI Integrations and observability:https://developers.openai.com/api/docs/guides/agents/integrations-observability
- OpenAI Using tools:https://developers.openai.com/api/docs/guides/tools
17. 落地检查清单
- 是否区分了面向用户的结构 schema 和面向工具的参数 schema
- 是否给 schema 变更定义了兼容性等级、版本号和迁移策略
- 是否把工具结果拆成 raw / sanitized / model-facing 三层
- 是否为高风险函数设计了 draft / approve / execute 分层
- 是否为并发、重试、超时和幂等写清了工具契约