工具调用API的循环骨架只有四段:声明 tools → 发起请求 → 执行 tool_calls 并按 tool_call_id 回传 → 判断本轮是否还有 tool_calls 决定是否继续。差异集中在四个层:函数声明、触发控制、返回结构、递归终止。
| 差异层 | 常见故障现象 |
|---|---|
| 函数声明 | 换模型后参数缺字段、Schema 被拒或解析失败 |
| 触发控制 | 模型不返回 tool_calls,直接给文本 |
| 返回结构 | 并行调用时 tool_call_id 配错或漏配 |
| 递归终止 | 多步循环死循环或中途中断 |
本文规范描述基于 2026 年 8 月可查的官方文档状态。
先厘清概念:function calling 和工具调用是一回事吗
很多开发者被“function calling”“工具调用”“MCP tool”这几个词绕晕。简单说,OpenAI 早期的 functions 字段与现在的 tools / tool_calls 是同一件事的不同代际写法;而 MCP 规范 中的 tool 则是同一份 JSON Schema 在另一层协议(JSON-RPC)中的表达。
所以你完全可以把 MCP 里的 name、description、inputSchema 与 OpenAI 格式的 tools 定义直接映射,心智模型就是:声明格式可映射,调用协议不同。别混着调试,否则会浪费大量时间。
差异一:函数声明——JSON Schema 怎么写才在多家模型上都稳
在工具调用API里,每个函数由三部分构成:name、description、参数 JSON Schema。这和 MCP 的 name / description / inputSchema 结构如出一辙。
工程防御建议:
- 显式写
required:缺了可能导致参数缺失。 - 避免深层嵌套与超长枚举:部分模型解析困难。
description里写清何时调用:不只“做什么”,还要“什么条件下用”。
注意,具体支持范围以各厂商官方文档为准,没有统一死数字。
差异二:触发控制——auto、required、none 与指定函数分别什么时候用
tool_choice 有四种取值,语义各不相同(详见 OpenAI Function calling 官方文档):
| 取值 | 语义 | 典型场景 |
|---|---|---|
"auto" | 模型自行决定是否调用 | 探索式对话 |
"required" | 强制至少调用一个函数 | 必须产出结构化结果 |
"none" | 禁止调用,纯文本 | 兜底或简单问答 |
{type:"function", function:{name:"xxx"}} | 强制指定函数 | 路由到特定功能 |
所以“模型不返回 tool_calls 是什么原因”,排查顺序是:先查 tool_choice 是不是 none 或 auto 且模型判断不需要;再查 description 是否给出明确触发条件;最后才怀疑模型能力。
差异三:返回结构——并行 tool_calls 怎么拆包,id 与 tool 消息怎么配对
模型返回的是一个 tool_calls 列表,每个调用带唯一 id。你执行完业务后,必须为每一个 id 回传一条 role 为 tool、且带对应 tool_call_id 的消息。漏配、错配或合并回传,是多步循环最常见的 400 报错来源。
parallel_tool_calls 开关决定是否允许并行:并行省往返,串行好排障。处理时大致这样:
for tool_call in assistant_msg.tool_calls:
# 执行工具,得到 result
messages.append({'role': 'tool', 'tool_call_id': tool_call.id, 'content': result})注意:必须先 messages.append(assistant_msg),再回传 tool 消息。完整循环见下一节。
差异四:多步递归——终止条件、最大步数与中途报错的兜底
多步工具调用循环怎么写才不会死循环?以下骨架可独立运行:
import json
import openai
def get_weather(city):
return f"{city} 天气晴,25°C"
client = openai.OpenAI()
MODEL = "..."
messages = [{"role": "user", "content": "查询北京和上海天气,并总结"}]
max_steps = 5
TOOLS = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取指定城市的天气",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string"}
},
"required": ["city"]
}
}
}]
for step in range(max_steps):
resp = client.chat.completions.create(
model=MODEL,
messages=messages,
tools=TOOLS,
tool_choice="auto"
)
assistant_msg = resp.choices[0].message
if not assistant_msg.tool_calls:
break
messages.append(assistant_msg)
for tool_call in assistant_msg.tool_calls:
try:
args = json.loads(tool_call.function.arguments)
result = get_weather(**args)
except Exception as e:
result = f"错误: {e}"
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result
})
else:
# 超过最大步数,降级要求文本结论
messages.append({
"role": "user",
"content": "请直接给出文本结论,不要再调用工具。"
})
resp = client.chat.completions.create(model=MODEL, messages=messages)工具执行失败时,把错误信息作为 tool 消息内容回传,而不是抛出中断;超限后降级返回文本结论。这样循环既稳又不会卡死。
写一次跑多家:把四处差异收进一层薄适配器
工具调用API 的跨模型迁移之痛,可以用一层薄适配器解决:业务代码只依赖统一的工具注册表与结果结构。OpenAI 的 tools/tool_choice 结构与 MCP 的 tools/list、tools/call JSON-RPC 接口已成事实标准的两端,工程重点应从选协议转向补齐 Schema 与终止逻辑的防御。适配器负责四件事:
- Schema 降级:把深层嵌套拍平,补全 required
- tool_choice 语义归一:统一成 auto/required/none/指定函数
- tool_calls 归一化:统一转为列表结构
- 流式分片累积:流式模式下工具调用分片的合并
适配层最好留出可观测点,每步记录 model、步数、tool 名称与耗时,方便排障。跨模型迁移的延伸阅读可参考 OpenAI API迁移。

怎么验证适配层真的可移植:一份 schema、一段循环,只换 model 跑对照
最小可复现验证方法:固定一份 tool schema,同一段循环代码,只改 model 参数在 OpenAI 兼容端点上跑,此时可参考 OpenAI base_url 怎么改。记录是否触发、参数完整度、并行拆包形态与步数。
例如把 base_url 指向 NexAIX 的 OpenAI 兼容端点(https://api.nexaix.net/v1),同一套代码只改 model 就能横向跑通不同模型。特别留意返回体中的 model 字段,它对应实际执行模型——这决定了你能否把“没触发工具”归因到具体模型,而不是被中间层吞了字段。当前可用模型与规格以 NexAIX 模型页 为准。
换模型前必跑的工具调用回归清单
| 检查项 | 失败时定位方向 |
|---|---|
| 单函数触发 | Schema 与 description |
| 并行多函数 | tool_call_id 回传 |
| required 强制触发 | 模型是否支持 required |
| 嵌套参数解析 | Schema 降级 |
| 工具报错兜底 | try/except 与回传内容 |
| 超步数降级 | max_steps 与降级逻辑 |
| 流式分片消费 | 分片累积 |
| tool_call_id 配对 | 回传循环漏配 |
常见问题
换模型后 tool_calls 格式不一样怎么办?
先看返回体是否符合 OpenAI Chat Completions 规范。字段名有差异就在适配层做归一化。所有模型都以官方文档为准。
tool_choice required 和 auto 有什么区别?
required 强制模型至少调用一个函数,适合结构化输出;auto 让模型自主决定,适合开放对话。若 auto 下模型不触发,先检查 description。
并行工具调用结果怎么回传?
为每个 tool_call_id 单独追加一条 role: "tool" 消息,不要合并。然后追加助手消息,再发起下一次请求。
模型不返回 tool_calls 是哪些原因?
依次排查:tool_choice 是否设为 none 或 auto 且模型认为不需要;description 是否写清触发条件;最后才怀疑模型能力。有时是中间层吞了字段。
function calling 和工具调用是一回事吗?
基本是一回事,今天说的工具调用API 就是早期 function calling 的现行写法。MCP 里的 tool 是同一份声明的另一种协议表达,结构可映射。
NexAIX-官方博客
评论(0)