Skip to content

结构化输出与函数调用专题

版本: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:

  1. user-facing schema 给下游页面、工单、审批结论或 API 消费。
  2. tool-input schema 约束模型调用工具时能提交哪些参数。
  3. tool-output schema 约束工具结果进入下一轮推理前该长成什么样。

这样你后面在演进时才更容易回答:

  • 是展示层要变
  • 还是工具契约要变
  • 还是结果清洗层要变

2.3 函数调用

函数调用并不是“让模型自己执行代码”,而是让模型在受控边界内提出一个工具调用建议:

  1. 你向模型暴露工具定义和参数 schema。
  2. 模型决定是否调用工具以及填写参数。
  3. 你的应用在外部执行工具。
  4. 工具结果再回传给模型继续推理或生成结果。

函数调用真正解决的是:

  • 模型如何受控地接入外部能力

所以这三者的边界可以这样记:

  • JSON mode:输出至少像 JSON
  • Structured Outputs:输出必须长成你规定的结构
  • Function calling:模型不只是说话,还能受控地请求外部能力

3. 什么时候该用结构化输出,什么时候该用函数调用

3.1 更适合结构化输出的场景

  • 结果只是“产出数据”
  • 不需要外部副作用
  • 只想稳定得到字段化结论

例如:

  • 意图分类
  • 摘要字段抽取
  • 风险标签判定
  • 审核结论输出
  • 中间状态封装

3.2 更适合函数调用的场景

  • 需要外部系统能力
  • 需要查询真实数据
  • 需要写入真实系统
  • 需要多步工具协同

例如:

  • 查订单
  • 搜知识库
  • 创建工单
  • 调数据库
  • 发消息
  • 发起审批

3.3 两者经常一起使用

在真实系统里,这两个能力经常组合出现:

  • 先用结构化输出做分类、意图识别或路由判断
  • 再用函数调用执行工具
  • 最后再用结构化输出产出稳定结果

这比“所有环节都自由文本”更可控,也更适合评测和审计。

4. 先定义 schema,再写 prompt

很多团队会先写一大段 prompt,然后再想“最后能不能顺便给个 JSON”。这个顺序经常会反。

更稳妥的顺序通常是:

  1. 先定义这个节点到底要输出什么结构。
  2. 再定义哪些字段必填、哪些字段可选。
  3. 再定义字段类型、枚举和边界。
  4. 最后再写 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 变更标一个兼容性等级:

  1. backward-compatible 例如新增可选字段、放宽说明文字。
  2. forward-compatible risky 例如新增枚举、改变默认行为但不删字段。
  3. 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 高风险函数最好拆成“建议参数”和“最终执行参数”两阶段

很多系统会直接让模型给出最终执行参数,然后交给后端判断能不能做。

更稳的做法通常是拆成两层:

  1. proposed_action 模型先给出建议动作和候选参数。
  2. approved_execution 通过权限、审批、额度、幂等和对象状态校验后,才生成最终执行参数。

这对高风险场景很重要,因为它能明显降低:

  • 模型一次性越过人审边界
  • 执行参数和审批参数不一致
  • 后续补偿时说不清“到底批准了什么”

6. 工具定义不是后端接口照搬

很多团队把现有后端 API 原样暴露给模型,最后常见问题是:

  • 工具太大
  • 参数太多
  • 名字太抽象
  • 模型很难选对

更适合给模型使用的工具,通常应该满足这些原则:

6.1 单一职责

不要把多个动作塞进一个“万能工具”里。

不推荐:

  • manage_customer
  • process_case
  • operate_order

更推荐:

  • lookup_order
  • draft_refund_recommendation
  • create_refund_approval
  • issue_refund

6.2 命名体现动作和副作用

工具名最好一眼能看出:

  • 它做什么
  • 是读还是写
  • 是否可能产生真实副作用

6.3 参数对模型友好

字段名要清晰、含义要稳定,尽量避免:

  • 模糊缩写
  • 一参多义
  • 让模型猜业务语义

6.4 输出对下游友好

工具返回值不应只是大段自由文本,而应能直接支持下一个判断节点。

6.5 工具 schema 最好带版本号、稳定别名和弃用策略

OpenAI 当前 Function callingMigrate to the Responses API 的思路,都在提醒一个现实:

  • 工具契约本身会演进

更成熟的做法通常是给工具定义保留这些元信息:

  • tool_name
  • tool_version
  • stable_alias
  • deprecated_after
  • replacement_tool

这样你在升级 schema 或改参数时,才能更平滑地处理:

  • 老模型还在引用旧函数名
  • 某些工作流还没来得及切新参数
  • 回放和评测仍然需要旧契约重现

6.6 读工具和写工具最好分不同暴露层

很多系统把所有工具一次性暴露给模型,结果是:

  • 读工具、写工具、审批工具混在一起

这会明显放大误调用风险。

更稳的做法通常是按阶段或风险层级拆开暴露:

  1. read-only tool set 允许查数据、搜知识、取状态。
  2. draft / recommendation tool set 允许生成建议、草稿、候选动作。
  3. 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 工具结果清洗最好分成原始层、白名单层和模型消费层

很多团队知道“不要把原始结果整包塞回模型”,但没有把清洗流程结构化。

更稳的做法通常是拆三层:

  1. raw result 原始后端返回,便于审计和排障。
  2. sanitized result 经过脱敏、白名单字段过滤、异常值规整后的结果。
  3. model-facing summary 只保留模型下一步推理真正需要的字段和摘要。

这样你能同时满足:

  • 可追溯
  • 可脱敏
  • 可控 token 成本
  • 可稳定推理

7.2 tool result 清洗层最好有固定的 rejection code

很多工具结果虽然不该继续传播,但如果只返回一段自由文本说明,后续工作流还是很难稳定消费。

更适合生产的做法通常会给清洗层返回结构化拒绝码,例如:

  • RESULT_EMPTY
  • RESULT_NOT_AUTHORIZED
  • RESULT_PARTIAL
  • RESULT_SCHEMA_MISMATCH
  • RESULT_SENSITIVE_REDACTED

这样后面的模型、工作流和人工接管逻辑就能更稳定地区分:

  • 是继续推理
  • 进入补查
  • 直接升级人工

很多线上问题恰恰不是工具本身失败,而是:

  • 模型给了勉强可解析但业务不合法的参数
  • 工具返回太脏,污染后续推理

8. 工具结果不一定要原样回灌给模型

很多系统会把工具的原始返回结果整包喂回模型,这经常带来三个问题:

  • 噪音太大
  • token 成本太高
  • 敏感数据泄漏风险变大

更稳的做法通常是:

  • 先清洗
  • 先脱敏
  • 只保留必要字段
  • 对大对象做摘要

例如查询客户资料后,不一定要把整份 CRM 原始 JSON 回传给模型,而可以只返回:

  • 客户状态
  • 关键标签
  • 最近工单摘要
  • 是否命中高风险规则

这既省 token,也更不容易被工具噪音带偏。

8.1 工具结果回灌前最好先做“拒绝传播”判断

有些工具结果不是“脏”,而是根本不该继续传播给模型或下游节点。

例如:

  • 明显越权查询结果
  • 空对象但状态异常
  • 后端错误页被包装成 200
  • 含高敏字段但当前角色不该看

更稳的做法通常是在清洗前先判断:

  • 这份结果能不能进入下一轮推理
  • 只能进入审计
  • 还是必须直接中断并升级人工

这一步能有效避免:

  • 后端脏结果把模型带偏
  • 敏感字段在模型上下文里继续扩散

9. 高风险工具必须显式暴露风险

函数调用一旦接入真实系统,风险就会从“说错话”升级为“做错事”。

高风险工具通常包括:

  • 删除
  • 转账
  • 发消息
  • 改权限
  • 发起退款
  • 修改生产配置

这类工具在定义层就应明确表达:

  • 它是写工具
  • 它可能不可逆
  • 它是否需要审批
  • 它失败后能否补偿或回滚

不要把高风险动作包装成看起来无害的名字,例如:

  • sync_customer

如果它实际会:

  • 发信
  • 改状态
  • 扣费

那就应该拆开,让副作用可见。

9.1 高风险工具最好默认两阶段调用:draft_*execute_*

高风险动作非常适合从工具命名层就体现两阶段边界,例如:

  • draft_refund_recommendation
  • create_refund_approval
  • execute_refund

这种拆法的价值不只是好看,而是能把:

  • 生成建议
  • 创建审批
  • 真正执行

明确拆成不同控制点,方便:

  • 权限矩阵设计
  • trace 审计
  • 回滚和补偿
  • 高风险动作评测

9.2 execute 工具最好要求显式确认字段,而不是只靠函数名

很多团队以为只要工具名带了 execute_,风险边界就足够清楚。

但真实生产里更稳的做法通常还会要求执行参数里显式带上:

  • approval_id
  • confirmed_intent
  • idempotency_key
  • object_snapshot_id

这样你后面才能判断:

  • 执行是不是基于已批准对象
  • 是否真的是同一次动作
  • 是否发生了对象漂移

10. 幂等性和补偿是写工具的底线

只要工具会产生外部副作用,就要思考:

  • 这次动作能不能被安全重放

建议至少为这些动作设计幂等键:

  • 创建工单
  • 发通知
  • 提交退款
  • 创建审批单
  • 更新外部记录

否则一旦出现这种情况:

  • 工具执行成功,但本地状态没及时写回

后续系统就很难判断:

  • 该不该重试
  • 重试会不会造成重复写入

这类问题最终会从“工具设计问题”演变成“业务事故”。

10.1 并发、重试和超时边界最好提前写进工具契约

很多团队会讨论幂等键,但忽略另一个同样关键的问题:

  • 这类工具能不能并发调用
  • 超时后是否允许自动重试
  • 重试前是否必须重新查状态

更稳的工具契约通常还会明确:

  • concurrency_policy
  • retry_policy
  • timeout_policy
  • state_recheck_required

这样工作流引擎和模型编排层才不会对写工具做出过度乐观的假设。

10.2 工具最好定义“可安全自动重试”和“必须人工确认重试”两类

不是所有失败都适合自动重试。

更稳的分类通常是:

  1. safe retry 读查询、幂等草稿、无副作用状态检查。
  2. manual retry required 退款、发信、改权限、提交审批、写 CRM。

如果这层不分开,模型或工作流引擎就可能在最不该重试的地方自动补发第二次动作。

11. 审批和人机协同要前置到工具设计里

高风险工具不应该等到工作流最后才想起要审批。

更成熟的做法是从工具契约开始就表达清楚:

  • 是否需要审批
  • 谁来审批
  • 什么条件下必须升级人工
  • 审批拒绝后如何收束

因此函数调用设计天然会和这些专题联动:

11.1 审批型函数最好把“审批对象快照”单独留存

很多团队会保存审批结论,但不保存当时真正被批准的参数快照。

这会带来一个很实际的问题:

  • 审批通过的是 A
  • 执行时却跑成了 B

更稳的做法通常是在审批型函数链路里显式保存:

  • draft 参数
  • 审批对象快照
  • 审批结论
  • 最终执行参数

这样你后面才能判断:

  • 是审批错了
  • 还是执行前对象漂移了

11.2 审批链最好消费结构化理由,而不是只看通过 / 拒绝

高风险函数如果最终要接审批,人审阶段最有价值的往往不是一个布尔结果,而是:

  • 为什么建议执行
  • 为什么建议拒绝
  • 哪些字段最关键
  • 哪些风险被接受了

所以更成熟的设计通常会在草稿阶段就输出:

  • decision_rationale
  • risk_flags
  • required_review_scope

这样审批人和后续审计看到的就不只是:

  • “批准了”

而是一个可解释、可回放的结构化判断。

12. Structured Outputs 和函数调用怎么评测

12.1 结构化输出评测

建议至少看:

  • schema 命中率
  • 必填字段完整率
  • 枚举值正确率
  • 数值范围合法率
  • 额外字段违规率

12.2 工具调用评测

建议至少看:

  • 工具选择正确率
  • 参数正确率
  • 非必要工具调用率
  • 高风险动作误触发率
  • 审批触发率
  • 审批绕过率

12.3 过程级评测

还应该看:

  • 工具结果是否被正确消费
  • 空结果和异常结果是否被正确处理
  • 超时和重试是否可控
  • 多工具循环是否进入低价值回合

如果只评最终答案,很容易漏掉“工具路径错了但最后看起来还能说通”的问题。

12.4 schema 兼容和工具版本切换也要单独评测

很多团队会测:

  • 工具选得对不对
  • 参数填得对不对

但不会专门测:

  • 新 schema 是否打坏旧链路
  • 新工具版本和旧别名是否还能共存
  • 老回放样本是否还能被新工作流解释

更成熟的评测通常还会补两类:

  1. compatibility eval 验证 schema 演进前后是否还兼容。
  2. migration eval 验证工具别名、版本切换、Responses API 迁移后链路是否稳定。

12.5 迁移评测最好覆盖“老样本 + 新样本 + 高风险样本”三层

很多团队做迁移评测时,只会挑最新的一批 happy path 样本。

这通常不够,因为真正会暴露问题的经常是:

  • 老 schema 时代留下的历史样本
  • 新功能刚引入的新样本
  • 高风险写工具和审批链样本

更稳的迁移评测通常至少要覆盖:

  1. legacy set
  2. current set
  3. high-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 越来越长
  • 模型越来越难稳定遵守
  • 同类问题反复出现

更稳的处理顺序通常是:

  1. 先修 schema / tool contract
  2. 再修 prompt
  3. 再补评测

14. 一个客服 Agent 的工具设计示例

假设系统需要支持:

  • 查询订单
  • 查询政策
  • 生成退款建议
  • 发起退款
  • 给客户发邮件

更合理的拆法通常是:

  • lookup_order
  • search_refund_policy
  • draft_refund_recommendation
  • create_refund_approval
  • issue_refund
  • draft_customer_email
  • send_customer_email

这里最关键的不是“工具数量”,而是边界清晰:

  • 建议和执行分开
  • 草稿和外发分开
  • 审批创建和真实退款分开

这样模型就不容易跨越本不该跨越的边界。

15. 推荐搭配阅读

16. 重点官方资源

以下入口已按 2026-07-09 的 OpenAI 官方资料重新整理:

17. 落地检查清单

  • 是否区分了面向用户的结构 schema 和面向工具的参数 schema
  • 是否给 schema 变更定义了兼容性等级、版本号和迁移策略
  • 是否把工具结果拆成 raw / sanitized / model-facing 三层
  • 是否为高风险函数设计了 draft / approve / execute 分层
  • 是否为并发、重试、超时和幂等写清了工具契约