Skip to content

工具调用(Tool Calling)

工具调用(Tool Calling,也叫 Function Calling)让模型不再只是生成文本,而是在需要时返回一个结构化的「函数调用请求」。你的程序执行真实逻辑(查天气、查数据库、下单),把结果回传给模型,模型再据此生成最终回复。RouteAPI 在 /v1/chat/completions(OpenAI 格式)和 /v1/messages(Claude 格式)上都支持工具调用。

工具调用是一次多轮往返:

  1. 你在请求里声明一组「工具」(每个工具有名字、描述和参数 schema)。
  2. 模型判断是否需要某个工具,如果需要,返回一个带参数的调用请求,而不是直接回答。
  3. 你的代码解析参数、执行真实逻辑、拿到结果。
  4. 你把结果回传给模型,模型生成面向用户的自然语言回复。

模型本身不会执行任何代码,它只负责决定「调用哪个工具」和「传什么参数」。真正的执行永远发生在你这一侧,这也意味着安全边界(权限校验、参数过滤)由你控制。

场景说明
实时数据查询天气、汇率、股价、库存等模型训练数据里没有的信息
内部系统集成查数据库、调内部 API、读订单状态
执行动作下单、发邮件、创建工单等有副作用的操作
结构化抽取强制模型按固定 schema 输出,等价于一种结构化输出手段
智能体编排Agent 框架用工具调用驱动多步任务

工具调用能力取决于所选模型。主流模型(OpenAI GPT 系列、Claude 系列、Gemini 系列等)大多支持,但最大工具数、是否支持并行调用等细节各不相同。模型列表接口不返回工具调用能力字段,请查阅模型供应商文档,或直接用目标模型发一次带 tools 的请求验证,详见 Models。

在 /v1/chat/completions 中,工具通过顶层 tools 数组声明,每个工具是一个 type: "function" 的嵌套对象。

{
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市的当前天气。城市名使用中文全称。",
"parameters": {
"type": "object",
"properties": {
"city": { "type": "string", "description": "城市名,如 北京" },
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "温度单位,默认摄氏度"
}
},
"required": ["city"]
}
}
}
]
}
字段类型必填说明
typestring是固定为 "function"
function.namestring是工具名,只能包含字母、数字、下划线和连字符
function.descriptionstring建议工具用途说明,模型据此决定是否调用
function.parametersobject否参数定义,标准 JSON Schema

description 的质量直接决定模型是否会正确选用工具。写清用途、每个参数的含义、取值范围和边界条件,比调整任何采样参数都更有效。

parameters 使用标准 JSON Schema 描述参数结构:

  • type: 通常是 "object"。
  • properties: 每个参数的类型、描述、枚举值。
  • required: 必填参数名列表。
  • enum: 限定取值范围,能显著降低模型传错值的概率。

没有参数的工具也要给出空 schema:"parameters": { "type": "object", "properties": {} }。

在 /v1/messages 中,工具定义是平铺结构,没有外层 type 和 function 包装,参数字段名叫 input_schema。

{
"tools": [
{
"name": "get_weather",
"description": "查询指定城市的当前天气。城市名使用中文全称。",
"input_schema": {
"type": "object",
"properties": {
"city": { "type": "string", "description": "城市名,如 北京" },
"unit": { "type": "string", "enum": ["celsius", "fahrenheit"] }
},
"required": ["city"]
}
}
]
}

input_schema 的内部结构和 OpenAI 的 parameters 完全一致,都是标准 JSON Schema。差异只在外层包装。

对比项OpenAI(/v1/chat/completions)Claude(/v1/messages)
外层包装{ "type": "function", "function": {...} }直接平铺,无包装
工具名字段function.namename
描述字段function.descriptiondescription
参数字段function.parametersinput_schema
参数 schema标准 JSON Schema标准 JSON Schema(一致)
模型返回message.tool_calls 数组content 里的 tool_use 块
结果回传角色独立的 role: "tool" 消息user 消息里的 tool_result 块
结果关联字段tool_call_idtool_use_id

Claude 协议的完整细节(含 is_error、流式 input_json_delta 等)见 Claude Messages 协议。本页后续示例默认以 OpenAI 格式为主。

tool_choice 控制模型如何选择工具。

值行为
"auto"模型自行决定是否调用工具,以及调用哪个。有 tools 时的默认值
"none"禁止调用任何工具,模型只输出文本
"required"必须调用至少一个工具,但由模型选择调用哪个
{ "type": "function", "function": { "name": "get_weather" } }强制调用指定工具
{ "tool_choice": "auto" }

最常用。适合「用户问题有时需要工具、有时直接回答」的通用场景。

{ "tool_choice": "none" }

临时关闭工具但保留定义。常用于「先让模型总结,不要再调工具」的收尾阶段。

{ "tool_choice": "required" }

强制模型走工具路径。适合结构化抽取这类「必须产出结构化结果」的场景。

{
"tool_choice": {
"type": "function",
"function": { "name": "get_weather" }
}
}

Claude 格式的对应写法是 { "type": "tool", "name": "get_weather" },required 对应 { "type": "any" },none 对应 { "type": "none" }。

一次完整的工具调用至少包含两轮请求。

发送带 tools 的请求:

Terminal window
curl https://api.routeapi.ai/v1/chat/completions \
-H "Authorization: Bearer $ROUTEAPI_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.5",
"messages": [
{ "role": "user", "content": "北京现在天气怎么样?" }
],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市的当前天气。",
"parameters": {
"type": "object",
"properties": { "city": { "type": "string" } },
"required": ["city"]
}
}
}
]
}'

模型判断需要调用工具时,finish_reason 为 tool_calls,message.content 为 null,tool_calls 里带上调用请求:

{
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\": \"北京\"}"
}
}
]
},
"finish_reason": "tool_calls"
}
]
}

注意 arguments 是一个 JSON 字符串,不是对象,需要你自己 JSON.parse / json.loads 解析。模型偶尔会生成不合法的 JSON,解析要放在 try/catch 里。

按 function.name 分发到你的真实逻辑,用解析后的参数执行:

import json
args = json.loads(tool_call["function"]["arguments"])
result = get_weather(**args) # "北京,晴,23 摄氏度"

把第一轮的 assistant 消息(含 tool_calls)原样加回 messages,再追加一条 role: "tool" 消息携带结果。tool_call_id 必须与第一轮的 id 完全一致:

{
"model": "gpt-5.5",
"messages": [
{ "role": "user", "content": "北京现在天气怎么样?" },
{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": { "name": "get_weather", "arguments": "{\"city\": \"北京\"}" }
}
]
},
{
"role": "tool",
"tool_call_id": "call_abc123",
"content": "北京,晴,气温 23 摄氏度,湿度 45%。"
}
],
"tools": []
}

第二轮请求返回自然语言结果,finish_reason 恢复为 stop:

{
"choices": [
{
"message": {
"role": "assistant",
"content": "北京现在是晴天,气温 23 摄氏度,湿度 45%,比较舒适。"
},
"finish_reason": "stop"
}
]
}

模型可能需要多轮工具调用才能完成任务:先查订单号,再用订单号查物流。每一轮都遵循「模型返回 tool_calls → 执行 → 回传结果」的循环,直到 finish_reason 变回 stop。生产代码应写成循环,并设置最大轮数上限防止无限循环:

MAX_TURNS = 5
for _ in range(MAX_TURNS):
resp = call_model(messages)
msg = resp["choices"][0]["message"]
messages.append(msg)
if not msg.get("tool_calls"):
break # 模型给出最终回复
for tc in msg["tool_calls"]:
result = dispatch(tc) # 执行并返回字符串
messages.append({
"role": "tool",
"tool_call_id": tc["id"],
"content": result,
})

一轮里模型可能同时请求多个互不依赖的工具(如同时查北京和上海的天气),tool_calls 会是一个多元素数组。你需要为每一个 tool_call 都追加一条对应的 role: "tool" 消息,tool_call_id 一一对齐,缺一条下一轮就会报错。

如果想禁用并行、强制模型一次只调一个工具,OpenAI 格式加 "parallel_tool_calls": false,Claude 格式在 tool_choice 里加 "disable_parallel_tool_use": true。

设置 stream: true 时,工具调用参数是逐片增量返回的。

每个 SSE chunk 的 delta.tool_calls 里带 index(标识第几个工具调用),function.arguments 是参数 JSON 的一个片段:

data: {"choices":[{"delta":{"tool_calls":[{"index":0,"id":"call_abc123","type":"function","function":{"name":"get_weather","arguments":""}}]}}]}
data: {"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"city\":"}}]}}]}
data: {"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\"北京\"}"}}]}}]}
data: {"choices":[{"finish_reason":"tool_calls"}]}
data: [DONE]

按 index 分组累积:id 和 name 通常只在第一个片段出现,arguments 需要把所有片段拼接完整后再解析。不要在拼接过程中尝试解析中间态(那是不完整的 JSON)。

buffers = {} # index -> {"id", "name", "args"}
for chunk in stream:
for tc in chunk["choices"][0]["delta"].get("tool_calls", []):
i = tc["index"]
buf = buffers.setdefault(i, {"id": "", "name": "", "args": ""})
if tc.get("id"):
buf["id"] = tc["id"]
fn = tc.get("function", {})
if fn.get("name"):
buf["name"] = fn["name"]
if fn.get("arguments"):
buf["args"] += fn["arguments"]
# 流结束后再解析
for buf in buffers.values():
args = json.loads(buf["args"])

Claude 格式的流式工具参数通过 input_json_delta 事件的 partial_json 增量返回,按 index 累积后解析,细节见 Claude Messages 协议。

RouteAPI 聚合的多数主流模型都支持工具调用,包括 OpenAI GPT 系列、Claude 系列、Gemini 系列等。先用模型列表接口确认模型在你账户下可用:

Terminal window
curl https://api.routeapi.ai/v1/models \
-H "Authorization: Bearer $ROUTEAPI_KEY"

该接口只返回模型是否可用及可用端点,不返回工具调用能力。是否支持工具调用请以供应商文档或一次实际带 tools 的请求为准。

不同模型在以下维度存在差异,请在测试环境验证后再上线:

维度说明
最大工具数单次请求可声明的工具数量上限因模型而异
并行调用部分模型不支持一轮返回多个 tool_calls
tool_choice 支持度required / 指定特定工具并非所有模型都支持
参数复杂度深层嵌套或超大 JSON Schema 可能被部分模型截断或忽略
流式增量粒度arguments 分片方式在不同模型间不一致,务必按 index 累积

以下三段代码功能等价:定义工具 → 第一轮拿到 tool_calls → 执行 → 第二轮回传结果 → 拿到最终回复。

curl(手动完成两轮):

Terminal window
# 第一轮:发起带工具的请求
curl https://api.routeapi.ai/v1/chat/completions \
-H "Authorization: Bearer $ROUTEAPI_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.5",
"messages": [{ "role": "user", "content": "北京现在天气怎么样?" }],
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市的当前天气。",
"parameters": {
"type": "object",
"properties": { "city": { "type": "string" } },
"required": ["city"]
}
}
}]
}'
# 第二轮:把工具结果回传(tool_call_id 用第一轮返回的值)
curl https://api.routeapi.ai/v1/chat/completions \
-H "Authorization: Bearer $ROUTEAPI_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.5",
"messages": [
{ "role": "user", "content": "北京现在天气怎么样?" },
{
"role": "assistant",
"content": null,
"tool_calls": [{
"id": "call_abc123",
"type": "function",
"function": { "name": "get_weather", "arguments": "{\"city\": \"北京\"}" }
}]
},
{
"role": "tool",
"tool_call_id": "call_abc123",
"content": "北京,晴,气温 23 摄氏度,湿度 45%。"
}
]
}'

Python(OpenAI SDK,自动完成两轮):

import json
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["ROUTEAPI_KEY"],
base_url="https://api.routeapi.ai/v1",
)
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市的当前天气。城市名使用中文全称。",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名,如 北京"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
},
"required": ["city"],
},
},
}
]
def get_weather(city: str, unit: str = "celsius") -> str:
# 这里替换为真实的天气服务调用
return f"{city},晴,气温 23 摄氏度,湿度 45%。"
messages = [{"role": "user", "content": "北京现在天气怎么样?"}]
# 第一轮
resp = client.chat.completions.create(
model="gpt-5.5", messages=messages, tools=tools
)
msg = resp.choices[0].message
if msg.tool_calls:
# assistant 消息必须原样加回,否则 tool_call_id 无法对齐
messages.append(msg)
for tc in msg.tool_calls:
args = json.loads(tc.function.arguments)
result = get_weather(**args)
messages.append(
{"role": "tool", "tool_call_id": tc.id, "content": result}
)
# 第二轮
resp = client.chat.completions.create(
model="gpt-5.5", messages=messages, tools=tools
)
print(resp.choices[0].message.content)

Node.js(openai 包,自动完成两轮):

import OpenAI from 'openai';
const client = new OpenAI({
apiKey: process.env.ROUTEAPI_KEY,
baseURL: 'https://api.routeapi.ai/v1',
});
const tools = [
{
type: 'function',
function: {
name: 'get_weather',
description: '查询指定城市的当前天气。城市名使用中文全称。',
parameters: {
type: 'object',
properties: {
city: { type: 'string', description: '城市名,如 北京' },
unit: { type: 'string', enum: ['celsius', 'fahrenheit'] },
},
required: ['city'],
},
},
},
];
function getWeather(city, unit = 'celsius') {
// 这里替换为真实的天气服务调用
return `${city},晴,气温 23 摄氏度,湿度 45%。`;
}
const messages = [{ role: 'user', content: '北京现在天气怎么样?' }];
// 第一轮
let resp = await client.chat.completions.create({
model: 'gpt-5.5',
messages,
tools,
});
const msg = resp.choices[0].message;
if (msg.tool_calls) {
messages.push(msg); // assistant 消息原样加回
for (const tc of msg.tool_calls) {
const args = JSON.parse(tc.function.arguments);
const result = getWeather(args.city, args.unit);
messages.push({ role: 'tool', tool_call_id: tc.id, content: result });
}
// 第二轮
resp = await client.chat.completions.create({
model: 'gpt-5.5',
messages,
tools,
});
}
console.log(resp.choices[0].message.content);

把工具当成对内部系统的安全封装。关键:SQL 不应由模型直接生成,而应由模型选参数、你的代码拼接参数化查询,避免注入。

tools = [
{
"type": "function",
"function": {
"name": "query_order",
"description": "根据订单号查询订单状态和金额。",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "订单号,形如 ORD-20260917-001",
}
},
"required": ["order_id"],
},
},
}
]
def query_order(order_id: str) -> str:
# 用参数化查询,绝不把模型输出直接拼进 SQL
row = db.execute(
"SELECT status, amount FROM orders WHERE order_id = %s",
(order_id,),
).fetchone()
if row is None:
return json.dumps({"found": False})
return json.dumps({"found": True, "status": row[0], "amount": row[1]})

工具结果建议回传 JSON 字符串,模型能更稳定地解析结构化字段。

同时声明多个工具,模型按需选择甚至并行调用。这里用天气 + 汇率两个工具:

tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市的当前天气。",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
},
},
{
"type": "function",
"function": {
"name": "get_exchange_rate",
"description": "查询两种货币之间的汇率。",
"parameters": {
"type": "object",
"properties": {
"from_currency": {"type": "string", "description": "如 USD"},
"to_currency": {"type": "string", "description": "如 CNY"},
},
"required": ["from_currency", "to_currency"],
},
},
},
]
dispatch = {
"get_weather": lambda city: f"{city},晴,23 摄氏度。",
"get_exchange_rate": lambda from_currency, to_currency: f"1 {from_currency} = 7.2 {to_currency}",
}
messages = [{"role": "user", "content": "北京天气怎么样?顺便告诉我美元对人民币的汇率。"}]
# 循环处理多轮 / 并行工具调用,设上限防止死循环
for _ in range(5):
resp = client.chat.completions.create(
model="gpt-5.5", messages=messages, tools=tools
)
msg = resp.choices[0].message
messages.append(msg)
if not msg.tool_calls:
break
# 并行调用时 tool_calls 是多元素数组,每个都要回传结果
for tc in msg.tool_calls:
args = json.loads(tc.function.arguments)
result = dispatch[tc.function.name](**args)
messages.append(
{"role": "tool", "tool_call_id": tc.id, "content": result}
)
print(messages[-1]["content"])

工具调用引入了模型侧、你的代码侧、上游服务侧三处可能出错的环节。

模型可能返回不合法的 JSON,或缺少必填参数、传入超出 enum 范围的值。务必:

  • 用 try/catch 包裹 JSON.parse / json.loads。
  • 解析后按 schema 校验必填字段和取值范围。
  • 校验失败时,把错误信息作为工具结果回传,让模型自我纠正,而不是直接抛异常终止对话:
try:
args = json.loads(tc.function.arguments)
city = args["city"] # 校验必填
except (json.JSONDecodeError, KeyError) as exc:
result = f"参数解析失败:{exc}。请重新给出合法的 city 参数。"
else:
result = get_weather(city)
messages.append({"role": "tool", "tool_call_id": tc.id, "content": result})

工具背后是真实服务,可能超时或不可用。给每个工具调用设置超时上限,超时后把结果回传为一条明确的错误说明,让模型改走别的策略:

try:
result = call_service(args, timeout=5)
except TimeoutError:
result = "服务超时,未获取到数据,请稍后重试或改用其他方式。"

不要对有副作用的工具(下单、发邮件)做自动重试,否则可能重复执行。幂等设计或先查后写更安全。

业务层错误(订单不存在、无权限)同样应回传给模型,而不是静默返回空值。回传结构化错误信息,模型能据此给用户合理解释:

messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": json.dumps({"error": "order_not_found", "order_id": order_id}),
})

Claude 格式对应地在 tool_result 块上设 "is_error": true,见 Claude Messages 协议。

  • 工具调用能力、最大工具数、并行调用支持度取决于所选模型,上线前请在测试环境验证。
  • arguments 永远是 JSON 字符串而非对象,务必显式解析。
  • 流式场景务必按 index 累积 arguments 分片,拼接完整后再解析。
  • 明确传入 0 或 false 的可选参数会被视为用户显式设置,不会当作缺省丢弃。
  • 记录每次请求的 request ID、模型 ID、状态码和 token 用量,便于排查。错误结构详见 错误与调试。