# 工具调用

> OpenAI Chat & Responses Function Tools / Anthropic Tool Use / Gemini Function Calling 跨 SDK 对照,含内置工具的模型限制

来源：https://zhonkemodel.dflop.top/docs/guides/tool-calling

> 让模型决定**何时**调用你的代码逻辑 —— 查天气 / 查数据库 / 调用其他 API / 控制软件 ...

本平台在 4 个聊天协议端点(OpenAI Chat / OpenAI Responses / Anthropic Messages / Gemini Native)上都支持 function / tool 调用,但**字段名跟 schema 形状不同**。本页给跨 SDK 对照与最佳实践。

## 概念对照

| 概念 | OpenAI Chat | Anthropic | Gemini |
|---|---|---|---|
| 工具列表字段 | `tools` | `tools` | `tools` |
| 单工具 wrapper | `{type:"function","function":{...}}` | 直接 `{...}` (不包 wrapper) | `{functionDeclarations:[...]}` |
| 参数 schema 字段 | `function.parameters` | `input_schema` | `parameters` |
| 模型选择策略 | `tool_choice` | `tool_choice` | `toolConfig.functionCallingConfig` |
| 模型工具调用响应 | `message.tool_calls[i]` | `content[i] type:tool_use` | `parts[i].functionCall` |
| 工具结果回填 | `role:"tool"` + `tool_call_id` | `content type:tool_result` + `tool_use_id` | `parts[i].functionResponse` |

## OpenAI Chat —— Function Tools

### 1. 定义工具

```python
tools = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "Get the current weather in a city",
        "parameters": {
            "type": "object",
            "properties": {
                "city": {"type": "string", "description": "City name"},
                "unit": {"type": "string", "enum": ["c", "f"], "default": "c"},
            },
            "required": ["city"],
        },
    },
}]
```

### 2. 发起调用

```python
from openai import OpenAI
client = OpenAI(api_key="sk-gpushare-xxx", base_url="https://zhonkezhonkeapi.dflop.top/v1")

messages = [{"role": "user", "content": "Weather in Tokyo?"}]
resp = client.chat.completions.create(
    model="claude-sonnet-4-6",
    messages=messages,
    tools=tools,
)
```

### 3. 处理模型工具调用

```python
import json

choice = resp.choices[0]
if choice.finish_reason == "tool_calls":
    # assistant 消息(含全部 tool_calls)只 append 一次。
    # 放进循环会在多工具调用时产生 [assistant, tool, assistant, tool] 非法序列,
    # 协议要求所有 tool 结果紧跟在唯一一条 assistant(tool_calls) 消息之后。
    messages.append(choice.message)

    for tc in choice.message.tool_calls:
        args = json.loads(tc.function.arguments)
        result = get_weather(**args)  # 你的真实函数

        # 每个 tool_call 各回填一条 role:"tool" 结果
        messages.append({
            "role": "tool",
            "tool_call_id": tc.id,
            "content": json.dumps(result),
        })

    # 再发一轮拿最终回答
    final = client.chat.completions.create(
        model="claude-sonnet-4-6",
        messages=messages,
        tools=tools,
    )
    print(final.choices[0].message.content)
```

## Anthropic Messages —— Tool Use

### 1. 定义工具

```python
tools = [{
    "name": "get_weather",
    "description": "Get the current weather in a city",
    "input_schema": {
        "type": "object",
        "properties": {
            "city": {"type": "string"},
            "unit": {"type": "string", "enum": ["c", "f"]},
        },
        "required": ["city"],
    },
}]
```

### 2. 发起调用

```python
from anthropic import Anthropic
client = Anthropic(api_key="sk-gpushare-xxx", base_url="https://zhonkezhonkeapi.dflop.top")

messages = [{"role": "user", "content": "Weather in Tokyo?"}]
resp = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    tools=tools,
    messages=messages,
)
```

### 3. 处理模型工具调用

```python
import json

if resp.stop_reason == "tool_use":
    tool_blocks = [b for b in resp.content if b.type == "tool_use"]
    tool_results = []
    for tb in tool_blocks:
        result = get_weather(**tb.input)
        tool_results.append({
            "type": "tool_result",
            "tool_use_id": tb.id,
            "content": json.dumps(result),
        })

    messages.append({"role": "assistant", "content": resp.content})
    messages.append({"role": "user", "content": tool_results})

    final = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        tools=tools,
        messages=messages,
    )
```

## Gemini Native —— Function Calling

### 1. 定义工具

```python
from google.genai import types

weather_tool = types.Tool(function_declarations=[
    types.FunctionDeclaration(
        name="get_weather",
        description="Get the current weather in a city",
        parameters={
            "type": "object",
            "properties": {
                "city": {"type": "string"},
                "unit": {"type": "string", "enum": ["c", "f"]},
            },
            "required": ["city"],
        },
    )
])
```

### 2. 发起调用

```python
from google import genai
client = genai.Client(api_key="sk-gpushare-xxx", http_options={"base_url": "https://zhonkezhonkeapi.dflop.top"})

response = client.models.generate_content(
    model="gemini-2.5-pro",
    contents="Weather in Tokyo?",
    config=types.GenerateContentConfig(tools=[weather_tool]),
)
```

### 3. 处理模型工具调用

```python
candidate = response.candidates[0]
function_calls = [p.function_call for p in candidate.content.parts if p.function_call]

if function_calls:
    # 第二轮 contents: 原始提问 + 模型的 functionCall 轮(原样放回) + functionResponse parts
    contents = [
        types.Content(role="user", parts=[types.Part.from_text(text="Weather in Tokyo?")]),
        candidate.content,  # 模型上一轮(含 functionCall part),必须一并回传
    ]
    response_parts = [
        types.Part.from_function_response(
            name=fc.name,
            response={"result": get_weather(**dict(fc.args))},
        )
        for fc in function_calls
    ]
    contents.append(types.Content(role="user", parts=response_parts))

    final = client.models.generate_content(
        model="gemini-2.5-pro",
        contents=contents,
        config=types.GenerateContentConfig(tools=[weather_tool]),
    )
    print(final.text)
```

> Gemini 回填最容易写错的两点: (1) `functionResponse` 必须用 `types.Part.from_function_response` 包装,`response` 字段是 dict; (2) 模型上一轮的 `functionCall` content 必须原样放回 `contents`,否则模型不知道自己调过什么。Gemini Native 通道是字节透传,回填形状完全由客户端负责。

## OpenAI Responses (`/v1/responses`) —— Function Tools

Codex CLI 等 Responses-API 客户端走 `POST /v1/responses`,function tools 同样可用,但**字段形状与 Chat Completions 不同**:

- 工具定义**没有 `function` wrapper** —— `name` / `description` / `parameters` 直接平铺
- 模型的调用以 `function_call` output item 返回(不是 `message.tool_calls`)
- 结果以 `function_call_output` input item 回填(不是 `role:"tool"` 消息)

```python
import json
from openai import OpenAI
client = OpenAI(api_key="sk-gpushare-xxx", base_url="https://zhonkezhonkeapi.dflop.top/v1")

tools = [{
    "type": "function",
    "name": "get_weather",          # 平铺,没有 function:{...} 包装
    "description": "Get the current weather in a city",
    "parameters": {
        "type": "object",
        "properties": {"city": {"type": "string"}},
        "required": ["city"],
    },
}]

resp = client.responses.create(model="gpt-5.5", input="Weather in Tokyo?", tools=tools)

input_items = [{"role": "user", "content": "Weather in Tokyo?"}]
for item in resp.output:
    if item.type == "function_call":
        result = get_weather(**json.loads(item.arguments))
        input_items.append(item)  # 模型的 function_call item 原样放回
        input_items.append({
            "type": "function_call_output",
            "call_id": item.call_id,
            "output": json.dumps(result),
        })

final = client.responses.create(model="gpt-5.5", input=input_items, tools=tools)
print(final.output_text)
```

## 跨厂商 + 工具调用

本平台协议翻译层让你**用任意 SDK 调任意模型**,工具同样跨厂商可用:

```python
# 用 OpenAI SDK 调 Claude 做工具调用
from openai import OpenAI
client = OpenAI(api_key="sk-gpushare-xxx", base_url="https://zhonkezhonkeapi.dflop.top/v1")

resp = client.chat.completions.create(
    model="claude-sonnet-4-6",  # Claude 走 T1 翻译
    messages=[{"role": "user", "content": "Weather in Tokyo?"}],
    tools=tools,  # 同上文「OpenAI Chat —— 1. 定义工具」的 tools 定义
)
# resp.choices[0].message.tool_calls 跟纯 OpenAI 一致
```

翻译路径的响应 header 会加 `X-Protocol-Translation: openai_chat_to_anthropic_messages`,你可以据此知晓走了哪条路径;若有能力在翻译中被降级或丢弃(例如 `cache_control` 受限),还会附 `X-Protocol-Warning` 列出具体说明 —— 调试「为什么我的工具没生效」时先看它。注意:路由选中**原生协议渠道**时这两个 header 都不出现,header 缺失是正常现象,不代表请求异常。

## 高级技巧

### 强制工具调用

```python
# OpenAI: tool_choice 指定具体工具
tool_choice = {"type": "function", "function": {"name": "get_weather"}}

# Anthropic: tool_choice 限制必须用工具
tool_choice = {"type": "tool", "name": "get_weather"}

# Gemini: function_calling_config mode = ANY
config = types.GenerateContentConfig(
    tool_config=types.ToolConfig(
        function_calling_config=types.FunctionCallingConfig(mode="ANY")
    )
)
```

### 多个工具

各协议 SDK 都支持单次请求多个工具,模型可能一次性返回多个调用:

```python
# OpenAI 一次 response 可能有 multiple tool_calls
for tc in resp.choices[0].message.tool_calls:
    handle(tc)
```

### Stream 模式下的工具

工具调用**可以**配合 `stream=true`。增量 chunk 里的 `delta.tool_calls` 是字段级累加的(`function.arguments` 一次发一片),客户端需要拼接所有 chunk 才能拿到完整 JSON。

## 注意事项

1. **工具 schema 校验严格** —— `parameters` 必须是合法 JSON Schema,模型按它生成 arguments
2. **arguments 是字符串** (OpenAI) / 是对象 (Anthropic / Gemini) —— 客户端解析方式不同
3. **不要在工具结果里返回长 base64 图像** —— 会被算进下一轮 input tokens,极易爆账
4. **内置工具** (`web_search` / `image_generation`) 不需要你定义 schema,但**必须配 `stream=true`**(详见 [流式响应](./streaming.md)),且**仅部分模型可用** —— 见 [模型列表](../reference/models.md) 的能力列:
   - `web_search`: GPT (`gpt-5.5`)、Claude(翻译为 `web_search_20250305`)、Gemini(翻译为 `googleSearch`)系列支持;GLM / DeepSeek / Grok 等其它模型默认不支持
   - `image_generation`(对话内置出图): 仅 `gpt-5.5`;独立出图请走按张计费的 `/v1/images/generations`,见 [图像 / 视频 / 音乐 API](../reference/media-apis.md)
5. **不支持的 tool type 一律 400** —— 在不支持的模型/渠道上带内置工具,返回 400 `tool_not_supported`;`function` / `web_search` / `image_generation` 之外的 tool type(如从 OpenAI 官方迁移带来的 `file_search` / `code_interpreter`)也会被 400 拒绝,错误信息形如 `Tool type 'file_search' is not supported`。各错误码详见 [错误码](../reference/errors.md)
