Function Calling 是 AI Agent 的心脏——模型再聪明,工具调不好就是废的。
上周生产环境出了个诡异的 bug:同一套 Tool Schema,GPT-5.4 调得好好的,Claude 4.6 偶尔漏参数,Gemini 2.5 直接把返回的 JSON 搞坏了。排查两天才定位到根因——三家对 required 字段和嵌套对象的解析逻辑压根不一样。
踩完这个坑我就不淡定了。虽然各家都管自己叫"Function Calling",但底层实现差得远。索性花一周设计了一套标准化评测,拿 Claude Opus 4.6、GPT-5.4、Gemini 2.5 Pro 做了 900 个 case 的横向对比。
评测方案设计
六个维度,900 个用例
为了覆盖实际 Agent 开发中的典型场景,测试拆成六个维度:
| 维度 | 具体测试内容 | 用例数 |
|---|---|---|
| 基础调用 | 单工具、意图明确 | 200 |
| 参数补全 | 用户信息不全,模型需推理填参 | 200 |
| 多工具串联 | 单次请求涉及 2-3 个工具协作 | 150 |
| 嵌套结构 | 参数含 array、嵌套 object | 150 |
| 意图拒绝 | 用户意图不匹配任何工具定义 | 100 |
| 异常恢复 | 工具报错后模型的重试行为 | 100 |
每个模型各跑 3 轮,取平均值。
统一的工具定义
三家都用 OpenAI JSON Schema 格式定义工具,通过各自 SDK 做转换。测试工具集包含电商搜索、订单创建、天气查询三个典型场景:
import openai
client = openai.OpenAI(
base_url="https://api.ofox.ai/v1", # 统一走这个,方便切模型
api_key="sk-xxx"
)
tools = [
{
"type": "function",
"function": {
"name": "search_products",
"description": "搜索商品信息",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "搜索关键词"},
"category": {
"type": "string",
"enum": ["electronics", "clothing", "food"]
},
"price_range": {
"type": "object",
"properties": {
"min": {"type": "number", "description": "最低价(元)"},
"max": {"type": "number", "description": "最高价(元)"}
},
"description": "价格区间,包含 min 和 max 两个字段"
},
"sort_by": {
"type": "string",
"enum": ["price", "rating", "sales"]
}
},
"required": ["query"]
}
}
},
{
"type": "function",
"function": {
"name": "create_order",
"description": "创建订单",
"parameters": {
"type": "object",
"properties": {
"product_id": {"type": "string"},
"quantity": {"type": "integer", "minimum": 1},
"shipping_address": {
"type": "object",
"properties": {
"city": {"type": "string"},
"street": {"type": "string"},
"zip": {"type": "string"}
},
"required": ["city", "street"]
}
},
"required": ["product_id", "quantity", "shipping_address"]
}
}
},
{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取天气信息",
"parameters": {
"type": "object",
"properties": {
"location": {"type": "string"},
"date": {"type": "string", "format": "date"}
},
"required": ["location"]
}
}
}
]
结果:Claude 和 GPT 并驾齐驱,Gemini 掉队
综合得分
| 模型 | 基础调用 | 参数补全 | 多工具串联 | 嵌套结构 | 意图拒绝 | 异常恢复 | 加权综合 |
|---|---|---|---|---|---|---|---|
| Claude Opus 4.6 | 99.5% | 94.2% | 91.3% | 96.7% | 97.0% | 88.3% | 94.5% |
| GPT-5.4 | 99.0% | 96.1% | 93.7% | 91.2% | 95.0% | 92.7% | 94.6% |
| Gemini 2.5 Pro | 98.5% | 90.8% | 86.4% | 84.3% | 93.0% | 85.0% | 89.7% |
总分 Claude 和 GPT 几乎打平(差 0.1%),但偏科方向完全不同。
逐模型拆解
Claude 4.6:保守但精准
Claude 的策略是"宁可留空,也不瞎填"。用户说"帮我搜便宜的耳机",它的调用是这样的:
{
"name": "search_products",
"arguments": {
"query": "耳机",
"sort_by": "price"
}
}
注意它没猜 category 也没编 price_range,留给工具用默认值处理。多数场景这是正确选择,但偶尔显得不够"聪明"。
嵌套对象是 Claude 的绝对强项。 shipping_address 之类的多层 JSON,Claude 格式正确率 96.7%,遥遥领先。
但它有个坑值得注意:tool_choice: "auto" 模式下,大约 3% 的概率,Claude 该调工具的时候选择了纯文本回复。 在多工具场景更明显。生产环境关键路径建议强制 tool_choice: "required"。
GPT-5.4:编排王者,嵌套翻车
GPT 在多工具并行调度上真的强。"查一下北京明天天气,顺便搜搜适合出门穿的外套"——GPT 稳定地同时触发两个调用:
[
{
"name": "get_weather",
"arguments": {"location": "北京", "date": "2026-03-26"}
},
{
"name": "search_products",
"arguments": {"query": "户外外套", "category": "clothing"}
}
]
同一场景 Claude 有 8.7% 的概率退化成串行——先查天气,拿到结果,再搜商品。功能上没毛病,但多一轮 round-trip,延迟翻倍。
GPT 的雷区在嵌套对象。 price_range 这种 object 字段,大约 8.8% 的概率被序列化成字符串:
{
"price_range": "{\"min\": 100, \"max\": 500}"
}
如果你的解析层没做兼容处理,直接炸。后面会讲怎么修。
Gemini 2.5 Pro:基础够用,复杂拉胯
简单的单工具场景 Gemini 没问题。但嵌套对象和多工具编排一上强度,差距就出来了。
最头疼的是 JSON 格式硬伤,15.7% 的嵌套对象输出有语法错误:
{
"shipping_address": {
"city": "上海",
"street": "南京路100号"
}, // ← 多了这个尾逗号
}
生产环境这就是直接 crash。
另一个毛病是"废话前置"——该直接调工具的地方,Gemini 会先输出一段分析文字,然后才给 tool_call。解析时需要额外处理。
工程层面的实战方案
踩完坑不能白踩,以下是我在项目里用的几个防御性写法。
1. Schema 描述越详细越好
别偷懒只写 "description": "价格范围",改成这样:
{
"description": "价格区间对象,包含最低价和最高价,单位人民币元。示例:{\"min\": 100, \"max\": 500}",
"type": "object",
"properties": {
"min": {"type": "number", "description": "最低价格(元)"},
"max": {"type": "number", "description": "最高价格(元)"}
}
}
就这一个改动,GPT 的嵌套序列化 bug 从 8.8% 降到 2.1%。
2. 参数解析兜底层
import json
import re
def safe_parse_arguments(raw_args):
"""兼容各家模型的参数格式差异"""
if isinstance(raw_args, dict):
# 检查是否有被序列化成 string 的嵌套对象
for key, val in raw_args.items():
if isinstance(val, str) and val.startswith('{'):
try:
raw_args[key] = json.loads(val)
except json.JSONDecodeError:
pass
return raw_args
if isinstance(raw_args, str):
try:
return json.loads(raw_args)
except json.JSONDecodeError:
# 修复 Gemini 常见的尾逗号问题
cleaned = re.sub(r',\s*}', '}', raw_args)
cleaned = re.sub(r',\s*]', ']', cleaned)
return json.loads(cleaned)
return raw_args
3. System Prompt 里声明并行策略
加一段简单的指令,效果显著:
当用户请求涉及多个工具调用时:
- 工具间无数据依赖 → 并行调用
- 后一个工具依赖前一个结果 → 串行调用并说明原因
Claude 加上之后,不必要的串行调用减少了约 60%。
4. 带反馈的重试机制
MAX_RETRIES = 2
async def call_with_retry(client, messages, tools):
for attempt in range(MAX_RETRIES + 1):
response = await client.chat.completions.create(
model="claude-opus-4-6",
messages=messages,
tools=tools
)
tool_calls = response.choices[0].message.tool_calls
if tool_calls and validate_tool_calls(tool_calls, tools):
return tool_calls
# 告诉模型哪里出了问题,让它自己修
error_msg = get_validation_error(tool_calls)
messages.append({
"role": "tool",
"content": f"参数格式错误: {error_msg},请修正后重新调用"
})
raise ToolCallError("超过最大重试次数")
费用对比
900 用例 × 3 轮的实际开销:
| 模型 | 输入 tokens | 输出 tokens | 基础费用 | 含重试总费用 | 重试溢价 |
|---|---|---|---|---|---|
| Claude Opus 4.6 | 2.1M | 380K | $41.7 | $43.2 | +3.6% |
| GPT-5.4 | 2.0M | 410K | $38.5 | $39.8 | +3.4% |
| Gemini 2.5 Pro | 2.1M | 450K | $22.3 | $26.1 | +17.0% |
Gemini 单价确实便宜,但重试导致的隐性成本达 17%。综合考虑稳定性的话,便宜不等于划算。
测试过程中涉及频繁切换三家模型,我是通过 ofox 一个 endpoint 统一管理的,省得分别维护三套 key 和 SDK,对这种横评场景确实方便。
选型建议
| 场景 | 推荐模型 | 原因 |
|---|---|---|
| 客服 / 对话 Agent | Claude 4.6 | 拒绝调用精准,不会误操作 |
| 数据处理流水线 | GPT-5.4 | 多工具并行调度省时间 |
| 复杂嵌套参数 | Claude 4.6 | JSON 格式正确率最高 |
| 简单单工具 | 三家差不多 | 基础能力差距 <1% |
| 预算敏感 | 按实际重试率算 | Gemini 便宜但算上重试未必省 |
小结
2026 年 3 月的 Function Calling 现状:
- Claude 4.6 和 GPT-5.4 是第一梯队,总分差 0.1%,但偏科明显——Claude 稳在结构化输出,GPT 强在并行编排
- Gemini 2.5 Pro 简单场景 OK,复杂 Agent 场景需要额外的兜底代码
- 没有万能选择——根据你的工具复杂度和调用模式选模型,比追排行榜有用
建议在你自己的真实工具集上跑一轮测试再做决定。模型行为和 schema 写法、prompt 风格强相关,公开 benchmark 不等于你的 benchmark。
最后更新:2026-03-25