# 工具调用

来源: https://platform.lyrouter.com/ai_gateway/docs/tool-call
更新日期: 2026-10-22

工具调用（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` 判断。

## 定义工具

```json
{
  "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 轮：发送问题和工具定义

```bash
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"]
        }
      }
    }]
  }'
```

模型决定调用工具，返回：

```json
{
  "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 轮相同：

```bash
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"]
        }
      }
    }]
  }'
```

模型根据工具结果回答：

```json
{
  "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 和设置环境变量见[完成第一次模型调用](https://platform.lyrouter.com/ai_gateway/docs/quickstart)。

**工具调用循环**

Python:
```python
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),
            }
        )
```

Node.js:
```js
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` 的一段。

```text
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` 给出完整参数。事件格式见[流式响应](https://platform.lyrouter.com/ai_gateway/docs/streaming)。

## 三种协议的结构对照

| 环节 | 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 的调用与回传：

```json
{"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 的调用与回传：

```json
{"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](https://platform.lyrouter.com/ai_gateway/docs/responses-api)。

## 跨协议转换

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 校验，并检查调用方是否有权执行该操作。有副作用的工具（下单、发邮件、删除数据）先让用户确认，并为每次操作携带幂等标识，避免流式中断后重试造成重复执行。
- **限制循环次数**：为工具循环设置最大轮数和总超时，防止模型反复调用。
- **工具定义保持稳定**：同一会话中不要增删或调整工具的顺序和内容，否则之后的提示词缓存都无法命中，见[降低成本与优化缓存](https://platform.lyrouter.com/ai_gateway/docs/cost-practice)。
- **日志会保存工具调用**：模型返回的工具调用（函数名与参数）在全文日志关闭时也会保存，参数中不要放密码等敏感信息，见[日志内容与数据处理](https://platform.lyrouter.com/ai_gateway/docs/privacy)。

## 常见问题

### 模型没有调用工具，直接给出了回答？

**答：** 先确认所选模型支持工具调用；再检查工具的 `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 消息之后。报错原文由上游返回，在[请求日志](https://platform.lyrouter.com/ai_gateway/docs/logs)详情「原始报文」的「上游响应」中查看。

### 在日志里怎么看工具调用？

**答：** 打开[请求日志](https://platform.lyrouter.com/ai_gateway/docs/logs)详情的「对话」页签，「工具调用」中列出请求定义的工具和 `tool_choice`，以及模型返回的每个调用的函数名与参数。路由到哪个服务商、是否发生协议转换，在「服务商」和「原始报文」的「上游请求」中确认。
