工具调用

工具调用(Function Calling)让模型以结构化参数请求调用你定义的函数:应用在请求中声明工具,模型返回要调用的函数名和参数,应用自己执行函数,再把结果发回给模型,由模型给出最终回答。LYRouter 只负责转发和协议转换,不会执行你的任何业务操作。Chat Completions、Responses、Anthropic Messages 三种接口的工具结构不同,网关会在 Chat Completions 与 Anthropic 协议之间自动转换,本章以 Chat Completions 为主给出完整示例,并说明转换时发生了什么。

工作流程

  1. 在请求的 tools 中定义函数:名称、用途描述、参数的 JSON Schema。
  2. 模型决定调用工具时,响应的 message.tool_calls 中给出每个调用的 id、函数名和参数(JSON 字符串),finish_reason 一般为 tool_calls。
  3. 应用解析参数,校验后执行函数。
  4. 把模型返回的 assistant 消息原样追加到 messages,再为每个调用追加一条 role: tool 消息,tool_call_id 填对应调用的 id。
  5. 再次请求。模型根据工具结果回答,或者继续调用工具,直到响应中不再有 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),
            }
        )

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、Responses input 中 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,以及模型返回的每个调用的函数名与参数。路由到哪个服务商、是否发生协议转换,在「服务商」和「原始报文」的「上游请求」中确认。