跳到內容

工具調用(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 用量,便於排查。錯誤結構詳見 錯誤與調試。