跨 OpenAI、Claude 与 Gemini 的可移植工具调用
映射 OpenAI、Claude 与 Gemini 的工具字段,用 JSON Schema 校验参数,并通过网关在多个供应商之间保持一套工具循环。
可移植工具调用意味着 agent 只定义一次工具——通常采用 OpenAI Chat Completions 的 tools 形态——并能通过 OpenAI、Claude、Gemini 或其它供应商的模型调用这些工具,而不必在应用代码中维护三套分叉 schema 方言。可靠做法不是「指望每个原生 SDK 都一致」,而是标准化一种客户端契约,在运行时校验参数,并把模型推理路由到可在边缘映射供应商差异的 OpenAI 兼容网关。
本文聚焦于字段映射、返回的工具调用循环以及不兼容矩阵。更广泛的 MCP 治理、鉴权、日志平台和多跳工具路由,应放在专门文章中,例如 MCP 服务器的 LLM 供应商路由 和 通过 MCP 网关路由智能体工具。
为什么工具 schema 会变成多供应商问题
函数调用创造了第二个 API 面:工具接口。这个接口应该比任何单一模型品牌存活得更久。原生供应商格式仍然不同:
- OpenAI 在 函数调用指南 中记录工具、
tool_choice和工具调用消息。 - Anthropic 原生 Messages API 使用 tool-use / tool-result 内容块;其 OpenAI SDK 兼容 路径接受 OpenAI 风格客户端用于评估,但对大多数应用而言,这不是 Anthropic 长期生产建议。
- Gemini 在 Google AI 文档中说明函数调用,并提供 OpenAI 兼容端点 给现有 OpenAI 客户端。
若每个 agent 微服务说不同原生方言,平台会堆积 N 个转换器、N 种日志格式与 N 类失败模式。可移植性从为产品代码选定一种方言开始。
字段映射速查表
产品代码保持使用 OpenAI Chat Completions 方言。网关或适配器大致按下表映射;具体名称可能随版本变化,依赖边界情况前应重新验证:
| 概念 | OpenAI Chat Completions | Claude Messages(原生) | Gemini(函数调用 / OpenAI 兼容) |
|---|---|---|---|
| 工具目录 | tools[].type=function + function.{name,description,parameters} | 带 name、description、input_schema 的 tools[] | function_declarations / 兼容端点上的 OpenAI tools |
| 强制或自动 | tool_choice:none / auto / required / 指定工具 | Claude 特定形态的 tool_choice | 供应商 tool_config 或兼容层上的 OpenAI 风格 tool_choice |
| 模型请求调用 | message.tool_calls[],含 id、function.name、function.arguments(字符串化 JSON) | content[] 中 type=tool_use 的块,含 id、name、input(对象) | functionCall 部分或兼容层上的 OpenAI 形态 tool_calls |
| 应用返回结果 | role=tool 消息 + tool_call_id + content 字符串 | 引用 tool_use_id 的 tool_result 块 | 函数响应部分 / 兼容层上的 OpenAI role=tool |
| 并行调用 | 多个 tool_calls 条目 | 多个 tool_use 块 | 启用时可返回多个函数调用 |
| 严格 schema | strict / 结构化输出(供应商特定) | 原生结构化输出;OpenAI 兼容层忽略 strict | 检查当前 Gemini 结构化输出文档;不要假定支持 OpenAI strict |
实用规则:即使网关在上游使用原生 Claude 或 Gemini,也要在进入产品代码时归一化为 OpenAI 形态。这样日志和评估工具只需比较一种结构。
保持可移植的 JSON Schema
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather for a city",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "minLength": 1},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
},
"required": ["city"],
"additionalProperties": false
}
}
}
保持名称稳定、描述精确、枚举精简。当每次供应商实验都把 city 改成 location「因为提示词感觉更好」时,可移植性就死了。
带真实校验的参考客户端循环
仅调用 json.loads 不等于校验。在任何副作用发生前,请使用工具名称白名单与 JSON Schema(或 Pydantic):
from openai import OpenAI
import json
from typing import Any, Callable
# pip install jsonschema
from jsonschema import Draft202012Validator
client = OpenAI(
api_key="GATEWAY_API_KEY",
base_url="https://your-gateway.example.com/v1",
)
TOOL_PARAMS = {
"get_weather": {
"type": "object",
"properties": {
"city": {"type": "string", "minLength": 1},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
},
"required": ["city"],
"additionalProperties": False,
}
}
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather for a city",
"parameters": TOOL_PARAMS["get_weather"],
},
}
]
def get_weather(city: str, unit: str = "celsius") -> dict:
return {"city": city, "unit": unit, "temperature": 22}
HANDLERS: dict[str, Callable[..., dict]] = {
"get_weather": get_weather,
}
def parse_and_validate(name: str, raw_arguments: str) -> dict[str, Any]:
if name not in HANDLERS or name not in TOOL_PARAMS:
raise ValueError(f"tool_not_allowed:{name}")
try:
args = json.loads(raw_arguments or "{}")
except json.JSONDecodeError as exc:
raise ValueError(f"invalid_json:{name}") from exc
if not isinstance(args, dict):
raise ValueError(f"arguments_not_object:{name}")
Draft202012Validator(TOOL_PARAMS[name]).validate(args)
return args
MODEL = "claude-sonnet" # gateway alias; swap to gemini-flash or gpt-* without code rewrites
messages = [{"role": "user", "content": "Weather in Lyon in celsius?"}]
response = client.chat.completions.create(
model=MODEL,
messages=messages,
tools=tools,
tool_choice="auto",
)
msg = response.choices[0].message
messages.append(msg)
if msg.tool_calls:
for call in msg.tool_calls:
try:
args = parse_and_validate(call.function.name, call.function.arguments)
result = HANDLERS[call.function.name](**args)
except Exception as exc:
result = {"error": "tool_validation_failed", "detail": str(exc)}
messages.append(
{
"role": "tool",
"tool_call_id": call.id,
"content": json.dumps(result),
}
)
final = client.chat.completions.create(
model=MODEL,
messages=messages,
tools=tools,
)
print(final.choices[0].message.content)
只更换模型别名即可试用 Gemini 或 OpenAI。工具执行器和 JSON Schema 保持不变。这就是可移植性的核心收益。
测试中应断言的响应片段
成功的第一跳应具有以下结构,字段已缩略:
{
"choices": [
{
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_123",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\":\"Lyon\",\"unit\":\"celsius\"}"
}
}
]
}
}
]
}
测试工具应断言:工具名称位于白名单中,arguments 是表示 JSON 对象的字符串,schema 校验通过,并且第二跳以同一个 tool_call_id 收到 role=tool。
不兼容矩阵(务必测试,不要假定)
| 失败模式 | 你会看到什么 | 安全处理方式 |
|---|---|---|
| 近似 JSON 参数 | 尾随逗号、单引号 | 拒绝;返回工具错误内容;不要 eval |
| 缺少必填字段 | schema 校验错误 | 返回结构化工具错误;允许模型重试一次 |
| 未知工具名称 | 模型编造 getWeather | 白名单未命中;绝不执行 |
空 arguments | 必填字段存在时返回 "" 或 {} | 校验失败 |
| 并行扇出 | 一轮返回 5 个以上工具调用 | 限制并发,尤其是变更操作 |
忽略 tool_choice=required | 返回文本而不是工具调用 | 副作用必须发生时视为任务失败 |
Claude 兼容层上的 strict=true | 按 Anthropic 文档被静默忽略 | 始终本地校验 |
| 流式的部分工具参数 | 参数字符串被分段 | 缓冲到最终工具调用事件 |
| 供应商拒绝工具 | 只返回安全或策略文本 | 失败关闭;不要在应用代码中伪造工具调用 |
| 幂等重试 | 同一个 create_ticket 执行两次 | 在工具层使用去重键 |
自动执行 schema 检查。不要把主观质量评分混入可移植性门禁。
更换供应商时仍然会坏的地方
参数合法性
模型可能输出近似 JSON、错误枚举或缺少必填字段。可移植系统在副作用前用同一 JSON Schema 校验。不要因为「模型通常遵守」就在未校验参数上执行支付或写入工具。
并行工具调用
有些模型在一轮中发出多个 tool_calls。运行时必须安全并行执行,或按明确顺序串行。对启用的每个供应商测试多调用提示。
tool_choice 语义
auto、none 与强制函数选择并非处处相同实现。若工作流必须调用工具,用确切模型别名在 CI 中断言该行为。
严格结构化工具 vs 自由文本
模型可能用自然语言回答而不是调用工具。产品策略应决定这是否可接受。对结账或基础设施 agent,把缺失工具调用视为失败。
流式 + 工具
流式输出部分工具参数比流式散文更难。若 UI 需要 token 流与工具,验证具体供应商/网关组合,而不是假设与非工具聊天对等。
副作用幂等
超时后的重试可能重复执行工具。可移植工具平台需要对任何非只读工具使用幂等键。没有幂等的供应商故障转移会让客户被扣两次费。
网关对可移植工具的职责
AI 网关应当:
- 接受一种入口工具方言,通常是 OpenAI tools
- 映射到 Claude、Gemini 或 OpenAI 上游,而不强迫每个应用学习原生形态
- 发布稳定模型别名,使工具测试工具不必写死供应商快照 ID
- 生成包含模型别名和工具轮次结束原因的使用日志
- 强制密钥作用域,避免批处理密钥调用大量变更型智能体
架构选型(代理还是网关)见 LLM 代理与 AI 网关。Claude 和 Gemini 的 SDK 配置分别见 用 OpenAI SDK 调用 Claude 与 用 OpenAI SDK 调用 Gemini。
八用例可移植性矩阵
通过同一个网关客户端,对 OpenAI、Claude 和 Gemini 别名运行以下用例:
- 不需要工具(纯文本)
- 单个必需工具,参数有效
- 缺少必填参数
- 无效枚举值
- 未知工具名称
- 并行的两个工具调用(如果产品允许)
- 为指定工具强制
tool_choice - 变更型工具的幂等重试
用例 3–5 和 8 的通过标准应是校验或执行器行为,而不是“模型道歉得很礼貌”。
保持工具可移植的设计规则
- 一名一义。 不要让
search覆盖无关系统。 - 偏好无聊 JSON。 嵌套联合与巨型自由文本会降低跨模型可靠性。
- 显式版本化工具。
create_ticket_v2好过静默改变含义。 - 检索工具与变更工具分开。 对变更施加更严格鉴权与幂等。
- 限制扇出。 无限递归工具循环是在等一次模型更新变成成本与安全事故。
- 带脱敏记录工具 I/O。 没有证据无法调试可移植性,也不能把含密钥的证据直接外发。
OWASP LLM 指引在此相关:过度代理与敏感数据暴露往往最先出现在启用工具的系统,而不是普通聊天。
curl 冒烟测试
curl https://your-gateway.example.com/v1/chat/completions \
-H "Authorization: Bearer $GATEWAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gemini-flash",
"messages": [{"role": "user", "content": "What is the weather in Lyon?"}],
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather for a city",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]
}
}
}],
"tool_choice": "auto"
}'
检查 choices[0].message.tool_calls。对 Claude 与 OpenAI 别名重复。是否调用工具的差异是产品信号;响应包络形态差异是网关映射 bug。
官方来源
最近核对:2026-07-15。扩大 agent 副作用前请按供应商复核 tool_choice、并行工具调用与严格 schema 支持。
常见问题
每个团队都要学习各供应商原生工具格式吗?
产品应用代码不需要。保持一套入口 schema——通常是 OpenAI tools——并把供应商特性隔离在网关或单一适配库中。
OpenAI 兼容工具调用对生产 agent 够吗?
它是强有力的传输与 schema 策略,但生产仍需要校验、授权、幂等、预算与可读审计日志。兼容不能取代这些控制。
MCP 能否取代可移植函数调用?
MCP 是互补的:它标准化工具如何暴露给客户端,而模型供应商仍需要一种请求工具调用的方式。许多技术栈会两者并用。拓扑细节见 agent/MCP 路由文章。
在多供应商故障转移前运行矩阵
取一套生产工具 schema,通过 /v1/chat/completions 对 OpenAI、Claude 和 Gemini 别名运行八用例矩阵,并在为该智能体启用多供应商故障转移前修复校验缺口。只有完成这些工作后,才扩大流量。