函数调用深入:OpenAI、Anthropic、Gemini 本节摘要:三家前沿厂商在 2024 年收敛到了同一个工具调用循环,然后又在其他一切上分道扬镳。OpenAI 用 和 ;Anthropic 用 与 块;Gemini 用 加唯一 id 关联。本节把三者并排 diff,让你写在一个厂商上的代码,移植到别的厂商时不会崩——同一个循环,不同的声明信封、不同的参数类型约定(字符串 vs 对象)、不同的关联机制。我们会建一个把三种格式统一成一份规范工具声明的翻译器,并标注每家真正会撞上的硬限制(工具数、schema 深度、参数长度)。 学习目标 阅读完本节,你应当能够: 说清 OpenAI、Anthropic、Gemini 三家函数调用 payload 的三处形状差异(声明、调用、结果)。
本节摘要:三家前沿厂商在 2024 年收敛到了同一个工具调用循环,然后又在其他一切上分道扬镳。OpenAI 用
tools和tool_calls;Anthropic 用tool_use与tool_result块;Gemini 用functionDeclarations加唯一 id 关联。本节把三者并排 diff,让你写在一个厂商上的代码,移植到别的厂商时不会崩——同一个循环,不同的声明信封、不同的参数类型约定(字符串 vs 对象)、不同的关联机制。我们会建一个把三种格式统一成一份规范工具声明的翻译器,并标注每家真正会撞上的硬限制(工具数、schema 深度、参数长度)。
阅读完本节,你应当能够:
tool_choice 强制 / 禁止 / 自动选择工具调用。函数调用请求的形状,因厂商而异。三个 2026 年生产栈里的具体例子:
OpenAI Chat Completions / Responses API:你传 tools: [{type: "function", function: {name, description, parameters, strict}}]。响应里 choices[0].message.tool_calls: [{id, type: "function", function: {name, arguments}}],其中 arguments 是一个你必须自己解析的 JSON 字符串。strict 模式(strict: true)靠受限解码强制 schema 合规。
Anthropic Messages API:你传 tools: [{name, description, input_schema}]。响应以 content: [{type: "text"}, {type: "tool_use", id, name, input}] 形式返回,input 已被解析过(对象而非字符串)。你用一条新的 user 消息回复,内含 {type: "tool_result", tool_use_id, content} 块。
Google Gemini API:你传 tools: [{functionDeclarations: [{name, description, parameters}]}](嵌在 functionDeclarations 下)。响应以 candidates[0].content.parts: [{functionCall: {name, args, id}}] 抵达,其中 id 在 Gemini 3 及以上版本里唯一,用于并行调用关联。你用 {functionResponse: {name, id, response}} 回复。
同一个循环,不同的字段名、不同的嵌套、不同的字符串-vs-对象约定、不同的关联机制。一个在 OpenAI 上写好天气 Agent 的团队,光是把管线移植到 Anthropic 就要花两天,再花一天到 Gemini。
本节建一个翻译器:把三种格式统一成一份规范工具声明,在边缘处路由。第 17 节会把这个模式泛化成 LLM 网关。
每家厂商都需要五样东西:
| 方面 | OpenAI | Anthropic | Gemini |
|---|---|---|---|
| 声明信封 | {type:"function", function:{...}} |
{name, description, input_schema} |
{functionDeclarations:[{...}]} |
| Schema 字段名 | parameters |
input_schema |
parameters |
| 响应容器 | 助手消息上的 tool_calls[] |
content[] 中类型为 tool_use 的块 |
parts[] 中类型为 functionCall 的条目 |
| 参数类型 | JSON 字符串 | 已解析对象 | 已解析对象 |
| id 格式 | call_...(OpenAI 生成) |
toolu_...(Anthropic) |
UUID(Gemini 3+) |
| 结果块 | role tool,带 tool_call_id |
user 消息含 tool_result、tool_use_id |
functionResponse,带匹配 id |
| 强制某工具 | tool_choice:{type:"function",function:{name}} |
tool_choice:{type:"tool",name} |
tool_config:{function_calling_config:{mode:"ANY"}} |
| 禁止工具 | tool_choice:"none" |
tool_choice:{type:"none"} |
mode:"NONE" |
| strict schema | strict:true |
schema 即契约(始终强制) | 请求级 responseSchema |
$ref、禁止有重叠的 oneOf/anyOf/allOf、要求每个属性都列在 required 里。tool_choice 的三种模式每家都支持,但叫法不同:
外加每家独有的一个模式:
disable_parallel_tool_use 标志区分单调用与多调用。mode:"VALIDATED" 把每个响应都过一遍 schema 校验器,不管模型意图。OpenAI 的 parallel_tool_calls:true(默认)在一条助手消息里发出多个调用,你全部跑完后用一条批量的 tool 角色消息回复,每个 tool_call_id 一条。Anthropic 历史上只做单调用;disable_parallel_tool_use:false(Claude 3.5 起默认)启用多调用。Gemini 2 允许并行调用但不给稳定 id;Gemini 3 加了 UUID,使乱序响应能干净关联。
三家的流式工具调用线格式各异:
tool_calls[i].function.arguments 的增量 delta 分块抵达,你累加直到 finish_reason:"tool_calls"。input_json_delta 分块携带部分参数。streamFunctionCallArguments(Gemini 3 新增)发射带 functionCallId 的分块,使多个并行调用能交错。第 03 节深入并行 + 流式的重组,本节聚焦声明与单调用形状。
非法参数的错误形态也不同:
arguments:"{bad json}",你的 JSON 解析失败,你注入错误信息再重调。refusal。input 可能含未声明字段;schema 是建议性的,需服务端校验。enum 会被静默忽略,需自行校验。你代码里的一份规范工具声明长这样(形状由你定):
from dataclasses import dataclass @dataclass class Tool: name: str description: str input_schema: dict strict: bool = True # 三个小函数把它翻译成三家厂商的形状 def to_openai(t: Tool) -> dict: return {"type": "function", "function": { "name": t.name, "description": t.description, "parameters": t.input_schema, "strict": t.strict}} def to_anthropic(t: Tool) -> dict: return {"name": t.name, "description": t.description, "input_schema": t.input_schema} # 无 strict 标志 def to_gemini(t: Tool) -> dict: return {"functionDeclarations": [{ "name": t.name, "description": t.description, "parameters": t.input_schema}]} # OpenAPI 3.0 子集 # 一个 canonical_call 把三家的响应都抽成 {id, name, args} def canonical_call(resp: dict, provider: str) -> dict: if provider == "openai": c = resp["tool_calls"][0] return {"id": c["id"], "name": c["function"]["name"], "args": json.loads(c["function"]["arguments"])} # 字符串→对象 if provider == "anthropic": b = next(b for b in resp["content"] if b["type"] == "tool_use") return {"id": b["id"], "name": b["name"], "args": b["input"]} # 已是对象 if provider == "gemini": p = resp["parts"][0]["functionCall"] return {"id": p["id"], "name": p["name"], "args": p["args"]}
code/main.py 里的脚手架正是这么做的,然后把一个假工具调用在每家的响应形状里往返一遍——无需联网,本节教的是形状,不是 HTTP。
生产团队把这个翻译器包成 AbstractToolset(Pydantic AI)、UniversalToolNode(LangGraph)或 BaseTool(LlamaIndex)。第 17 节会交付一个网关,在三者任一之前暴露一个 OpenAI 形状的 API。
| 维度 | OpenAI | Anthropic | Gemini |
|---|---|---|---|
| 参数类型 | JSON 字符串(需自解析) | 已解析对象 | 已解析对象 |
| 关联 id 格式 | call_... |
toolu_... |
UUID |
| strict 强制 | 显式 strict:true |
始终强制 | 请求级 responseSchema |
| 默认并行 | 开 | 开(3.5 起) | 开(3 起带稳定 id) |
| schema 方言 | JSON Schema 2020-12(strict 下受限) | JSON Schema | OpenAPI 3.0 子集 |
💡 移植心法:写一份规范
Tool,用三个翻译器输出;响应侧用一个canonical_call收口。这样换厂商只改边缘路由,业务逻辑不动——这是第 17 节网关的设计原型。
本节产出 outputs/skill-provider-portability-audit.md——一个可移植性审计技能。给定一段针对某家厂商的函数调用集成,它产出审计报告:依赖了哪些厂商限制、哪些字段需要改名、移植到其他两家时各自会断在哪里。
跑翻译器:运行 code/main.py,验证三份厂商声明 JSON 序列化自同一个底层 Tool 对象。给规范工具加一个 enum 参数,确认只有 Gemini 翻译器需要处理 OpenAPI 怪癖。
写 list 响应解析器:为每家写一个 ListToolsResponse 解析器,提取模型在 list_tools 或发现调用后返回的工具列表。OpenAI 原生没有,记录这一不对称。
实现 tool_choice 转换:把规范的 ToolChoice(mode="force", tool_name="x") 映射到三家形状;再映射 mode="any" 与 mode="none",对照本节 diff 表。
读一家的文档:挑一家,从头到尾读它的函数调用指南,找一个它支持而另两家不支持的 schema 字段。候选:OpenAI strict、Anthropic disable_parallel_tool_use、Gemini function_calling_config.allowed_function_names。
写错误测试向量:构造一个参数违反声明 schema 的工具调用,过每家的校验器(第 01 节的标准库版可作代理),记录各自报什么错,并写下「生产里你会选哪家来保严格性」。
$ref;Anthropic 64 工具、无 strict 标志但契约式;Gemini 64 函数、OpenAPI 3.0 子集。input_json_delta、Gemini streamFunctionCallArguments 带 id。Tool + 三个翻译器 + 一个 canonical_call,是第 17 节网关的设计原型。下一节,我们深入并行工具调用与流式重组——多个调用乱序返回时如何按 id 关联,以及如何把分块的
arguments流正确拼回。