見出し画像

同じ社内ツールをOpenAI・Claude(MCP)・Geminiから呼ぶ最小設計

Tool Callingを「移植できるスキル」にするための共通スキーマとハーネス


はじめに

社内で生成AIを実務に載せ始めると、ほぼ確実に同じ壁に当たります。

  • モデルやプラットフォームが増えるほど、ツール連携がバラバラになる

  • ある環境で動いた「検索」「要約」「参照」は、別環境だと作り直しになる

  • 認可、ログ、評価、失敗時の挙動が場当たりになり、保守が崩壊する

この問題の本質は「モデルが賢いか」ではなく、ツールのI/Oと実行ハーネスが移植可能な形になっていないことです。

そこで本記事では、同じ仕事(例:社内DB検索)を、OpenAI、Claude(MCP)、Geminiの3環境から呼び出すための最小パターンを、設計思想ごとまとめます。



この記事で扱うゴール

ゴールは「プロバイダ別の書き方紹介」ではありません。

狙いは次の2つです。

  • 共通の型(スキーマ)を作り、どの環境でも同じ入出力で呼べるようにする

  • 実装差分を薄いアダプタに閉じ込め、スキルのポータビリティを確保する


まず押さえる前提

多くのLLM APIには、モデルが外部機能を呼ぶための仕組みがあります。OpenAIでは tool calling(function calling)として、JSON Schemaで定義したツールを tools として渡し、モデルが呼ぶべきツールと引数を返す流れが一般的です。

Gemini APIも同様に、関数宣言(function declaration)を渡し、モデルが関数呼び出しを提案し、クライアントが実行して結果を返す流れを採ります。

Claude側は、MCP(Model Context Protocol)を通じて外部ツールやデータソースに接続する設計が強く推されています。Claude CodeはMCPサーバに接続し、ツールやDBにアクセスします。

ここまでで重要なのは、仕組み自体は似ているという点です。
違うのは「APIの形」と「周辺運用」です。


最小設計の結論

移植可能にする最短ルートはこれです。

  • ツールを共通スキーマで定義する

  • 実行部分をハーネス(検証・認可・ログ・リトライ)として共通化する

  • プロバイダごとの差分はアダプタに閉じ込める

図で言うと、こういう層構造です。

  • 上層:プロンプト、計画、評価(モデルに依存しやすい)

  • 中層:ツール契約(スキーマ、命名、戻り値)

  • 下層:実行ハーネス(認可、検証、ログ、冪等、リトライ、縮退)

この中で、移植性の生命線は中層と下層です。


共通ツール契約の作り方

例として db_search を作ります。

1) 入力スキーマを固定する

ポイントは次の3つです。

  • required を必ず入れる

  • additionalProperties: false を基本にする

  • 曖昧な object や自由形式の辞書を避ける(モデルが暴れやすい)

{
  "$id": "https://example.internal/schemas/db_search.request.json",
  "type": "object",
  "properties": {
    "query": { "type": "string", "minLength": 1 },
    "top_k": { "type": "integer", "minimum": 1, "maximum": 50, "default": 10 }
  },
  "required": ["query"],
  "additionalProperties": false
}

2) 出力も形を決める

「モデルに返す結果」を整えると、プロンプトがシンプルになります。

{
  "$id": "https://example.internal/schemas/db_search.response.json",
  "type": "object",
  "properties": {
    "hits": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "title": { "type": "string" },
          "score": { "type": "number" },
          "snippet": { "type": "string" }
        },
        "required": ["id", "title", "score"],
        "additionalProperties": false
      }
    }
  },
  "required": ["hits"],
  "additionalProperties": false
}

ここまでが「スキルのコア」です。
環境が変わっても、このI/Oは変えない。


実行ハーネスで必ずやること

ツールを呼べるだけでは、実務で壊れます。
最低限、次を同梱します。

  • 入力検証(JSON Schema)

  • 認可(最小権限、短寿命、スコープ分離)

  • 冪等性(idempotency key)

  • リトライ(指数バックオフ、サーキットブレーカ)

  • 構造化ログ(後で必ず調査できる形)

  • 失敗時の縮退(キャッシュ、トップK縮小、人手レビュー)

この層が、スキルの品質保証です。


3環境それぞれの最小呼び出しパターン

ここからは、同じ db_search を「違う環境」から呼ぶときの最小形です。
コードは骨格です。SDKやレスポンス形式は更新されうるので、実装時は公式リファレンスに合わせてください。


パターンA:OpenAI(toolsで呼び出す)

OpenAIでは、ツールを tools として渡し、モデルが「呼び出すべきツール名と引数」を返し、クライアントが実行して結果を戻します。

Responses APIは、1リクエスト内で複数ツール呼び出しを回せる設計を強く打ち出しています。擬似コードの形はこうなります。

# 擬似コード:OpenAI tool calling の最小骨格
# 1) tools を渡す
# 2) tool call を受け取る
# 3) 引数を schema validate
# 4) db_search を実行
# 5) tool result をモデルに返して続行

tools = [{
  "type": "function",
  "function": {
    "name": "db_search",
    "description": "社内DBを検索する",
    "parameters": DB_SEARCH_REQUEST_SCHEMA
  }
}]

resp = openai.responses.create(
  model="gpt-5",
  input="冷間圧延の不具合事例を検索して",
  tools=tools
)

call = extract_tool_call(resp, name="db_search")
args = validate_json_schema(call.arguments, DB_SEARCH_REQUEST_SCHEMA)

result = db_search_api(args["query"], top_k=args.get("top_k", 10))

final = openai.responses.create(
  model="gpt-5",
  input=[{"role":"tool", "name":"db_search", "content": result}]
)

OpenAI側の考え方として「ツール呼び出しは強い構造化出力」として扱われます。Structured Outputsの文脈でも、function calling と json_schema 形式の使い分けが整理されています。


パターンB:Claude(MCPで呼び出す)

Claudeを「多数の社内ツール」に接続するなら、MCPはかなり相性が良いです。

  • MCPサーバがツール群を公開する

  • Claude側(クライアント)が必要なツールだけを呼ぶ

  • ツール定義と実装が分離され、移植性が上がる

Claude CodeがMCPで外部ツールに接続できることは、公式に案内されています。MCP自体も「データソースとAIツールを安全に双方向接続する標準」として紹介されています。HTTPでMCPサーバを叩く最小イメージは、こういう形になります。

# 擬似コード:MCPサーバに db_search を実行させる最小骨格
curl -s -X POST "https://mcp.example.internal/run" \
  -H "Authorization: Bearer ${MCP_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "tool": "db_search",
    "input": { "query": "冷間圧延 不具合 事例", "top_k": 10 },
    "request_id": "uuid-1234"
  }'

Claude側のツール利用(tool use)でも、MCPツールをMessages APIから直接使う話が整理されています。MCPの美味しさは、プロバイダ差分を「MCPクライアント層」で吸収しやすい点です。
つまり、スキルの実体はMCPサーバ側に寄せられます。


パターンC:Gemini(function callingで呼び出す)

Gemini APIでも、関数宣言を渡し、モデルが関数呼び出しを提案し、クライアントが実行して結果を返す流れが基本です。概念的にはこうです。

# 擬似コード:Gemini function calling の最小骨格
functions = [{
  "name": "db_search",
  "description": "社内DBを検索する",
  "parameters": DB_SEARCH_REQUEST_SCHEMA
}]

resp = gemini.generate(
  model="gemini-2.x",
  prompt="冷間圧延の不具合事例を検索して",
  functions=functions
)

call = extract_function_call(resp, name="db_search")
args = validate_json_schema(call.args, DB_SEARCH_REQUEST_SCHEMA)

result = db_search_api(args["query"], top_k=args.get("top_k", 10))

final = gemini.send_tool_response(
  tool_name="db_search",
  content=result
)

Gemini側も、Function CallingとToolsの位置づけを公式に整理しています。


3環境を「同じスキル」に見せるコツ

ここからが実務で効きます。

1) 命名を固定する

  • ツール名は不変にする(db_search を変えない)

  • 引数名も不変にする(query, top_k など)

命名が揺れると、プロンプトも評価も壊れます。

2) I/Oを宣言的にする

  • JSON Schemaを単一の正本にする

  • そこから各プロバイダ形式に変換する(薄いアダプタ)

「正本が1つ」だと、移植のコストが極小になります。

3) 返却データは証拠として扱う

検索結果は「モデルの自由作文」ではなく、後で検証可能な証拠として扱うべきです。

  • id, title, score, snippet を返す

  • モデルには「この証拠だけで要約せよ」と指示する

  • 出典を辿れるようにする


運用チェックリスト

ここは最初に仕込むと、後で何十倍も効きます。

セキュリティ

  • 最小権限でツールトークンを発行する

  • 読み取り系と書き込み系はスコープを分ける

  • ログに機密が混ざる前提でマスキングする

再現性

  • スキーマ検証を必須にする

  • 失敗時は縮退ルールを決める

  • request_id を全レイヤで引き回す

可観測性

最低限、これを構造化ログで残します。

  • tool

  • request_id

  • latency_ms

  • status

  • error_type

  • tenant(必要なら)

評価

正解がない世界でも、軸は作れます。

  • 関連性(欲しい答えに近いか)

  • 網羅性(抜けがないか)

  • 説明性(根拠に紐づいているか)

  • 応答時間

  • コスト


小さく始める導線

最短で「動く」まで持っていく手順です。

  1. 既存の社内検索APIに、入力と出力のJSON Schemaをかぶせる

  2. どれか1環境で tool calling の往復を通す

  3. 構造化ログを入れて、失敗パターンを回収する

  4. 2環境目、3環境目はアダプタ追加だけで動く状態に寄せる

この順序にすると「最初からマルチ環境対応」をやらずに済みます。


よくある落とし穴

スキーマがゆるくてモデルが暴れる

additionalProperties: false を入れないと、関係ないフィールドが混ざりやすくなります。最小で固めて、必要なときにだけ拡張します。

ツール結果が大きすぎてコストが爆発する

検索結果を全部返さないで、証拠として必要な粒度に整形します。
スニペットを短くするだけで、体感が変わります。

失敗時に黙って終わる

本番で一番困るのは「静かに壊れる」ことです。
縮退経路を決め、ログに必ず残します。


まとめ

スキルのポータビリティは「モデル選定の自由度」そのものです。

  • ツール契約を共通化する

  • 実行ハーネスを共通化する

  • プロバイダ差分は薄いアダプタに閉じ込める

この3点を押さえるだけで、OpenAIでもClaudeでもGeminiでも、同じ社内ツールを同じ感覚で呼べる状態に近づきます。


次に取る行動

まずは db_search の入力と出力スキーマを1つ決め、どれか1環境で「呼び出し往復」と「構造化ログ」を通してください。



いいなと思ったら応援しよう!