工具调用 函数调用 Agents 多供应商 OpenAI 兼容

跨 OpenAI、Claude 与 Gemini 的可移植工具调用

映射 OpenAI、Claude 与 Gemini 的工具字段,用 JSON Schema 校验参数,并通过网关在多个供应商之间保持一套工具循环。

AveMujica API 3 分钟阅读

可移植工具调用意味着 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 CompletionsClaude 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 块启用时可返回多个函数调用
严格 schemastrict / 结构化输出(供应商特定)原生结构化输出;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 别名运行以下用例:

  1. 不需要工具(纯文本)
  2. 单个必需工具,参数有效
  3. 缺少必填参数
  4. 无效枚举值
  5. 未知工具名称
  6. 并行的两个工具调用(如果产品允许)
  7. 为指定工具强制 tool_choice
  8. 变更型工具的幂等重试

用例 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 别名运行八用例矩阵,并在为该智能体启用多供应商故障转移前修复校验缺口。只有完成这些工作后,才扩大流量。