工具调用(Tool Calling)
工具调用(Tool Calling,也叫 Function Calling)让模型不再只是生成文本,而是在需要时返回一个结构化的「函数调用请求」。你的程序执行真实逻辑(查天气、查数据库、下单),把结果回传给模型,模型再据此生成最终回复。RouteAPI 在 /v1/chat/completions(OpenAI 格式)和 /v1/messages(Claude 格式)上都支持工具调用。
1. 工具调用概述
Section titled “1. 工具调用概述”什么是工具调用
Section titled “什么是工具调用”工具调用是一次多轮往返:
- 你在请求里声明一组「工具」(每个工具有名字、描述和参数 schema)。
- 模型判断是否需要某个工具,如果需要,返回一个带参数的调用请求,而不是直接回答。
- 你的代码解析参数、执行真实逻辑、拿到结果。
- 你把结果回传给模型,模型生成面向用户的自然语言回复。
模型本身不会执行任何代码,它只负责决定「调用哪个工具」和「传什么参数」。真正的执行永远发生在你这一侧,这也意味着安全边界(权限校验、参数过滤)由你控制。
| 场景 | 说明 |
|---|---|
| 实时数据查询 | 天气、汇率、股价、库存等模型训练数据里没有的信息 |
| 内部系统集成 | 查数据库、调内部 API、读订单状态 |
| 执行动作 | 下单、发邮件、创建工单等有副作用的操作 |
| 结构化抽取 | 强制模型按固定 schema 输出,等价于一种结构化输出手段 |
| 智能体编排 | Agent 框架用工具调用驱动多步任务 |
工具调用能力取决于所选模型。主流模型(OpenAI GPT 系列、Claude 系列、Gemini 系列等)大多支持,但最大工具数、是否支持并行调用等细节各不相同。模型列表接口不返回工具调用能力字段,请查阅模型供应商文档,或直接用目标模型发一次带 tools 的请求验证,详见 Models。
2. 工具定义格式(OpenAI)
Section titled “2. 工具定义格式(OpenAI)”在 /v1/chat/completions 中,工具通过顶层 tools 数组声明,每个工具是一个 type: "function" 的嵌套对象。
tools 数组结构
Section titled “tools 数组结构”{ "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"] } } } ]}function schema
Section titled “function schema”| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
type | string | 是 | 固定为 "function" |
function.name | string | 是 | 工具名,只能包含字母、数字、下划线和连字符 |
function.description | string | 建议 | 工具用途说明,模型据此决定是否调用 |
function.parameters | object | 否 | 参数定义,标准 JSON Schema |
description 的质量直接决定模型是否会正确选用工具。写清用途、每个参数的含义、取值范围和边界条件,比调整任何采样参数都更有效。
parameters(JSON Schema)
Section titled “parameters(JSON Schema)”parameters 使用标准 JSON Schema 描述参数结构:
type: 通常是"object"。properties: 每个参数的类型、描述、枚举值。required: 必填参数名列表。enum: 限定取值范围,能显著降低模型传错值的概率。
没有参数的工具也要给出空 schema:"parameters": { "type": "object", "properties": {} }。
3. 工具定义格式(Claude)
Section titled “3. 工具定义格式(Claude)”在 /v1/messages 中,工具定义是平铺结构,没有外层 type 和 function 包装,参数字段名叫 input_schema。
tools 数组差异
Section titled “tools 数组差异”{ "tools": [ { "name": "get_weather", "description": "查询指定城市的当前天气。城市名使用中文全称。", "input_schema": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名,如 北京" }, "unit": { "type": "string", "enum": ["celsius", "fahrenheit"] } }, "required": ["city"] } } ]}input_schema
Section titled “input_schema”input_schema 的内部结构和 OpenAI 的 parameters 完全一致,都是标准 JSON Schema。差异只在外层包装。
两种格式对比
Section titled “两种格式对比”| 对比项 | OpenAI(/v1/chat/completions) | Claude(/v1/messages) |
|---|---|---|
| 外层包装 | { "type": "function", "function": {...} } | 直接平铺,无包装 |
| 工具名字段 | function.name | name |
| 描述字段 | function.description | description |
| 参数字段 | function.parameters | input_schema |
| 参数 schema | 标准 JSON Schema | 标准 JSON Schema(一致) |
| 模型返回 | message.tool_calls 数组 | content 里的 tool_use 块 |
| 结果回传角色 | 独立的 role: "tool" 消息 | user 消息里的 tool_result 块 |
| 结果关联字段 | tool_call_id | tool_use_id |
Claude 协议的完整细节(含 is_error、流式 input_json_delta 等)见 Claude Messages 协议。本页后续示例默认以 OpenAI 格式为主。
4. tool_choice 选项
Section titled “4. tool_choice 选项”tool_choice 控制模型如何选择工具。
| 值 | 行为 |
|---|---|
"auto" | 模型自行决定是否调用工具,以及调用哪个。有 tools 时的默认值 |
"none" | 禁止调用任何工具,模型只输出文本 |
"required" | 必须调用至少一个工具,但由模型选择调用哪个 |
{ "type": "function", "function": { "name": "get_weather" } } | 强制调用指定工具 |
auto(默认)
Section titled “auto(默认)”{ "tool_choice": "auto" }最常用。适合「用户问题有时需要工具、有时直接回答」的通用场景。
{ "tool_choice": "none" }临时关闭工具但保留定义。常用于「先让模型总结,不要再调工具」的收尾阶段。
required
Section titled “required”{ "tool_choice": "required" }强制模型走工具路径。适合结构化抽取这类「必须产出结构化结果」的场景。
指定特定工具
Section titled “指定特定工具”{ "tool_choice": { "type": "function", "function": { "name": "get_weather" } }}Claude 格式的对应写法是 { "type": "tool", "name": "get_weather" },required 对应 { "type": "any" },none 对应 { "type": "none" }。
5. 工具调用流程
Section titled “5. 工具调用流程”一次完整的工具调用至少包含两轮请求。
第一轮:模型返回 tool_calls
Section titled “第一轮:模型返回 tool_calls”发送带 tools 的请求:
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 摄氏度"第二轮:传递工具结果
Section titled “第二轮:传递工具结果”把第一轮的 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": []}模型生成最终响应
Section titled “模型生成最终响应”第二轮请求返回自然语言结果,finish_reason 恢复为 stop:
{ "choices": [ { "message": { "role": "assistant", "content": "北京现在是晴天,气温 23 摄氏度,湿度 45%,比较舒适。" }, "finish_reason": "stop" } ]}6. 多轮工具调用
Section titled “6. 多轮工具调用”连续调用多个工具
Section titled “连续调用多个工具”模型可能需要多轮工具调用才能完成任务:先查订单号,再用订单号查物流。每一轮都遵循「模型返回 tool_calls → 执行 → 回传结果」的循环,直到 finish_reason 变回 stop。生产代码应写成循环,并设置最大轮数上限防止无限循环:
MAX_TURNS = 5for _ 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, })并行工具调用
Section titled “并行工具调用”一轮里模型可能同时请求多个互不依赖的工具(如同时查北京和上海的天气),tool_calls 会是一个多元素数组。你需要为每一个 tool_call 都追加一条对应的 role: "tool" 消息,tool_call_id 一一对齐,缺一条下一轮就会报错。
如果想禁用并行、强制模型一次只调一个工具,OpenAI 格式加 "parallel_tool_calls": false,Claude 格式在 tool_choice 里加 "disable_parallel_tool_use": true。
7. 流式工具调用
Section titled “7. 流式工具调用”设置 stream: true 时,工具调用参数是逐片增量返回的。
流式输出中的 tool_calls
Section titled “流式输出中的 tool_calls”每个 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]如何处理增量 delta
Section titled “如何处理增量 delta”按 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 协议。
8. 各模型支持情况
Section titled “8. 各模型支持情况”支持工具调用的模型
Section titled “支持工具调用的模型”RouteAPI 聚合的多数主流模型都支持工具调用,包括 OpenAI GPT 系列、Claude 系列、Gemini 系列等。先用模型列表接口确认模型在你账户下可用:
curl https://api.routeapi.ai/v1/models \ -H "Authorization: Bearer $ROUTEAPI_KEY"该接口只返回模型是否可用及可用端点,不返回工具调用能力。是否支持工具调用请以供应商文档或一次实际带 tools 的请求为准。
各模型的限制
Section titled “各模型的限制”不同模型在以下维度存在差异,请在测试环境验证后再上线:
| 维度 | 说明 |
|---|---|
| 最大工具数 | 单次请求可声明的工具数量上限因模型而异 |
| 并行调用 | 部分模型不支持一轮返回多个 tool_calls |
tool_choice 支持度 | required / 指定特定工具并非所有模型都支持 |
| 参数复杂度 | 深层嵌套或超大 JSON Schema 可能被部分模型截断或忽略 |
| 流式增量粒度 | arguments 分片方式在不同模型间不一致,务必按 index 累积 |
9. 完整示例
Section titled “9. 完整示例”天气查询工具示例(端到端)
Section titled “天气查询工具示例(端到端)”以下三段代码功能等价:定义工具 → 第一轮拿到 tool_calls → 执行 → 第二轮回传结果 → 拿到最终回复。
curl(手动完成两轮):
# 第一轮:发起带工具的请求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 jsonimport osfrom 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);数据库查询工具示例
Section titled “数据库查询工具示例”把工具当成对内部系统的安全封装。关键: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 字符串,模型能更稳定地解析结构化字段。
多工具编排示例
Section titled “多工具编排示例”同时声明多个工具,模型按需选择甚至并行调用。这里用天气 + 汇率两个工具:
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"])10. 错误处理
Section titled “10. 错误处理”工具调用引入了模型侧、你的代码侧、上游服务侧三处可能出错的环节。
工具参数验证失败
Section titled “工具参数验证失败”模型可能返回不合法的 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})工具执行超时
Section titled “工具执行超时”工具背后是真实服务,可能超时或不可用。给每个工具调用设置超时上限,超时后把结果回传为一条明确的错误说明,让模型改走别的策略:
try: result = call_service(args, timeout=5)except TimeoutError: result = "服务超时,未获取到数据,请稍后重试或改用其他方式。"不要对有副作用的工具(下单、发邮件)做自动重试,否则可能重复执行。幂等设计或先查后写更安全。
工具返回错误
Section titled “工具返回错误”业务层错误(订单不存在、无权限)同样应回传给模型,而不是静默返回空值。回传结构化错误信息,模型能据此给用户合理解释:
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 用量,便于排查。错误结构详见 错误与调试。