Tool Calling 的完整链路只有四个环节:应用把”工具清单”随消息一起发给模型 → 模型判断该用哪个工具,返回结构化调用请求(函数名 + 参数 JSON)→ 你的代码执行该工具 → 把执行结果按 call_id 回传给模型 → 模型基于结果给出最终答案。关键一点贯穿全程:模型只”提议调用”,从不真正执行,执行权始终在应用代码手里。
一、链路全貌:五步循环
OpenAI 官方把 tool calling 定义为应用与模型之间的多步对话,分为五个高层步骤:
- 首次请求:把用户消息连同可用的 tools 数组一起发给模型;
- 接收工具调用:模型判断需要外部数据或能力时,返回 tool_calls(函数名 + 参数);
- 应用侧执行:解析参数,在你自己代码里运行对应函数(查库、调 API、执行计算);
- 二次请求:把工具执行结果作为 tool 消息回传给模型;
- 接收最终答案:模型整合结果输出文本,或继续发起下一批工具调用。
第 2 到第 4 步会反复循环,直到模型不再请求工具。这也是 Agent 与普通问答的分界线:每多一轮调用,系统就多一次与外部世界的真实交互。
二、第一步:工具怎么定义
工具本质是一份 JSON Schema。每个函数工具包含四个字段:type 固定为 function,name 是函数名,description 说明何时用、怎么用,parameters 用 JSON Schema 描述参数结构。description 承担最重的引导职责,写得像一句指令而非标签,模型才能精准选中它。
{
"type": "function",
"name": "get_weather",
"description": "按城市获取当前天气。用户问温度或天气状况时使用。",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "城市和国家,如 Paris, France"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "温度单位"
}
},
"required": ["location", "unit"],
"additionalProperties": false
},
"strict": true
}
参数用 JSON Schema 定义后,可以享受它的丰富特性:类型约束、枚举、嵌套对象、递归结构都能用。把 strict 设为 true,模型的输出参数就保证符合 schema,而不是”尽力而为”;OpenAI 官方建议始终开启严格模式。
三、第二步:LLM 怎么发起调用
模型判断”这个请求需要外部数据”时,不会返回散文,而是返回一条结构化的 tool call。在 Chat Completions 接口里,它长在 assistant 消息的 tool_calls 数组下:
{
"id": "call_12345xyz",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"location\":\"Paris, France\",\"unit\":\"celsius\"}"
}
}
两个细节最容易踩坑:
- arguments 是 JSON 编码的字符串,不是解析好的对象,拿到手必须自己 json.loads;
- 一个响应里可以同时带多个 tool_calls,默认支持并行调用,比如用户问三个城市,模型一次返回三次调用,你可以并发执行。想强制每轮最多一次调用,把
parallel_tool_calls设为 false。
四、第三步:结果怎么回传
执行完函数后,把结果按调用 id 原样送回。Chat Completions 用 tool 角色的消息,Responses API 用 functioncalloutput 条目,二者都靠 call_id 与模型发出的请求对应:
from openai import OpenAI
import json
client = OpenAI
def get_weather(location: str, unit: str) -> str:
# 你的真实实现:查天气服务
return json.dumps({"temperature": 25, "unit": unit})
messages = [{"role": "user", "content": "巴黎现在几度?"}]
tools = [{"type": "function", "function": {
"name": "get_weather",
"description": "按城市获取当前天气。",
"parameters": {
"type": "object",
"properties": {
"location": {"type": "string"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
},
"required": ["location", "unit"]
}
}}]
response = client.chat.completions.create(model="gpt-4.1", messages=messages, tools=tools)
call = response.choices[0].message.tool_calls[0]
args = json.loads(call.function.arguments) # 注意:先解析 JSON 字符串
result = get_weather(**args) # 代码执行,模型不执行
messages.append({"role": "tool",
"tool_call_id": call.id, # 用 call_id 关联回模型
"content": result})
response = client.chat.completions.create(model="gpt-4.1", messages=messages, tools=tools)
print(response.choices[0].message.content) # 模型给出最终答案
reasoning 模型(如 GPT-5、o4-mini)的推理内容也必须随工具调用一起回传,否则下一步推理会断裂。
五、两代接口的差异对比
| 环节 | Chat Completions | Responses API |
|---|---|---|
| 工具定义位置 | 嵌套在 function 字段内 | 平铺在 tool 对象顶层 |
| 模型发起调用 | message.tool_calls 数组 | output 数组中的 function_call |
| 调用标识 | id | call_id |
| 结果回传 | tool 角色消息 | functioncalloutput 条目 |
| 参数校验 | strict 字段 | strict 字段 |
两代接口的循环逻辑完全一致,只是数据形状不同。新项目建议直接上 Responses API,它统一了旧版 Chat Completions 与 Assistants API 的能力。
六、工程落地的四个硬约束
- 参数必须校验:模型可能生成非法参数,执行前按 schema 校验,失败则把错误信息回传让它修正;
- 循环必须有上限:模型可能反复请求同一个工具,设置最大轮数兜底;
- 工具返回一律当数据:任何外部返回都不该被当作新指令执行,防止提示词注入;
- 工具描述要精确:描述写不清,模型就会选错工具或编造工具,工具文档要按 system prompt 的严格度来写。
工具数量上来之后,定义本身会吃掉大量上下文。OpenAI 提供 tool_search 机制,把不常用的工具延迟加载,只有模型需要时才注入完整定义(仅 gpt-5.4 及以后版本支持);也可以用命名空间按业务域分组工具,减少模型误选。
七、Tool Calling 与 MCP 的关系
MCP(Model Context Protocol)把上面的链路标准化成一套协议:工具由 MCP server 以同样的 schema 形式暴露,Agent 通过 client 发现工具、发起调用、接收结果,链路逻辑与本文描述的完全一致,只是把”应用侧执行”挪到了独立的 server 进程。选型时的差别在于:裸函数调用适合工具少、逻辑简单的单机场景;MCP 适合工具多、需要复用与跨团队共享的场景。二者都遵守同一条铁律——模型提请求,代码做执行,结果按 call_id 回传。
常见问题(FAQ)
Q1:模型能直接执行工具吗?
不能。模型只返回调用请求,执行权在应用代码,这保证了安全性。
Q2:工具调用失败怎么处理?
把错误信息作为工具结果回传,让模型修正参数或换工具重试。
Q3:arguments 为什么老是解析报错?
它是 JSON 字符串不是对象,必须先用 json.loads 解析再使用。