API 返回 tool_calls 后怎么办?执行函数与回传结果的完整循环
API 返回自定义函数的 tool_calls,通常表示模型正在请求应用执行工具,还没有完成整个用户任务。应用应先校验工具名与参数,执行允许的函数,再携带对应调用 ID 回传结果,让模型继续生成答案或提出下一轮调用。本文解释非流式执行循环;文档核对日为 2026 年 10 月 24 日,配套代码只用合成响应做本地演练,没有发起真实 API 请求。
工具调用为什么需要第二次模型请求?
声明 tools 是告诉模型有哪些函数可选,并没有把函数实现交给模型。对于应用自己定义的函数,模型提出调用意图,应用负责执行,随后再把执行结果送回模型。服务端内置搜索等工具可能由供应商执行,不属于本文这个本地执行器。OpenAI Function calling
可以把一次闭环拆成五步:声明工具及参数结构;收到调用;校验并执行;回传结果;检查下一轮输出。如果仍有调用,就在应用设定的轮数、时间与费用边界内继续;若得到最终文本,还要检查任务是否完成。把第一轮 content 为空直接判为模型故障,可能只是漏掉了工具分支。
执行前至少检查三个条件:函数名在明确白名单内,JSON 能解析且字段类型正确,当前用户有权进行该操作。不要把模型返回的名字交给 eval、动态导入或任意命令执行;合法 JSON 也不代表它有权查另一个用户的订单。官方接口文档同样要求应用校验函数参数。参数字段说明
Chat Completions:保留 assistant,再补 tool 结果
在 Chat Completions 中,从 assistant message 的 tool_calls 数组读取每次调用。下面是人为构造的两条历史消息,省略了最初的用户消息;不是一次真实 API 返回,也不是完整请求体:
[
{
"role": "assistant",
"content": null,
"tool_calls": [{
"id": "call_demo",
"type": "function",
"function": {
"name": "add_integers",
"arguments": "{\"a\":2,\"b\":3}"
}
}]
},
{
"role": "tool",
"tool_call_id": "call_demo",
"content": "{\"ok\":true,\"sum\":5}"
}
]
第二条消息由应用生成:tool_call_id 对应第一条调用的 id,content 是实际执行结果的字符串。将完整 assistant 消息和每个工具的结果追加到原 messages 后,再请求模型;只发数字 5,或只保留 assistant 的文本,都会丢失必要关联。Chat Completions 消息字段
一次响应可能有多个调用,不能只处理 tool_calls[0]。有 N 个待处理调用,就逐一核对 N 个结果;函数执行失败也要有与原 ID 对应的错误结果,或者明确终止本轮。不要悄悄漏掉失败项后继续发送一个不完整的历史。
Responses:用 call_id,不要拿 item id 顶替
Responses 把函数调用放在 output 中,调用条目的类型为 function_call;应用回传的条目则是 function_call_output。下面仍是合成示意,两个不同的 ID 特意保留,便于看清配对:
[
{
"type": "function_call",
"id": "fc_demo",
"call_id": "call_demo",
"name": "add_integers",
"arguments": "{\"a\":2,\"b\":3}"
},
{
"type": "function_call_output",
"call_id": "call_demo",
"output": "{\"ok\":true,\"sum\":5}"
}
]
手动维护历史时,将上一轮必要的 output items 与工具结果加入下一轮 input,不能只截取文本。推理模型返回的 reasoning items 也需要按接口要求保留。OpenAI 提供状态衔接机制,但兼容服务不一定支持同一套参数:例如 DeepSeek 当前 Responses 文档明确说明它是无状态接口,不支持 previous_response_id。OpenAI 调用流程、DeepSeek 兼容表
| 项目 | Chat Completions | Responses |
|---|---|---|
| 收到的调用 | message.tool_calls[] |
output[] 中的 function_call |
| 结果关联 | tool_call_id 匹配调用 id |
call_id 匹配调用 call_id |
| 结果容器 | role: tool 的 message |
type: function_call_output 的 item |
| 下一轮主要输入 | messages |
input,按服务商处理状态 |
不出最终答案时,先检查哪一段?
| 现象 | 先核对 | 处理方向 |
|---|---|---|
| 有调用但工具没运行 | 白名单、参数解析、执行器是否接入 | 记录未执行原因,不伪造成功结果 |
报 tool_call_id 缺结果 |
调用列表、结果列表及完整历史 | 逐个 ID 配对,查漏项与串号 |
| 多个调用只回了一个结果 | 是否只读取数组第一项 | 收齐本轮结果后再继续 |
| 工具已执行,回传却超时 | 原执行状态、请求记录 | 先查结果,避免重复业务操作 |
| 多轮一直请求相同工具 | 参数、错误结果及停止预算 | 到上限就停,保留未完成状态 |
| 没有调用也没有有效文本 | 拒绝、截断、响应未完成等状态 | 不把空响应当验收成功 |
若工具会发送消息、写数据库或付款,重试前需要业务幂等与执行记录。调用 ID 用来匹配协议消息,不能独自保证跨请求、跨进程的业务只执行一次。超时表示客户端没及时收到结果,不等于服务端一定没做。
流式场景还多一道组装步骤:参数可能分多段到达,不要对第一段 JSON 直接执行。本文资源没有实现增量拼接;应按实际协议收齐相应调用参数及完成状态,再走同样的校验路径。官方流式工具调用说明
用离线演练检查自己的理解
下载工具调用离线演练与验收模板。解压后进入目录,运行:
python3 tool_call_lab.py
python3 -m unittest -v test_tool_call_lab.py
资源无需 Key 或第三方包,只允许执行固定的整数加法。它回放两轮人为编写的调用,再显示人为编写的最终文本;第一轮包含两个调用,便于检查漏回问题。最终文本不是模型根据结果生成的,不能拿来证明某个 API 已连通。
测试会检查未知工具、非法 JSON、参数类型、重复键、ID 错配和停止条件;代码字符串只当数据处理。默认最多三轮、每轮四个调用,是演练选择,不是供应商限制。将逻辑接入真实程序时,还需要另外验证工具声明、请求适配、认证授权、超时、持久化幂等与账单。
包内 acceptance-record.md 可另存填写。每次至少保留协议与版本、请求 ID、调用 ID、工具名、脱敏参数校验、执行状态、返回结果、回传 ID、最终文本验收和各轮 usage。缺失数据写“待核”;离线演练没有真实 token 消耗或成功率。
常见问题
模型返回 tool_calls,函数已经执行了吗?
对于本文的自定义本地函数,没有。调用内容只是执行请求,具体实现由应用提供;必须检查执行器日志和真实结果。DeepSeek 的官方示例也明确指出,模型本身不执行用户提供的具体函数。DeepSeek Tool Calls
开启 strict,就不用检查参数了吗?
仍然要检查业务范围、权限和执行条件。结构约束解决的是参数格式问题,不负责批准操作,也不保证输入数据真实。目标服务是否支持 strict、支持哪些 schema,需要查该接口文档;本地演练不会替你证明兼容性。
多个调用一定要并行执行吗?
不一定。先判断它们是否相互独立、工具是否允许并发;有依赖就按业务顺序处理。无论采用哪种调度方式,都要按原调用 ID 配对,不能把“第二个完成的结果”简单当成“第二个调用的结果”。
换成云岚API,只改 base URL 就能用吗?
不能据此保证。需要分别确认模型、渠道、协议、工具参数和结果回传能力,再测试完整循环。先看 base URL 与协议核对,已有请求错误看报错排查;本篇没有验证云岚API或任一第三方的全部工具功能。
接收流式数据时,先组装事件
本文演练使用完整的非流式响应。如果程序直接读取网络流,先用 SSE 分帧与完成状态检查区分传输字节、完整事件和业务结果。新页的演练只处理限定文本流,不实现工具参数增量拼接;完整工具参数仍须按相应协议收齐后才可校验、执行。
在自动化工作流里调用接口,需要再核对什么?
若用 n8n 组织输入和处理响应,继续看 HTTP Request 节点配置、取值与工作流验收。HTTP Request 负责这次 HTTP 请求,不会因为响应出现工具名称就自动完成本文的工具执行与回传循环;输入条目、凭据和后续节点仍需逐项核对。
