Skip to content
Tool Calling 机制:从模型工具调用到外部工具执行
概述
Tool Calling(在 OpenAI API 中称为 Function Calling,在 Anthropic API 中称为 Tool Use)是让大语言模型在需要外部信息或操作时,输出结构化工具调用指令的一种机制。模型本身不执行实际函数,而是生成一个包含函数名和参数的结构化数据,由应用程序执行对应函数,并把执行结果回传给模型,让模型基于结果生成最终回复。
Tool Calling 的出现弥补了语言模型的两类局限:其一是知识截止时间带来的信息缺失,其二是模型无法主动执行操作(如查询数据库、调用外部 API、操作文件系统)。通过 Tool Calling,应用可以在模型与外部系统之间形成一个可编程的完整闭环。
基本概念
Tool / Function
工具(Tool)是模型可以调用的外部函数。在 API 层面上,一个工具由名称、描述和输入参数的 JSON Schema 构成。模型根据用户请求和工具描述,决定是否调用以及如何使用该工具。
Function Calling 与 Tool Calling 这两个术语在实际使用中基本等价。OpenAI 官方文档将两者并列使用,并明确说明:Function Calling 就是 Tool Calling 的一种具体形式。Anthropic 则使用 Tool Use 来表述模型在响应中输出工具调用块的行为。
Agent
Agent 指能够自主完成多步任务的系统。与单次问答的聊天机器人不同,Agent 在一个循环中反复调用模型,根据模型输出的工具调用指令执行相应操作,并把操作结果再次交给模型,直到任务完成。Oracle 开发者博客将这种循环称为 “AI Agent Loop”,并指出其核心区别于普通聊天机器人:Agent 能跨多个步骤保持目标、感知结果并决定下一步动作。所有主流 AI 公司的产品虽然实现各异,但都收敛于这一核心模式[6]。
工具定义:JSON Schema 与描述设计
模型需要通过工具定义来了解外部函数的存在、用途、输入参数和返回值。工具定义通常以 JSON Schema 描述输入参数。
以 OpenAI 格式为例,一个查询天气的工具定义如下:
json
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市的当前天气",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,如「北京」"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "温度单位,可选值为摄氏或华氏"
}
},
"required": ["city"]
}
}
}工具定义中的 description 字段会作为模型推理的依据之一。当模型判断用户请求需要工具能力时,它会综合工具名称、描述、参数的 description 等信息来生成 JSON 参数。因此,工具描述应尽量说明函数的行为和边界,参数描述应明确字段格式和取值范围。
在 API 的请求消息结构上,OpenAI 将工具列表放在 tools 字段中。Anthropic 使用的字段名同为 tools,但其工具定义是扁平结构:
json
{
"name": "get_weather",
"description": "查询指定城市的当前天气",
"input_schema": {
"type": "object",
"properties": {
"city": { "type": "string" }
},
"required": ["city"]
}
}两者的差异集中体现在:OpenAI 的工具包了一层 type: "function" 和 function 键,参数 schema 字段名为 parameters;Anthropic 直接在顶层写 name、description 和 input_schema。
一次工具调用的完整流程
一次工具调用的完整流程可以分为以下阶段:
- 应用将系统提示词、历史消息和当前用户请求发送给模型,附带可用工具列表。
- 模型判断是否需要调用工具,若需要,则返回结构化
tool_calls数据,包含工具名和参数;若不需要,返回普通回复。 - 应用解析
tool_calls,执行对应的工具函数。 - 应用将工具执行结果以
tool消息(OpenAI 格式)或tool_result块(Anthropic 格式)回传给模型。 - 模型整合结果,生成最终回复。如果结果不足以完成任务,模型可能继续请求调用其他工具。
下面的流程图展示了这一闭环:
mermaid
graph TD
A[用户发送请求] --> B[模型接收消息与工具列表]
B --> C{模型输出中是否包含tool_calls?}
C -- 是 --> D[应用执行对应工具]
C -- 否 --> E[模型返回最终回复]
D --> F[工具执行结果以tool消息回传]
F --> B
E --> G[返回给用户]模型如何决定调用工具
模型通过系统提示词和工具描述来决定是否调用工具以及调用哪个工具。这一决策发生在模型推理过程中:模型根据用户请求的语义,从候选工具列表中选择匹配的工具,并按该工具的 JSON Schema 生成参数。
在 OpenAI API 中,tool_choice 参数控制模型对工具调用的选择行为[1]:
"auto"(默认值):模型自行决定是否调用工具,可以调用零个、一个或多个工具。"none":禁止模型调用工具,此时即使有可用工具,模型也只生成普通回复。"required":强制模型至少调用一个工具。{"type": "function", "function": {"name": "get_weather"}}:强制模型调用指定函数。
下面用一个 Node.js 示例展示如何发送带工具定义的请求:
javascript
import OpenAI from "openai";
const openai = new OpenAI();
const tools = [
{
type: "function",
function: {
name: "get_weather",
description: "查询指定城市的当前天气",
parameters: {
type: "object",
properties: {
city: { type: "string", description: "城市名称" }
},
required: ["city"]
}
}
}
];
const response = await openai.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: "北京现在多少度?" }],
tools,
tool_choice: "auto"
});
console.log(response.choices[0].message);模型的响应中,当 finish_reason 为 "tool_calls" 时,message.tool_calls 数组包含一个或多个工具调用对象。每个工具调用对象的结构为:
json
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\": \"北京\"}"
}
}注意 arguments 是一个 JSON 字符串,应用需要先解析它,再作为参数执行工具函数。
执行工具并回传结果
应用收到 tool_calls 后,根据 function.name 找到对应的本地函数,把解析后的参数传给它执行。执行结果需要以消息的形式追加到对话历史中。
OpenAI API 要求工具结果使用 role: "tool" 的消息,并附带对应的 tool_call_id。这个 ID 用于关联工具调用和工具结果。
javascript
const message = response.choices[0].message;
// 如果模型要求调用工具
if (message.tool_calls) {
const toolCall = message.tool_calls[0];
const args = JSON.parse(toolCall.function.arguments);
// 执行本地工具函数
const result = getWeather(args.city);
// 把工具结果回传给模型
const secondResponse = await openai.chat.completions.create({
model: "gpt-4o",
messages: [
{ role: "user", content: "北京现在多少度?" },
message,
{
role: "tool",
tool_call_id: toolCall.id,
content: JSON.stringify(result)
}
],
tools
});
console.log(secondResponse.choices[0].message.content);
}在这个示例中,getWeather 可以是任意外部函数,比如调用天气服务 API 的封装函数。它的返回值会被序列化为 JSON 字符串放在 content 字段中。
模型整合工具结果生成最终回复
第二次请求中,模型会看到用户原始请求、它自己的工具调用消息以及工具执行结果,据此生成最终回复。例如:
用户:北京现在多少度?
模型第一个响应(工具调用):
json
{
"message": {
"role": "assistant",
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\": \"北京\"}"
}
}
]
},
"finish_reason": "tool_calls"
}工具结果消息:
json
{
"role": "tool",
"tool_call_id": "call_abc123",
"content": "{\"temperature\": 8, \"unit\": \"celsius\", \"condition\": \"晴\"}"
}模型整合后生成的最终回复:
北京现在的气温是 8 摄氏度,天气晴朗。
此时 finish_reason 为 "stop",对话结束。
主流 API 的 Tool Calling 实现
OpenAI Function Calling
OpenAI 在 chat.completions API 中提供函数调用能力[1]。工具以 tools 字段传入请求,模型在响应中返回 message.tool_calls。工具结果以 role: "tool" 的消息回传。OpenAI 还支持并行函数调用,即模型在一次响应中返回多个工具调用,可通过 parallel_tool_calls: false 关闭该行为。
Anthropic Tool Use
Anthropic API 的工具调用机制称为 Tool Use。Claude 在需要调用函数时,会在响应 content 数组中输出一个 type: "tool_use" 的块。应用执行工具后,需要在下一条 user 消息中加入 type: "tool_result" 块来返回结果。Anthropic SDK 提供了一个 beta 版本的 Tool Runner 来帮助自动运行调用循环,但手动循环可以更清晰地展示请求和响应结构[3]。
开源模型
以 Llama 3 为代表的开源模型也提供了 Tool Use 能力。其基本的做法是:在系统提示词中声明可用工具,模型在生成文本输出时输出结构化格式的工具调用文本,例如以特定标签包裹函数名和参数。相比商业 API,开源模型对工具调用格式没有强约束,通常需要应用侧定义解析规则,且不同模型之间的格式差异较大。
下表对比了三类 API 的核心差异:
| 方面 | OpenAI | Anthropic | 开源模型(如 Llama 3) |
|---|---|---|---|
| 工具定义结构 | {type, function: {name, description, parameters}} | {name, description, input_schema} | 因模型而异 |
| 请求字段 | tools | tools | 因模型而异 |
| 参数 schema 键名 | parameters | input_schema | 因模型而异 |
| 响应中的工具调用位置 | message.tool_calls 数组 | content 数组中的 tool_use 块 | 文本或结构化输出 |
| 结果回传方式 | role: "tool" 消息 | user 消息中包含 tool_result 块 | 以文本或消息形式回传 |
API 结构差异
OpenAI 响应中的工具调用结构为:
json
{
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_1",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\": \"北京\"}"
}
}
]
},
"finish_reason": "tool_calls"
}Anthropic 响应中的工具调用结构为:
json
{
"content": [
{
"type": "tool_use",
"id": "toolu_01",
"name": "get_weather",
"input": { "city": "北京" }
}
],
"stop_reason": "tool_use"
}两个平台都要求工具结果原样回传。OpenAI 用 tool_call_id 关联工具调用与结果;Anthropic 用 tool_use_id 关联,也就是 tool_use 块中的 id 值,tool_result 块通过 tool_use_id 字段指向该值。
多轮工具调用与 Agent 循环
当单次工具调用无法完成任务时,需要多轮工具调用。Agent Loop 将这一过程抽象为循环模式[4][5]:
text
while not done:
response = call_llm(messages)
if response has tool_calls:
results = execute_tools(response.tool_calls)
messages.append(results)
else:
done = True
return response翻译成 Node.js 代码:
javascript
const messages = [
{ role: "system", content: "你是一个能查询天气的助手。" },
{ role: "user", content: "北京和上海哪个更热?" }
];
const MAX_ITERATIONS = 5; // 保护上限,防止模型无限调用工具
for (let i = 0; i < MAX_ITERATIONS; i++) {
const response = await openai.chat.completions.create({
model: "gpt-4o",
messages,
tools
});
const message = response.choices[0].message;
if (!message.tool_calls) {
console.log(message.content);
break;
}
messages.push(message);
for (const toolCall of message.tool_calls) {
const args = JSON.parse(toolCall.function.arguments);
const result = executeTool(toolCall.function.name, args);
messages.push({
role: "tool",
tool_call_id: toolCall.id,
content: JSON.stringify(result)
});
}
}这里的关键是,模型每轮对话都会看到之前所有的工具调用记录和结果,因此它可以根据已经获得的中间结果决定下一步动作。MAX_ITERATIONS 是应用层防御措施,不是 API 强制字段,用于限制循环次数,防止模型无限调用工具。
Agent Loop 是 Agent 系统的核心架构。与单次问答不同,Agent 能跨多个步骤保持目标、适应环境变化并执行操作[6]。工程上,Agent 循环的 token 开销通常比标准对话大得多,Oracle 的博客指出代理系统大约消耗标准聊天的 4 倍 token,多代理系统可达 15 倍[4]。
上下文管理与消息历史维护
工具调用过程中,每次模型响应和工具结果都会累积到消息历史中。消息历史的长度直接影响上下文窗口的占用和 API 调用成本。
消息历史需要保留以下内容:
- 用户原始请求
- 助手消息(含或不含
tool_calls) - 工具结果消息
如果工具结果较大,可以通过摘要或截断来减小上下文占用。例如,用文本摘要替代完整 JSON、对长列表只保留前 N 项、或用独立状态存储来保存中间数据,只把精简结果放入消息历史。
某些平台对消息顺序有要求。OpenAI 要求在 tool 消息之前必须存在对应的助手工具调用消息;如果在工具调用后没有正确追加工具结果就进行下一次请求,请求会报错。
工具调用与 RAG、记忆机制的关系
RAG(检索增强生成)与工具调用是两种互补的能力。
RAG 的核心行为是:根据用户问题从外部知识库检索相关文档,再把检索结果作为上下文注入模型提示词,以增强生成质量。RAG 解决的是信息获取问题。
工具调用的核心行为是:模型按需调用外部函数,将工具执行结果回传后生成最终回复。工具调用解决的是模型与外部系统的交互问题。
两者通常可以在一个系统中并存。例如,一个助手可以先通过工具调用调用搜索接口,然后把搜索结果作为上下文传递给模型进行后续回答。这里的搜索接口就是一个工具,而搜索返回的文档内容可以被视为动态注入的上下文。
记忆机制与工具调用也是互不冲突的。长期记忆通常解决跨会话的用户偏好和历史事实存储;短期记忆则是对当前会话中消息历史的维护。工具调用是模型获取实时信息的手段,它产生的结果可以写入长期记忆,也可以在下一次工具调用时读取。
错误处理、安全控制与可观测性
错误处理
工具执行可能失败。常见的错误类型包括:工具函数抛出异常、参数解析失败、工具调用超时。应用侧需要对每种失败情况做处理。
一种常见做法是返回一个结构化的错误消息,让模型能够感知失败原因并决定下一步策略:
javascript
try {
const result = executeTool(name, args);
messages.push({
role: "tool",
tool_call_id: toolCall.id,
content: JSON.stringify(result)
});
} catch (error) {
messages.push({
role: "tool",
tool_call_id: toolCall.id,
content: JSON.stringify({
error: error.message,
status: "failed"
})
});
}如果参数解析失败(JSON 解析异常),可以重试一次,或者在错误消息中说明参数格式要求,让模型在下一轮重新生成参数。
安全控制
工具调用过程中需要处理以下安全风险:
- 工具名称冲突:确保工具名称在同一次请求中唯一。
- 参数注入:工具参数来自模型生成的文本,属于不可信输入。工具执行前需要校验参数类型和取值,特别是当参数会拼接到 SQL、shell 命令或文件路径时。
- 权限控制:每个工具的执行权限应该在应用侧定义,而不是依赖模型自行判断。例如,管理员操作工具应做额外鉴权。
- 提示注入:工具返回的外部内容可能包含恶意指令,应用需要在把工具结果放入消息历史时进行无害化处理,或者明确在系统提示词中约束模型不要执行结果内容中的指令。
可观测性
工具调用过程涉及多次模型请求和工具执行,问题排查需要完整的调用链记录。建议至少记录以下信息:
- 每次模型请求的 token 数
- 模型返回的工具调用列表(工具名、参数、tool_call_id)
- 每次工具执行的耗时和结果
- 循环迭代次数
- 最终完成的错误或成功状态
这些数据可以以结构化日志的形式输出,便于后续分析。
测试与评估
Tool Calling 应用的测试,核心是回答两个问题:
- 模型是否选择了正确的工具?
- 生成的参数是否能被工具函数直接使用?
自动化评估的常用方式是构建一个测试集,每个测试用例包含一条用户请求和期望的工具调用行为,然后循环执行并对比。
javascript
import OpenAI from "openai";
const openai = new OpenAI();
// 工具定义与前文相同,此处省略
const tools = [/* ... */];
const testCases = [
{
input: "北京现在的气温是多少?",
expectTool: "get_weather",
expectArgs: { city: "北京" }
}
];
async function evaluateCase(testCase) {
const response = await openai.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: testCase.input }],
tools
});
const message = response.choices[0].message;
if (!message.tool_calls) {
return { toolCorrect: false, argsValid: false, reason: "未生成工具调用" };
}
const call = message.tool_calls[0];
let args;
try {
args = JSON.parse(call.function.arguments);
} catch {
return { toolCorrect: false, argsValid: false, reason: "参数解析失败" };
}
return {
toolCorrect: call.function.name === testCase.expectTool,
argsValid: JSON.stringify(args) === JSON.stringify(testCase.expectArgs)
};
}
for (const testCase of testCases) {
const result = await evaluateCase(testCase);
console.log(result);
}直接的 JSON.stringify 比较有一些局限:模型可能调整参数顺序,也可能在必填参数之外补充可选字段。因此更常见的判定方式是逐个字段校验:先检查必填字段是否齐全,再检查每个字段的类型和取值范围,最后用 JSON Schema 校验器验证完整结构。只要参数能被工具函数接受,就应判定为合格,而不必与人工标注完全一致。
常用的评估指标包括:
- 工具选择准确率:正确选择工具的次数除以测试用例总数。
- 参数合格率:参数通过 JSON Schema 校验的次数除以工具调用次数。
- 端到端任务成功率:最终回复满足用户请求的比例。
自动校验之外还应保留人工抽检。部分用户请求在语义上可以匹配多个工具,自动判定无法覆盖这类情况。
多轮工具调用示例
下面是一个完整的 Node.js 示例,模拟一个能够查询数据库、计算汇总结果的 Agent 场景:
javascript
import OpenAI from "openai";
const openai = new OpenAI();
const tools = [
{
type: "function",
function: {
name: "query_user_orders",
description: "查询某个用户的历史订单",
parameters: {
type: "object",
properties: {
userId: { type: "string", description: "用户 ID" }
},
required: ["userId"]
}
}
},
{
type: "function",
function: {
name: "calculate_total",
description: "计算订单金额总和",
parameters: {
type: "object",
properties: {
orders: { type: "array", items: { type: "number" } }
},
required: ["orders"]
}
}
}
];
const messages = [
{ role: "system", content: "你是一个用户订单分析助手。" },
{ role: "user", content: "用户 U-1001 的总订单金额是多少?" }
];
for (let i = 0; i < 5; i++) {
const response = await openai.chat.completions.create({
model: "gpt-4o",
messages,
tools,
tool_choice: "auto"
});
const message = response.choices[0].message;
if (!message.tool_calls) {
console.log("最终回复:", message.content);
break;
}
messages.push(message);
for (const call of message.tool_calls) {
const args = JSON.parse(call.function.arguments);
let result;
if (call.function.name === "query_user_orders") {
// 此处为演示数据,实际应由数据库查询返回
result = [{ id: "o1", amount: 199 }, { id: "o2", amount: 89.5 }];
} else if (call.function.name === "calculate_total") {
result = { total: args.orders.reduce((a, b) => a + b, 0) };
}
messages.push({
role: "tool",
tool_call_id: call.id,
content: JSON.stringify(result)
});
}
}这个示例中,模型首先调用 query_user_orders 获取订单列表,然后调用 calculate_total 计算总和,最后生成回复。两次工具调用分别在独立的循环迭代中完成。
生态与趋势:MCP 与工具调用标准化
随着工具调用能力成为各平台的标配,工具的定义和调用格式开始出现标准化趋势。Anthropic 提出的 Model Context Protocol 是这一领域的代表性探索。
MCP 采用客户端-服务器架构,为模型上下文提供标准化的数据交互方式。MCP 协议中定义了三种核心原语:工具、资源和提示。工具与 Tool Calling 中的工具类似,用于让模型调用外部能力;资源用于提供上下文数据;提示用于管理提示词模板。通过连接 MCP 服务器,应用可以复用标准化的工具接口,而不必针对每家 API 单独适配。
MCP 的意义在于减少工具调用的集成成本。如果没有统一协议,每个工具提供方和模型平台之间都需要定制适配层;有了 MCP,工具提供商只需实现一份 MCP 服务器,即可接入多个支持 MCP 的客户端。
工具调用本身的协议仍在演进中。当前主流 API 的差异仍然存在:OpenAI 的 tool_calls、Anthropic 的 tool_use 块、开源模型的文本式调用格式,这些格式短期内不会完全统一。应用层要么针对具体 API 做适配,要么通过一层抽象接口屏蔽底层差异。
限制
Tool Calling 机制有一些明确的边界:
- 模型不保证生成的工具调用参数一定有效,应用需在参数进入工具函数前执行校验。
- 模型的工具选择也不一定总是最优的。工具数量增加时,模型可能选择错误的工具,或者遗漏应该使用的工具。
- 工具调用会增加延迟和 token 消耗。每次工具调用至少需要两次模型请求,Agent 循环场景下更高。
- 模型对工具的描述理解有限,工具描述写得模糊或不完整时,调用准确率会下降。
- 工具调用机制与训练相关,不同模型的能力差异很大。开源模型对工具调用的指令遵循能力普遍弱于商业 API 模型。
参考链接
- [1] https://developers.openai.com/api/docs/guides/function-calling
- [3] https://docs.parallel.ai/integrations/anthropic-tool-calling
- [4] https://blogs.oracle.com/developers/what-is-the-ai-agent-loop-the-core-architecture-behind-autonomous-ai-systems
- [5] https://blogs.oracle.com/developers/the-agent-loop-decoded-three-levels-every-agent-engineer-must-know
