ツール呼び出し Function calling エージェント マルチプロバイダ OpenAI 互換

OpenAI・Claude・Gemini 横断のポータブルツール呼び出し

OpenAI、Claude、Gemini のツールフィールドを対応付け、JSON Schema で引数を検証し、ゲートウェイ経由で一つのツールループを維持する方法を解説します。

AveMujica API 3 分で読めます

ポータブルなツール呼び出しとは、エージェントがツールを一度だけ—通常は OpenAI Chat Completions の tools 形式で—定義し、アプリケーションコード内に分岐した三つのスキーマ方言を持たずに、OpenAI、Claude、Gemini などのモデルからそのツールを呼び出せることです。これを確実に実現する方法は、「すべてのネイティブ SDK が一致することを期待する」ことではありません。一つのクライアント契約に標準化し、副作用の前に引数を検証し、境界でプロバイダ差をマッピングする OpenAI 互換ゲートウェイ経由で推論をルーティングします。

この記事では、フィールドの対応関係、返却されたツール呼び出しのループ、非互換性マトリクスに焦点を当てます。より広範な MCP ガバナンス、認証、ログ基盤、複数ホップのツールルーティングは、MCP サーバーの LLM プロバイダールーティング や MCP ゲートウェイによるエージェントツールルーティング などの専用記事で扱います。

なぜツールスキーマがマルチプロバイダ問題になるのか

Function calling は、ツールインターフェースという第 2 の API サーフェスを生みます。このインターフェースは、どの単一モデルブランドよりも長く使えるべきです。しかし、プロバイダのネイティブ形式には依然として違いがあります。

  • OpenAI は function calling ガイド で、tools、tool_choice、ツール呼び出しメッセージを説明しています。
  • Anthropic のネイティブ Messages API は tool-use / tool-result コンテンツブロックを使用します。OpenAI SDK 互換 経路は評価用途で OpenAI 形式のクライアントを受け付けますが、大半のアプリケーションに対する Anthropic の長期的な本番推奨ではありません。
  • Gemini は Google AI ドキュメントで function calling を説明し、既存の OpenAI クライアント向けに OpenAI 互換エンドポイント も提供しています。

各エージェントマイクロサービスが異なるネイティブ方言を使うと、プラットフォームには N 個の変換器、N 個のログ形式、N 個の失敗モードが蓄積します。ポータビリティは、製品コードで使う方言を一つに決めることから始まります。

フィールド対応早見表

製品コードでは OpenAI Chat Completions 方言を維持します。ゲートウェイやアダプターは、おおむね次のようにマッピングすると考えてください(名称はバージョンによって変わるため、境界事例に依存する前に確認してください)。

概念OpenAI Chat CompletionsClaude Messages(ネイティブ)Gemini(function calling / 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
モデルが呼び出しを要求id、function.name、文字列化された JSON の function.arguments を持つ message.tool_calls[]id、name、オブジェクト形式の input を持つ type=tool_use の content[] ブロックfunctionCall パート、または互換経路上の OpenAI 形式 tool_calls
アプリが結果を返すrole=tool + tool_call_id + 文字列 content のメッセージtool_use_id を参照する tool_result ブロックfunction response パート / 互換経路上の OpenAI role=tool
並列呼び出し複数の tool_calls エントリ複数の tool_use ブロック有効な場合は複数の function call
厳格なスキーマ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
    }
  }
}

名前は安定させ、説明は正確に、enum は小さく保ちます。プロバイダ実験のたびに「その方がプロンプトの感じが良いから」と 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 オブジェクトの文字列であること、スキーマ検証に通ること、第 2 ホップが同じ tool_call_id を持つ role=tool を受け取ることを検証してください。

非互換性マトリクス(仮定せずテストする)

失敗モード観測される内容安全な処理
ほぼ JSON の引数末尾カンマ、シングルクォート拒否してツールエラー内容を返す。eval は使わない
必須フィールドの欠落スキーマ検証エラー構造化されたツールエラーを返し、モデルの再試行は 1 回に限る
未知のツール名モデルが getWeather を作り出す許可リスト不一致として、決して実行しない
空の argumentsフィールド必須時に "" または {}検証失敗として扱う
並列ファンアウト1 ターンに 5 個以上のツール呼び出し同時実行数を制限する。特に変更操作では必須
tool_choice=required が無視されるツール呼び出しではなくテキスト応答副作用が必須ならタスク失敗として扱う
Claude 互換で strict=trueAnthropic のドキュメントどおり黙って無視される常にローカルで検証する
ストリーミング中の部分的なツール引数分割された引数文字列最終ツール呼び出しイベントまでバッファする
プロバイダがツールを拒否safety / policy テキストのみフェイルクローズする。アプリコードでツール呼び出しを作り出さない
冪等な再試行同じ create_ticket が 2 回ツール層で重複排除キーを使用する

スキーマ検査は自動化します。主観的な品質評価はポータビリティゲートに含めないでください。

プロバイダ変更時になお壊れるもの

引数の妥当性

スキーマが「単純」に見えても、モデルは誤った enum や必須フィールドの欠落を出力することがあります。ポータブルなシステムでは、副作用の前に同じ JSON Schema で検証します。「モデルは通常従う」という理由で、未検証の引数を使って決済や書き込みツールを実行してはいけません。

並列ツール呼び出し

一つのターンで複数ツールが起動するか、次のリクエストで結果をどの順序に並べる必要があるかは、プロバイダによって異なります。変更操作ではファンアウトを制限してください。業務アクションが可換でない場合は、ツールを直列に実行する方が安全です。

tool_choice の意味論

auto、none、required、指定ツールの強制は、どの環境でも同じように実装されているわけではありません。製品がツールの強制使用に依存する場合は、各モードをテストマトリクスに含めてください。

厳格な構造化ツールと自由形式テキスト

OpenAI 形式の厳格な構造化出力、Claude のネイティブ構造化出力、Gemini の構造化出力は、それぞれ異なる製品です。Anthropic の OpenAI 互換レイヤーでは strict が無視されます。プロバイダの宣伝文句にかかわらず、アプリケーションレベルの検証を前提にしてください。

ストリーミングとツール

ストリーミングのツール呼び出しでは、引数文字列が断片として届くことがよくあります。呼び出しが完了するまでバッファしてから検証してください。ツールがすでに状態を変更した後のストリーム途中のフェイルオーバーは、モデルのバグではなくアプリケーションのバグです。

副作用の冪等性

ポータブルなツールでも、作成や決済には永続的な冪等キーが必要です。そうしなければ、モデルの再試行とクライアントの再試行によって二重送信が発生します。

ポータブルツールに対するゲートウェイの責務

AI ゲートウェイには、次の責務があります。

  • 一つの入口ツール方言(通常は OpenAI tools)を受け付ける
  • 各アプリにネイティブ形式を学ばせず、Claude、Gemini、OpenAI の上流へマッピングする
  • ツールハーネスがプロバイダのスナップショット ID をハードコードしないよう、安定したモデル別名を公開する
  • ツール呼び出しターンのモデル別名と終了理由を含む利用ログを出力する
  • バッチキーが変更操作の多いエージェントを呼び出せないよう、キーのスコープを強制する

アーキテクチャの選択(プロキシかゲートウェイか)は LLM プロキシと AI ゲートウェイ で説明しています。Claude と Gemini に固有の SDK セットアップは、OpenAI SDK で Claude を使う と OpenAI SDK で Gemini を使う を参照してください。

8 ケースのポータビリティマトリクス

同じゲートウェイクライアントを使い、OpenAI、Claude、Gemini の各別名に対して次を実行します。

  1. ツール不要(純粋なテキスト)
  2. 妥当な引数を持つ単一の必須ツール
  3. 必須引数の欠落
  4. 不正な enum
  5. 未知のツール名
  6. 2 ツールの並列呼び出し(製品が許可する場合)
  7. 指定ツールに対する強制 tool_choice
  8. 変更ツールの冪等な再試行

ケース 3〜5 と 8 の合格基準は、検証または実行器の挙動であるべきです。「モデルが丁寧に謝罪した」ことではありません。

ツールをポータブルに保つ設計ルール

  • 一つの名前には一つの意味だけを持たせます。無関係なシステム間で search を多義化しないでください。
  • 単純な JSON を優先します。ネストされた union や巨大な自由テキストは、モデル間の信頼性を下げます。
  • 意味を黙って変更せず、ツールを明示的にバージョン管理します(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 の別名でも繰り返してください。ツールを呼び出すかどうかの差は製品上のシグナルですが、応答エンベロープの形の差はゲートウェイのマッピングバグです。

公式資料

最終確認: 2026-07-15。エージェントの副作用を拡大する前に、プロバイダごとに tool_choice、並列ツール呼び出し、厳格なスキーマ対応を再確認してください。

よくある質問

各チームがすべてのプロバイダのネイティブツール形式を学ぶべきですか?

製品のアプリケーションコードでは不要です。入口スキーマを一つ—通常は OpenAI tools—に保ち、プロバイダ固有の癖はゲートウェイまたは単一のアダプターライブラリに隔離してください。

OpenAI 互換のツール呼び出しだけで本番エージェントに十分ですか?

転送とスキーマの戦略としては強力ですが、本番環境には許可リスト、スキーマ検証、認可、冪等性、予算管理が必要です。互換性はこれらの制御を置き換えません。

MCP はポータブルな function calling を置き換えられますか?

MCP は補完的な仕組みです。ホストへツールを公開する方法を標準化しますが、モデルプロバイダには呼び出しを要求する手段が引き続き必要です。トポロジーとガバナンスの詳細は、上記の MCP ルーティング記事で説明しています。

マルチプロバイダフェイルオーバー前にマトリクスを実行する

本番ツールスキーマを一つ選び、/v1/chat/completions 経由で OpenAI、Claude、Gemini の各別名に対して 8 ケースのマトリクスを実行し、そのエージェントのマルチプロバイダフェイルオーバーを有効にする前に検証の欠落を修正してください。その後で初めてトラフィックを拡大します。