工具调用
工具调用(Function Calling)让模型以结构化参数请求调用你定义的函数:应用在请求中声明工具,模型返回要调用的函数名和参数,应用自己执行函数,再把结果发回给模型,由模型给出最终回答。LYRouter 只负责转发和协议转换,不会执行你的任何业务操作。Chat Completions、Responses、Anthropic Messages 三种接口的工具结构不同,网关会在 Chat Completions 与 Anthropic 协议之间自动转换,本章以 Chat Completions 为主给出完整示例,并说明转换时发生了什么。
工作流程
- 在请求的
tools中定义函数:名称、用途描述、参数的 JSON Schema。 - 模型决定调用工具时,响应的
message.tool_calls中给出每个调用的id、函数名和参数(JSON 字符串),finish_reason一般为tool_calls。 - 应用解析参数,校验后执行函数。
- 把模型返回的 assistant 消息原样追加到
messages,再为每个调用追加一条role: tool消息,tool_call_id填对应调用的id。 - 再次请求。模型根据工具结果回答,或者继续调用工具,直到响应中不再有
tool_calls。
判断是否需要执行工具,请看响应里有没有 tool_calls(Anthropic Messages 中为 tool_use 块),不要只看 finish_reason(stop_reason):例如阿里云百炼在 tool_choice 指定函数时,可能返回 tool_calls 而 finish_reason 为 stop;通过 Anthropic Messages 用 {"type": "tool"} 指定工具时,目前会返回 tool_use 块,但 stop_reason 为 end_turn(any、auto 时正常)。下文的示例代码按 message.tool_calls 判断。
定义工具
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市当前的天气。用户询问天气、气温时调用。",
"parameters": {
"type": "object",
"properties": {
"city": { "type": "string", "description": "城市名,例如 北京" },
"unit": { "type": "string", "enum": ["celsius", "fahrenheit"] }
},
"required": ["city"]
}
}
}
| 字段 | 说明 |
|---|---|
type |
固定为 function |
function.name |
函数名,只用字母、数字、下划线和连字符,同一请求内不重复 |
function.description |
说明什么时候该调用、返回什么。描述越具体,模型越容易正确选择工具 |
function.parameters |
参数的 JSON Schema,顶层为 object。长整数 ID 请定义为 string,原因见下文「跨协议转换」 |
function.strict |
是否严格按 Schema 生成参数,透传给 OpenAI 兼容通道,是否生效取决于模型;Anthropic 协议通道不转发 |
模型必须支持工具调用,在模型广场的模型详情中确认。
完整的两轮示例
第 1 轮:发送问题和工具定义
curl https://api.lyrouter.com/v1/chat/completions \
-H "Authorization: Bearer $LYROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "MODEL_ID",
"messages": [
{"role": "user", "content": "北京现在天气怎么样?"}
],
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市当前的天气。",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]
}
}
}]
}'
模型决定调用工具,返回:
{
"id": "chatcmpl-5d1e8b20a3",
"object": "chat.completion",
"created": 1791950400,
"model": "MODEL_ID",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_8f2a1c",
"type": "function",
"function": { "name": "get_weather", "arguments": "{\"city\":\"北京\"}" }
}
]
},
"finish_reason": "tool_calls"
}
],
"usage": { "prompt_tokens": 96, "completion_tokens": 18, "total_tokens": 114 }
}
arguments 是 JSON 字符串,需要自己解析。content 可能为 null 或空字符串,也可能带一段说明文字。
第 2 轮:发回工具结果
把上一轮的 assistant 消息原样放回,后面接一条 role: tool 消息。tools 保持与第 1 轮相同:
curl https://api.lyrouter.com/v1/chat/completions \
-H "Authorization: Bearer $LYROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "MODEL_ID",
"messages": [
{"role": "user", "content": "北京现在天气怎么样?"},
{"role": "assistant", "content": null, "tool_calls": [
{"id": "call_8f2a1c", "type": "function",
"function": {"name": "get_weather", "arguments": "{\"city\":\"北京\"}"}}
]},
{"role": "tool", "tool_call_id": "call_8f2a1c",
"content": "{\"city\":\"北京\",\"condition\":\"晴\",\"temperature_c\":22}"}
],
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市当前的天气。",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]
}
}
}]
}'
模型根据工具结果回答:
{
"id": "chatcmpl-5d1e8b20b7",
"object": "chat.completion",
"created": 1791950403,
"model": "MODEL_ID",
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": "北京现在晴,气温 22°C。" },
"finish_reason": "stop"
}
],
"usage": { "prompt_tokens": 142, "completion_tokens": 14, "total_tokens": 156 }
}
一次返回多个 tool_calls 时,在 assistant 消息之后为每个调用各追加一条 role: tool 消息,然后再发起下一次请求。
可运行的最小循环
下面的程序把上面两轮写成循环:执行模型请求的工具,直到模型给出最终回答,最多 5 轮。安装 SDK 和设置环境变量见完成第一次模型调用。
import json
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["LYROUTER_API_KEY"],
base_url="https://api.lyrouter.com/v1",
)
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市当前的天气。",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
},
}
]
def get_weather(city):
# 换成真实的天气查询
return {"city": city, "condition": "晴", "temperature_c": 22}
TOOL_FUNCTIONS = {"get_weather": get_weather}
messages = [{"role": "user", "content": "北京现在天气怎么样?"}]
for _ in range(5): # 限制循环次数
response = client.chat.completions.create(
model="MODEL_ID", messages=messages, tools=tools
)
message = response.choices[0].message
if not message.tool_calls:
print(message.content)
break
# 原样追加 assistant 消息(含 tool_calls)
messages.append(message.model_dump(exclude_none=True))
for call in message.tool_calls:
func = TOOL_FUNCTIONS.get(call.function.name)
try:
args = json.loads(call.function.arguments or "{}")
result = func(**args) if func else {"error": "unknown tool"}
except Exception as exc: # 参数不合法或执行失败,把错误告诉模型
result = {"error": str(exc)}
messages.append(
{
"role": "tool",
"tool_call_id": call.id,
"content": json.dumps(result, ensure_ascii=False),
}
)
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.LYROUTER_API_KEY,
baseURL: "https://api.lyrouter.com/v1",
});
const tools = [
{
type: "function",
function: {
name: "get_weather",
description: "查询指定城市当前的天气。",
parameters: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
},
},
},
];
function getWeather({ city }) {
// 换成真实的天气查询
return { city, condition: "晴", temperature_c: 22 };
}
const toolFunctions = { get_weather: getWeather };
const messages = [{ role: "user", content: "北京现在天气怎么样?" }];
for (let turn = 0; turn < 5; turn++) {
const response = await client.chat.completions.create({
model: "MODEL_ID",
messages,
tools,
});
const message = response.choices[0].message;
if (!message.tool_calls?.length) {
console.log(message.content);
break;
}
messages.push(message); // 原样追加 assistant 消息(含 tool_calls)
for (const call of message.tool_calls) {
const fn = toolFunctions[call.function.name];
let result;
try {
const args = JSON.parse(call.function.arguments || "{}");
result = fn ? fn(args) : { error: "unknown tool" };
} catch (err) {
result = { error: String(err) }; // 参数不合法或执行失败,把错误告诉模型
}
messages.push({
role: "tool",
tool_call_id: call.id,
content: JSON.stringify(result),
});
}
}
tool_choice 与并行调用
tool_choice |
含义 | 发往 Anthropic 协议通道时 |
|---|---|---|
不写,或 "auto" |
模型自行决定是否调用 | {"type": "auto"} |
"required" |
必须调用至少一个工具 | {"type": "any"} |
{"type": "function", "function": {"name": "get_weather"}} |
必须调用指定的工具 | {"type": "tool", "name": "get_weather"} |
"none" |
不调用工具 | 不转发,模型仍可能调用工具。不希望调用时,请不要传 tools |
parallel_tool_calls: false要求一次最多返回一个调用,透传给 OpenAI 兼容通道,是否生效取决于模型;发往 Anthropic 协议通道时会被去掉,模型仍可能一次返回多个调用。应用应始终按数组处理tool_calls。- 强制调用(
"required"或指定函数)且请求没有写enable_thinking/thinking_budget时,阿里云百炼通道会被自动加上enable_thinking: false,因为这类上游在思考模式下不接受强制工具调用。
流式下拼接工具参数
流式请求中,工具调用分多个 chunk 到达:第一个分片带 id、type 和函数名,之后的分片只带 arguments 的一段。
data: {"id":"chatcmpl-5d1e8b20a3","object":"chat.completion.chunk","created":1791950400,"model":"MODEL_ID","choices":[{"index":0,"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"id":"call_8f2a1c","type":"function","function":{"name":"get_weather","arguments":""}}]},"finish_reason":null}]}
data: {"id":"chatcmpl-5d1e8b20a3","object":"chat.completion.chunk","created":1791950400,"model":"MODEL_ID","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"city\":"}}]},"finish_reason":null}]}
data: {"id":"chatcmpl-5d1e8b20a3","object":"chat.completion.chunk","created":1791950400,"model":"MODEL_ID","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\"北京\"}"}}]},"finish_reason":null}]}
data: {"id":"chatcmpl-5d1e8b20a3","object":"chat.completion.chunk","created":1791950400,"model":"MODEL_ID","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}]}
data: {"id":"chatcmpl-5d1e8b20a3","object":"chat.completion.chunk","created":1791950400,"model":"MODEL_ID","choices":[],"usage":{"prompt_tokens":96,"completion_tokens":18,"total_tokens":114}}
data: [DONE]
- 以
tool_calls[].index为键分组,同一index的arguments按到达顺序拼接成完整字符串。 id和函数名取第一个非空的值。部分上游会在后续分片里重复发送空字符串的id和name,不要用空值覆盖。- 收到带
finish_reason的 chunk 或流结束后再解析参数 JSON,中途的片段不是合法 JSON。finish_reason不一定是"tool_calls",见上文「工作流程」。 - Anthropic Messages 中对应的是
content_block_start(tool_use块,带id和name)和多个input_json_delta,按块的index拼接partial_json;Responses 中是response.output_item.added(function_call项)和response.function_call_arguments.delta,response.function_call_arguments.done给出完整参数。事件格式见流式响应。
三种协议的结构对照
| 环节 | Chat Completions | Responses | Anthropic Messages |
|---|---|---|---|
| 定义工具 | {"type": "function", "function": {"name", "description", "parameters"}} |
{"type": "function", "name", "description", "parameters"},没有 function 包裹层 |
{"name", "description", "input_schema"} |
| 模型发起调用 | message.tool_calls[]:id、function.name、function.arguments(JSON 字符串) |
output[] 中 type: "function_call" 的项:call_id、name、arguments(JSON 字符串) |
content[] 中 type: "tool_use" 的块:id、name、input(JSON 对象) |
| 表示需要工具结果 | message.tool_calls 非空(finish_reason 一般为 "tool_calls") |
output 中有 function_call 项 |
content 中有 tool_use 块(stop_reason 一般为 "tool_use") |
| 发回结果 | {"role": "tool", "tool_call_id": "...", "content": "..."} |
在 input 中追加 {"type": "function_call_output", "call_id": "...", "output": "..."} |
在 role: "user" 消息中放 {"type": "tool_result", "tool_use_id": "...", "content": "..."} 块 |
| 对应关系 | tool_call_id = tool_calls[].id |
call_id = function_call.call_id |
tool_use_id = tool_use.id |
| 强制调用 | "required" / 指定 function.name |
"required" / {"type": "function", "name": "..."} |
{"type": "any"} / {"type": "tool", "name": "..."} |
| 禁止并行 | parallel_tool_calls: false |
parallel_tool_calls: false |
tool_choice.disable_parallel_tool_use: true |
Responses 的调用与回传:
{"type": "function_call", "id": "fc_68f1b0a1", "call_id": "call_8f2a1c", "name": "get_weather", "arguments": "{\"city\":\"北京\"}", "status": "completed"}
{"type": "function_call_output", "call_id": "call_8f2a1c", "output": "{\"condition\":\"晴\",\"temperature_c\":22}"}
Anthropic Messages 的调用与回传:
{"role": "assistant", "content": [{"type": "tool_use", "id": "toolu_01A09q90qw90lq917835lq9", "name": "get_weather", "input": {"city": "北京"}}]}
{"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_01A09q90qw90lq917835lq9", "content": "{\"condition\":\"晴\",\"temperature_c\":22}"}]}
Responses 请求由网关原样转发,不做工具结构的转换。用 previous_response_id 接续时,网关只用它识别会话,接续请求不一定发往创建上一条响应的上游,详见 Responses。
跨协议转换
Chat Completions 请求由 Anthropic 协议通道服务、或 Anthropic Messages 请求由 OpenAI 兼容通道服务时,网关转换工具相关的结构:
| 方向 | 网关的处理 |
|---|---|
| Chat Completions → Anthropic 协议通道 | tools[].function 转为 name、description、input_schema,只转换 type: "function" 的工具,strict 不转发;历史中的 tool_calls[].function.arguments 解析为 input 对象,不是合法 JSON 时按 {} 发送;每条 role: tool 消息转为一条带 tool_result 块的 user 消息;tool_choice 按上表转换,parallel_tool_calls 去掉。响应中的 tool_use 块转回 tool_calls,stop_reason: "tool_use" 转为 finish_reason: "tool_calls" |
| Anthropic Messages → OpenAI 兼容通道 | tools 转为 type: "function" 的工具;tool_use 块转为 tool_calls,input 序列化为 arguments 字符串;tool_result 块转为 role: tool 消息,is_error 不转发;tool_choice 的 auto、any、tool 分别转为 "auto"、"required"、指定函数,{"type": "none"} 和 disable_parallel_tool_use 不转发。响应中的 tool_calls 转回 tool_use 块,参数不是合法 JSON 时 input 为 {};stop_reason 按上游的 finish_reason 换算(tool_calls 转为 tool_use,stop 转为 end_turn),与有没有 tool_calls 无关 |
另外,网关在发出请求前还会:
- 固定 JSON 键序:把发往上游的请求体中所有 JSON 对象的键按固定顺序重新排列(数组顺序不变),并对历史消息中
tool_calls[].function.arguments、Responsesinput中function_call项的arguments字符串里的 JSON 做同样的处理。客户端每轮把同一个参数对象写成不同键序时,上游看到的字节仍然相同,提示词缓存不会因此失效。参数不是合法 JSON 对象、或含有连续 15 位以上数字时保持原样。这一处理只影响发往上游的请求,不改变返回给你的内容。 - 数字精度:网关解析并重新编码 JSON 时,数字最多保留 14 位有效数字。Anthropic Messages 的
input对象、以及跨协议转换时的工具参数都会经过解析,19 位的订单号、雪花 ID 等长整数会变成科学计数法,请在参数 Schema 中定义为string。 - 思考内容:发往 OpenAI 兼容通道前,历史 assistant 消息上的
reasoning_content会被删除。DeepSeek 系列例外:思考模式要求回传当前轮的思考内容,所以最后一条 user 消息之后的 assistant 消息上非空的reasoning_content会保留;DeepSeek 仍因缺少它返回 400 时,网关补上空值后在同一通道重试一次。由开启思考的 Anthropic 协议通道服务时,非流式响应的message带有reasoning_blocks(LYRouter 扩展字段),把 assistant 消息原样放回,网关才能把带签名的思考块交还上游。
注意事项
- 工具结果和补充说明分开发送:通过 Anthropic Messages 调用时,把
tool_result块和给模型的补充文字放在两条消息里,不要放进同一条 user 消息,以便在转换到 OpenAI 兼容通道时完整保留。 - 执行失败也要回传:工具出错时,把错误信息写进结果内容(例如
{"error": "城市不存在"}),让模型据此调整;tool_result的is_error标记在转换到 OpenAI 兼容通道时不会保留。 - 校验后再执行:模型生成的参数可能不完整或不合法,执行前按 Schema 校验,并检查调用方是否有权执行该操作。有副作用的工具(下单、发邮件、删除数据)先让用户确认,并为每次操作携带幂等标识,避免流式中断后重试造成重复执行。
- 限制循环次数:为工具循环设置最大轮数和总超时,防止模型反复调用。
- 工具定义保持稳定:同一会话中不要增删或调整工具的顺序和内容,否则之后的提示词缓存都无法命中,见降低成本与优化缓存。
- 日志会保存工具调用:模型返回的工具调用(函数名与参数)在全文日志关闭时也会保存,参数中不要放密码等敏感信息,见日志内容与数据处理。
常见问题
模型没有调用工具,直接给出了回答?
答: 先确认所选模型支持工具调用;再检查工具的 description 是否说明了什么情况下调用。需要确保调用时,把 tool_choice 设为 "required" 或指定函数。
强制调用工具时,上游返回 400?
答: 部分模型在思考模式下不接受强制工具调用,例如阿里云百炼返回 The tool_choice parameter does not support being set to required or object in thinking mode。不写思考参数时,网关会为阿里云百炼通道自动关闭思考;如果请求中自己写了 enable_thinking: true 或 thinking_budget,请去掉,或把 tool_choice 改为 "auto"。
第 2 轮请求报错,提示找不到工具调用 ID 或消息顺序不对?
答: 检查三点:assistant 消息是否原样放回且带 tool_calls;每个调用是否都有一条 role: tool 消息,tool_call_id 与调用的 id 一致;role: tool 消息是否紧跟在对应的 assistant 消息之后。报错原文由上游返回,在请求日志详情「原始报文」的「上游响应」中查看。
在日志里怎么看工具调用?
答: 打开请求日志详情的「对话」页签,「工具调用」中列出请求定义的工具和 tool_choice,以及模型返回的每个调用的函数名与参数。路由到哪个服务商、是否发生协议转换,在「服务商」和「原始报文」的「上游请求」中确认。