工具调用API怎么写?跨模型四层差异与循环骨架

2026-08-26 58 0

工具调用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 里的 namedescriptioninputSchema 与 OpenAI 格式的 tools 定义直接映射,心智模型就是:声明格式可映射,调用协议不同。别混着调试,否则会浪费大量时间。

差异一:函数声明——JSON Schema 怎么写才在多家模型上都稳

在工具调用API里,每个函数由三部分构成:namedescription、参数 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 是不是 noneauto 且模型判断不需要;再查 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/listtools/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 是否设为 noneauto 且模型认为不需要;description 是否写清触发条件;最后才怀疑模型能力。有时是中间层吞了字段。

function calling 和工具调用是一回事吗?

基本是一回事,今天说的工具调用API 就是早期 function calling 的现行写法。MCP 里的 tool 是同一份声明的另一种协议表达,结构可映射。

相关文章

Agent API 怎么接:从框架配置到工具调用的四个验证点
GLM-5.3 API接入:立即要改的致命参数与迁移清单
AI API中转站锁定模型关闭自动路由的请求配置与验证
工具调用API怎么写?跨模型四层差异与循环骨架
OpenAI SDK兼容多轮对话怎么传思考历史?工具调用核对表
OpenAI base_url 怎么改?三种写法与报错对照

评论(0)

暂无评论

发布评论