同じ社内ツールを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(必要なら)
評価
正解がない世界でも、軸は作れます。
関連性(欲しい答えに近いか)
網羅性(抜けがないか)
説明性(根拠に紐づいているか)
応答時間
コスト
小さく始める導線
最短で「動く」まで持っていく手順です。
既存の社内検索APIに、入力と出力のJSON Schemaをかぶせる
どれか1環境で tool calling の往復を通す
構造化ログを入れて、失敗パターンを回収する
2環境目、3環境目はアダプタ追加だけで動く状態に寄せる
この順序にすると「最初からマルチ環境対応」をやらずに済みます。
よくある落とし穴
スキーマがゆるくてモデルが暴れる
additionalProperties: false を入れないと、関係ないフィールドが混ざりやすくなります。最小で固めて、必要なときにだけ拡張します。
ツール結果が大きすぎてコストが爆発する
検索結果を全部返さないで、証拠として必要な粒度に整形します。
スニペットを短くするだけで、体感が変わります。
失敗時に黙って終わる
本番で一番困るのは「静かに壊れる」ことです。
縮退経路を決め、ログに必ず残します。
まとめ
スキルのポータビリティは「モデル選定の自由度」そのものです。
ツール契約を共通化する
実行ハーネスを共通化する
プロバイダ差分は薄いアダプタに閉じ込める
この3点を押さえるだけで、OpenAIでもClaudeでもGeminiでも、同じ社内ツールを同じ感覚で呼べる状態に近づきます。
次に取る行動
まずは db_search の入力と出力スキーマを1つ決め、どれか1環境で「呼び出し往復」と「構造化ログ」を通してください。
