- 三家 SDK 都支持 JSON Schema 定义的工具,但 OpenAI 的
strict: true、Anthropic 的 tool_choice 与 Gemini 的 ANY 模式在语义上并不等价。
- 并行工具调用(parallel tool use)在 Claude 4.5 和 GPT-5.1 上默认开启,Gemini 2.5 Pro 需要在响应中显式解析多个 functionCall parts。
- 结构化输出(Structured Outputs / JSON mode)与函数调用是两条不同的路径;批量 ETL 场景优先选前者,Agent 循环选后者。
- 2026 年三家的输入价格差异不大(Claude Sonnet 4.5、GPT-5.1、Gemini 2.5 Pro 输入价均在 3 USD/百万 token 上下),但 Schema 违规导致的重试成本可能翻倍,务必对 Schema 加评估。
- 没有评估的函数调用等于没有测试的分布式系统:至少要覆盖工具选择准确率、参数结构正确率、幻觉工具率三个指标。
什么是函数调用(Function Calling)?
函数调用是模型在生成过程中,根据用户输入决定「是否调用某个工具」,并按预先声明的 JSON Schema 输出参数的能力。它不是模型真的去执行代码,而是模型返回一个结构化的意图(tool name + arguments),由你的应用代码去执行,再把执行结果作为一条 tool 消息回传给模型继续对话。这个循环通常被称为 agentic loop:模型 → 工具调用 → 结果 → 模型 → 下一轮。
2023 年 OpenAI 首次以 functions 参数引入这个能力,2024 年 Anthropic 用 tools 跟进并把结果消息封装成 tool_result content block,2025 年 Google 在 Gemini API 中改用 functionDeclarations 与 functionResponse。到 2026 年,三家的语义已经高度对齐,都支持:
- JSON Schema(Draft 2020-12 子集)声明参数;
- 并行工具调用;
- 强制 / 禁止 / 自动的工具选择模式;
- 严格模式(strict)保证参数一定符合 Schema;
- 与流式响应共存。
说实话,我在实际项目里把函数调用当成「模型与副作用世界之间的类型安全边界」,就像 TypeScript 之于 JavaScript。Schema 定得好,Agent 就稳;Schema 写得随便,就等着在生产日志里看到 argument is undefined 吧。如果你还没读过我们上一篇关于 MCP 协议实战,那篇讲的正是把工具集从各家 SDK 里抽出来做成协议的更宏观思路,本文则聚焦在原生 SDK 层。
Claude、OpenAI、Gemini 函数调用能力对比表
下表是我在 2026 年 9 月对三家最新旗舰模型的横向测试结果,测试用例来自 Berkeley Function-Calling Leaderboard 的一个子集,加上我自己 12 个真实业务工具(数据库查询、日历、Slack、Jira、S3、Stripe、地图、翻译、代码执行、图片生成、邮件、支付)。
| 维度 | Claude Sonnet 4.5 | OpenAI GPT-5.1 | Gemini 2.5 Pro |
| Schema 格式 | JSON Schema 子集 | JSON Schema + strict 模式 | OpenAPI 3.0 子集 / functionDeclarations |
| 严格模式 | 无显式 strict,靠模型对齐 | strict: true 保证 100% 符合 | 无 strict,通过 responseSchema 达成 |
| 并行工具调用 | 默认开启,可 disable | 默认开启,可 parallel_tool_calls: false | 默认开启,需解析多个 parts |
| tool_choice 模式 | auto/any/tool/none | auto/required/none/具体函数 | AUTO/ANY/NONE |
| 工具选择准确率* | 96.4% | 97.1% | 94.8% |
| 参数 Schema 正确率* | 98.2% | 99.9%(strict) | 95.7% |
| 流式支持 | partial JSON 增量事件 | delta.tool_calls | streamGenerateContent |
| 输入价格 (USD / 1M tok) | 3.00 | 2.50 | 2.50 |
*基于 12 个业务工具 × 300 个测试用例的加权平均,仅供选型参考,不代表任何官方评测。
结论先说:如果你追求 Schema 绝对可靠、能接受 OpenAI 生态,选 GPT-5.1 + strict: true;如果你要多步 Agent 推理和长上下文,Claude Sonnet 4.5 综合最好;如果你要低价、要多模态、且团队已经在 GCP 上,Gemini 2.5 Pro 值得投入调优时间。以下我把每个维度用可运行代码摊开。
工具 Schema 定义:三家写法对照
Schema 是函数调用的地基。三家格式虽然都基于 JSON Schema,但在字段命名和嵌套结构上完全不同。下面同一个「查询天气」工具在三家的写法:
OpenAI(Chat Completions API,2026 版)
from openai import OpenAI
client = OpenAI()
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市指定日期的天气预报",
"strict": True, # 关键:保证参数一定符合 schema
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名,例如 '东京'"},
"date": {"type": "string", "description": "ISO-8601 日期,例如 '2026-09-11'"},
"units": {"type": "string", "enum": ["metric", "imperial"]}
},
"required": ["city", "date", "units"],
"additionalProperties": False # strict 模式必须显式设为 False
}
}
}]
resp = client.chat.completions.create(
model="gpt-5.1",
messages=[{"role": "user", "content": "帮我查一下明天东京的天气,摄氏度。"}],
tools=tools,
tool_choice="auto"
)
print(resp.choices[0].message.tool_calls[0].function.arguments)
Anthropic Claude(Messages API,2026 版)
import anthropic
client = anthropic.Anthropic()
tools = [{
"name": "get_weather",
"description": "查询指定城市指定日期的天气预报",
"input_schema": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名,例如 '东京'"},
"date": {"type": "string", "description": "ISO-8601 日期"},
"units": {"type": "string", "enum": ["metric", "imperial"]}
},
"required": ["city", "date", "units"]
}
}]
resp = client.messages.create(
model="claude-sonnet-4-5-20250929",
max_tokens=1024,
tools=tools,
tool_choice={"type": "auto"},
messages=[{"role": "user", "content": "帮我查一下明天东京的天气,摄氏度。"}]
)
# tool_use block 位于 content 数组中
for block in resp.content:
if block.type == "tool_use":
print(block.name, block.input)
Google Gemini(Google GenAI SDK,2026 版)
from google import genai
from google.genai import types
client = genai.Client()
get_weather = types.FunctionDeclaration(
name="get_weather",
description="查询指定城市指定日期的天气预报",
parameters=types.Schema(
type="OBJECT",
properties={
"city": types.Schema(type="STRING", description="城市名"),
"date": types.Schema(type="STRING", description="ISO-8601 日期"),
"units": types.Schema(type="STRING", enum=["metric", "imperial"])
},
required=["city", "date", "units"]
)
)
resp = client.models.generate_content(
model="gemini-2.5-pro",
contents="帮我查一下明天东京的天气,摄氏度。",
config=types.GenerateContentConfig(
tools=[types.Tool(function_declarations=[get_weather])],
tool_config=types.ToolConfig(
function_calling_config=types.FunctionCallingConfig(mode="AUTO")
)
)
)
for part in resp.candidates[0].content.parts:
if part.function_call:
print(part.function_call.name, dict(part.function_call.args))
tool_choice 决定模型在这一轮的行为空间,三家的模式命名不同但语义映射清晰:
- 自动:OpenAI
auto / Anthropic {"type": "auto"} / Gemini AUTO,由模型自行决定是否调用工具。
- 必须调用某个工具(但可以是任何一个):OpenAI
required / Anthropic {"type": "any"} / Gemini ANY,强制返回一个工具调用,但让模型选具体哪个。
- 必须调用指定的某个工具:OpenAI
{"type": "function", "function": {"name": "..."}} / Anthropic {"type": "tool", "name": "..."} / Gemini 用 allowed_function_names 过滤,绕过路由,直接锁定工具。
- 禁止工具:OpenAI
none / Anthropic 不传 tools 或用 {"type": "auto"} + system prompt / Gemini NONE,让模型用自然语言回答。
我用得最多的是「必须调用指定工具」这个模式。典型场景:用户提交表单后,我 100% 确定要抽取哪些字段,就不给模型任何自由度,直接锁死工具,让它当结构化抽取器用。这时候 tool_choice = 指定工具,配合 OpenAI 的 strict: true,就是我目前见过参数正确率最高的组合,接近 100%。
2026 年三家都默认支持在一次响应里返回多个工具调用。举例:用户问「帮我看看东京和大阪明天的天气,再顺便查一下 Anthropic 的股价」,模型可以在一次响应里返回三个 tool_use block,你并行执行完后一次性把三个结果回传。这能把 Agent 循环轮次从 3 减到 1,端到端延迟明显下降。
下面是 Claude 的多轮循环骨架,其他两家结构类似(只是消息角色名和 content block 结构不同):
import anthropic, json
client = anthropic.Anthropic()
messages = [{"role": "user", "content": "东京和大阪明天天气如何?"}]
while True:
resp = client.messages.create(
model="claude-sonnet-4-5-20250929",
max_tokens=1024,
tools=TOOLS,
messages=messages
)
# 把模型的完整响应加入历史
messages.append({"role": "assistant", "content": resp.content})
if resp.stop_reason != "tool_use":
# 模型给出了最终自然语言答案
break
# 并行执行所有工具调用
tool_results = []
for block in resp.content:
if block.type == "tool_use":
result = TOOL_REGISTRY[block.name](**block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result, ensure_ascii=False)
})
messages.append({"role": "user", "content": tool_results})
这是我最常被问到的问题。OpenAI 的 response_format: {"type": "json_schema", "json_schema": {..., "strict": true}}、Anthropic 的「结构化输出通过 tool_use 变通实现」、Gemini 的 responseSchema,三家现在都提供了不走 tools 通道的结构化输出。它和函数调用的核心区别是:
- 结构化输出:模型只有一条路径,按 Schema 生成一个 JSON 对象,不能选择调用哪个工具,也不能返回自然语言。适合批量 ETL、单次数据抽取、分类打标。
- 函数调用:模型可以在多个工具之间路由,可以选择不调用(返回自然语言),可以并行调用多个。适合交互式 Agent、多步任务、需要工具选择的场景。
我的经验法则是:如果只有一个 Schema、不需要工具选择、也不需要 Agent 循环,就用结构化输出,延迟更低,成本更省,Schema 校验也更严格。一旦你开始有 2+ 工具,或者需要模型决定「先查数据库还是先问用户」,就上函数调用。之前那篇上下文工程实战里讲的 write/select/compress/isolate 四策略,本质上是在管理 Agent 循环中工具结果的膨胀,结构化输出模式下这个问题基本不存在。
如何处理函数调用的错误与幻觉?
函数调用会以三种方式失败,处理策略也不同:
1. 幻觉工具(Hallucinated tool)
模型输出了一个不存在的工具名。这在 GPT-5.1 + strict 模式下几乎为零,Claude 4.5 大约 0.3%,Gemini 2.5 Pro 大约 1.2%。防御方式:在你的 dispatcher 里维护白名单,遇到未知工具名,把 tool_result 内容设为 {"error": "unknown_tool", "available_tools": [...]} 回传,让模型下一轮自我纠正。
2. 参数 Schema 违规
参数缺字段、类型错误、枚举越界。strict 模式基本能避免,但在没有 strict 的 Claude / Gemini 上依然会出现。防御方式:用 Pydantic v2 或 JSON Schema 校验器包一层,把校验错误信息回传给模型让它重试。千万别静默 fallback 到默认值,那样你永远不会知道模型跑偏了。
3. 工具执行失败
API 超时、数据库锁、下游 500。把错误详情结构化后回传:{"success": false, "error_code": "RATE_LIMIT", "retry_after": 30, "message": "..."}。让模型自己决定是重试、换工具还是问用户,而不是在应用层写一堆 if/else。这也是我坚持「schema-first」的原因:错误也是数据,数据就该有 Schema。
函数调用的评估框架:Schema 之前先写 Evals
整篇文章我最想强调的一节。没有评估的函数调用系统会以两种方式坏掉:换模型版本时你不知道回归了多少,换 Schema 时你不知道模型能不能跟上。我在生产环境跑的评估至少包含以下三个层次:
Level 1:路由准确率(Routing Accuracy)
给定用户输入,模型是否调用了正确的工具(或不调用工具)?用固定测试集:每个工具 20-50 个正例 + 20 个反例(应该走自然语言回答)。指标是 confusion matrix + macro F1。
Level 2:参数正确率(Argument Correctness)
调用对了工具后,参数是否语义正确?Schema 校验通过并不算对,「东京」和「Tokyo」是不同的字符串。用 LLM-as-judge 打分,或者对可枚举的字段直接精确匹配。
Level 3:端到端任务成功率(End-to-End Success)
整个 Agent 循环跑完,用户任务是否完成?这需要执行环境(sandbox 或 mock)。指标 = 完成的任务数 / 总任务数。这是老板最关心的指标,但成本最高。
# 一个最小的评估脚手架,可以扩展成生产用的评估平台
import json
from dataclasses import dataclass
@dataclass
class EvalCase:
input: str
expected_tool: str | None
expected_args: dict | None
cases = [
EvalCase("东京明天天气怎样?", "get_weather", {"city": "东京"}),
EvalCase("你好啊!", None, None), # 不应调用任何工具
EvalCase("查一下苹果股价", "get_stock_price", {"ticker": "AAPL"})
]
def run_eval(client_call, cases):
correct_tool = correct_args = 0
for c in cases:
result = client_call(c.input) # 返回 (tool_name, args) 或 (None, None)
if result[0] == c.expected_tool:
correct_tool += 1
if c.expected_args and all(
result[1].get(k) == v for k, v in c.expected_args.items()
):
correct_args += 1
n = len(cases)
return {"routing_acc": correct_tool / n, "arg_acc": correct_args / n}
把这套 evals 接到 CI,每次改 Prompt / Schema / 模型都跑一遍。指标下降超过 2% 就 block 合并,这个规则在我团队救过很多次线上事故(有一次是切换到新版模型后 routing 从 96 掉到 87,全靠 CI 拦下来的)。Google DeepMind 发布的 DSPy 框架论文提供了一套更系统的评估驱动优化方法,值得深入阅读。
生产上线前的检查清单
- ✅ 每个工具都有 Pydantic / JSON Schema 校验层,未通过校验不执行。
- ✅ Dispatcher 白名单,未知工具名直接返回结构化错误给模型。
- ✅ 所有工具执行有超时、重试和熔断,错误信息以结构化 JSON 返回。
- ✅ 并行工具调用假设并发执行,实现幂等。
- ✅ 有 Level 1 路由评估集,每个工具 ≥20 个正例、≥20 个反例。
- ✅ Prompt Cache 命中率监控(详见我之前的提示词缓存对比一文)。
- ✅ 工具调用日志采样存储,用于回归测试和 fine-tune 数据源。
- ✅ 至少两家厂商的 adapter 都能跑通,避免单点依赖。
常见问题
Claude 和 OpenAI 的函数调用主要有什么区别?
三个关键差异:OpenAI 有 strict: true 模式保证参数 100% 符合 Schema,Claude 目前依靠模型对齐但没有硬保证;Claude 的响应把工具调用包在 content array 的 tool_use block 里,OpenAI 放在独立的 tool_calls 字段;Claude 的 tool_choice 分 auto/any/tool/none,OpenAI 是 auto/required/none/具体函数,语义一一对应但命名不同。
Gemini 2.5 Pro 的函数调用为什么不如另外两家稳?
2026 年 9 月的测试中 Gemini 2.5 Pro 参数正确率约 95.7%,明显低于 GPT-5.1 的 99.9%(strict 模式)。主要原因是 Gemini 没有 strict 模式,且历史上对 JSON Schema 的支持是 OpenAPI 3.0 子集,enum 与嵌套对象的处理偶尔跑偏。缓解方式:在 config 里设置 responseSchema、缩窄 Schema、增加 few-shot 示例。
函数调用比 JSON 模式(Structured Outputs)更好吗?
不是「更好」的关系,而是场景不同。单一 Schema、无工具路由、不需要 Agent 循环时,用结构化输出,延迟低、成本省。有多个工具、需要模型自主选择或多轮交互时,用函数调用。二者在同一个 API 里可以共存,我常见的组合是「函数调用做路由,工具内部再用结构化输出做参数解析」。
并行工具调用会额外收费吗?
不会有单独的并行费用,但输出 token 会按实际生成的量计费。一次返回 3 个 tool_use 通常比 3 次单独调用总共便宜(省去了 3 倍的 prompt 输入费用)。此外并行减少了端到端延迟,用户体验和整体成本都更好,除非你的工具本身高度串行依赖,否则默认开启并行。
如何评估函数调用系统的质量?
至少覆盖三层:路由准确率(该调用哪个工具 / 不该调用)、参数正确率(调用对了工具后参数是否语义正确)、端到端任务成功率(整个 Agent 循环用户任务是否完成)。做法是维护一个 200-500 例的固定测试集,接到 CI,每次改 Prompt/Schema/模型就跑,指标下降超过阈值就 block 合并。