CodeGym /コース /ChatGPT Apps /ツール一覧の動的制御(tool gating)

ツール一覧の動的制御(tool gating)

ChatGPT Apps
レベル 11 , レッスン 1
使用可能

1. tool gating とは何か、なぜ単独の講義テーマになるのか

これまでは簡略化した例として、App のツール一覧を定義し、MCP サーバーをつなぐ――それだけで常にモデルからすべて見える、という形にしてきました。デモを「5 分で作る」観点では機能しますが、実プロダクトの観点では十分ではありません。

Tool gating は、モデルが利用できるツール一覧を固定せず、コンテキストに応じて変えるパターンです。具体的にはワークフローの段階、ユーザー権限、データの状態などに依存します。

重要なのは、tools の一覧は「これまでに作ったもの全部の寄せ集め」ではなく、シナリオ設計の一部だという点です。ワークフローを設計するということは、すなわち各段階でモデルにどのツールを見せてよいかを設計することでもあります。

簡単なたとえとして、銀行のインターンにすべてのシステムへ初日からアクセスさせたりはしません。まずは閲覧のみ、次に簡単な操作、その後に重要な操作へ――という段階的な付与です。ここでも同じで、インターンが LLM に置き換わっただけです。

2. 「すべてのツールを常時」問題:コンテキスト汚染とセキュリティ

モデルに何十ものツールを与えると、コンテキスト過負荷、選択の混乱、そしてセキュリティの面で同時に問題が出ます。OpenAI/Anthropic の調査でも、多くの関数をコンテキストに記述するほど、狙ったものに当てる精度が下がることが示されています。

第一に、ツール定義はすべてトークンを消費します。名前、説明、JSON Schema。30〜40 の tools だけで容易に数千トークンを使います。本来は対話履歴やユーザーコンテキスト、良い回答の例に使えるはずのトークンが、API 説明の「長編小説」を読むことに費やされてしまいます。

第二に、ツールが似ているとモデルが混乱します。たとえば search_productsget_product_details があると、説明文の印象からテキスト検索なのに get_product_details を呼ぼうとしてしまうことがあります。

さらにセキュリティの話。退屈ですが重要な 最小権限(least privilege)の原則があります。システムは「いまここ」で本当に必要な能力だけを持つべきです。もし最初の導入段階からモデルが checkout を知っていたら、ユーザーの小さなプロンプトインジェクションで、時期尚早な決済呼び出しが起こり得ます。Tool gating は権限最小化を実現する便利な手段です。各ステップで必要なものだけを有効化します。

そして UX。ユーザーが想定していない「魔法のような」こと(例: まだプレゼントを選んでいるのに勝手に注文が作成される)をモデルが突然行うと、あなたの App への信頼は一気に下がります。

3. GiftGenius による tool gating の例

ケーススタディとして GiftGenius を取り上げ、ステップを整理します。

  1. インタビュー: 受け取り手の年齢・性別・興味・予算などを把握する。
  2. 選定: カタログから商品を検索し、アイデアを提示する。
  3. Checkout: ユーザーがプレゼントを選んだら、購入手続きへ進む。

もしインタビュー段階ですでにモデルが search_productsadd_to_cartcheckout を知っていると、次のようなことが起きます。

  • 十分な嗜好を集める前に、早すぎる検索呼び出しを始めてしまう。
  • ユーザーが「これ良いね、買う」とつぶやいただけで、いきなり「注文手続き」を試みてしまう。

正しいやり方は、ステップの進行に合わせて利用可能な tools を切り替えることです。以下でちょうどそのシナリオを見ます。インタビューでは嗜好保存系のみ、選定では検索とカート投入、checkout 段階では checkout そのものを見せます。

簡単な表にまとめます。

ワークフローのステップ ステップの目的 モデルが利用可能なツール このステップでモデルに「見せない」もの
INTERVIEW
受け取り手のプロファイルを集める
save_preference, finish_interview
search_products, add_to_cart, checkout
BROWSING
アイデアを選定・絞り込む
search_products, get_product_details, add_to_cart
save_preference
+
checkout
(カートが空の場合)
CHECKOUT
購入手続きを行う
search_products, get_product_details, add_to_cart, checkout
すでに不要な「設定系」ツール全般

注目点: checkout ツールは、手続き対象があるとき、かつ該当ステップに入ったときにだけ現れます。これはコマース系シナリオにおける典型的な tool gating の例です。

4. tool gating の戦略:状態・ロール・リソースに基づく切り替え

もっとも一般的なのは state‑based gating(ワークフロー段階に応じたゲーティング)です。つまりどこかに step という変数を持ち、それに応じて有効化するツールを決めます。

影響するのはステップだけではありません。

role‑based gating(ユーザーのロール別)を行うこともあります。管理者には管理系ツール(例: カタログ再インデックス)を、一般ユーザーにはユーザー向けツールだけを許可します。あるいは resource‑based gating(リソースの状態別)として、「ドアを開ける」ツールはドアが閉じている状態のときだけ現れる、という設計もあります。

空論にならないよう、TypeScript の小さな関数で表してみます。ステップ、ロール、現在のカート、あるリソースの状態を持つコンテキストがあるとしましょう。

type WorkflowStep = 'interview' | 'browsing' | 'checkout';
type UserRole = 'user' | 'admin';

interface WorkflowContext {
  step: WorkflowStep;
  role: UserRole;
  cartItems: number; // カート内の商品数
  doorIsClosed: boolean; // resource-based gating の例: 特定リソースの状態
}

次に、システムに存在するツールと、そのフィルタ方法を定義します。

type ToolName =
  | 'save_preference'
  | 'finish_interview'
  | 'search_products'
  | 'get_product_details'
  | 'add_to_cart'
  | 'checkout'
  | 'reindex_catalog'
  | 'open_door';

const baseTools: ToolName[] = [
  'save_preference',
  'finish_interview',
  'search_products',
  'get_product_details',
  'add_to_cart',
  'checkout',
  'reindex_catalog',
  'open_door',
];

ここで open_door は、特定リソース(ドアが閉じているかどうか)の状態に依存するツールの例です。

ゲーティング関数本体:

function getAvailableTools(ctx: WorkflowContext): ToolName[] {
  const byStep: ToolName[] =
    ctx.step === 'interview'
      ? ['save_preference', 'finish_interview']
      : ctx.step === 'browsing'
      ? ['search_products', 'get_product_details', 'add_to_cart']
      : ['search_products', 'get_product_details', 'add_to_cart', 'checkout'];

  const checkoutAllowed =
    ctx.step === 'checkout' && ctx.cartItems > 0
      ? byStep
      : byStep.filter((t) => t !== 'checkout');

  const withAdmin =
    ctx.role === 'admin'
      ? [...checkoutAllowed, 'reindex_catalog']
      : checkoutAllowed;

  const withResources =
    ctx.doorIsClosed
      ? [...withAdmin, 'open_door']
      : withAdmin.filter((t) => t !== 'open_door');

  return withResources;
}

ここではゲーティングの「3 層」が見えます。

  • ステップ別(byStep
  • ユーザーのロール別(withAdmin
  • リソース状態別(withResources とフラグ doorIsClosed

これは SDK のコードではなく、単なるアーキテクチャのスケッチです。しかし一般的に tool gating はこのように考えます。ツールの全カタログがあり、コンテキストに応じてその部分集合を返す関数がある、という発想です。

5. App アーキテクチャのどこに tool gating があるか

ChatGPT App スタックの話と少し結びつけます。

理屈としての MCP プロトコルの動き

MCP では、ツールは静的 JSON に固定で埋め込む必要はありません。セッションに応じてサーバーが動的に一覧を返せます。さらに仕様には capabilities という機構があり、「ツール一覧が変わる」ことをサーバーが宣言できますし、クライアント(ChatGPT/エージェント)が一覧を再取得するための通知 tools/list_changed もあります。

形式上はこの方法を取ることができ、いくつかの MCP クライアントは動的な MCP ツール一覧で動作します。しかし現時点では ChatGPT App は tools/list_changed をサポートしていません。将来変わる可能性はありますが、いまのところはこのやり方は機能しません

実際に機能するアプローチはこちら

状態と利用可能なメソッド一覧を「モデル側」で持たせます。各ステップでモデルに state と利用可能な tools を「世界の説明」の一部として送ればよいのです。システムプロンプトに現在のステップ(例: step = "browsing")、主要フラグ(例: cartItems = 2role = "user")を明示し、いま許可されているツールの部分集合だけを添付します。

モデルは「ツールを忘れる」ことはできませんが、「このステップで使ってよいのはこの機能だけ…」という明示的な指示にはよく従います。結果として、モデルにとってのゲーティングは単純な契約になります。すなわち「これが現在のシナリオ状態。これが使ってよいボタンの一覧。その他は存在しないものとして扱う」。特別な「魔法」は要りません。ステップ間の遷移でリクエスト中の state と tools の一覧を一貫して更新するだけです。

さらに、structuredContent に次のような指示を書き足すこともできます。

{
  "instructions": {
    "current_step": "browsing",
    "enabled_mcp_tools": ["search", "apply"]
  }
}

同時に、ビジネスコード側でも保護を追加できます。たとえツール一覧が「更新済み」でも、各ハンドラー内でゲーティングのロジックを二重化するのが重要です。理由は次の通りです。

  • 長い会話のあと、モデルが指示やデータを忘れることがある。
  • 前のステップで見えていた「ファントム」ツールを呼ぼうとすることがある。

したがって良い設計は、「モデルからツールを隠す」ことに加えて、ハンドラー内でも「いま実行してよいか」を検証することです。

6. モデル側 vs ロジック側の tool gating

前章との関係で言うと、モデル呼び出し側で起きること(どのフラグ/step をプロンプトに入れるか)はモデル側ゲーティング、ツールのハンドラー内での検証はロジック側ゲーティングです。

通常は次の 2 層を分けるのが有効です。

  1. モデル側ゲーティング — このステップで「許可」されているツールを、指示として明示します。モデルの世界観は「これが現在の state、使えるボタンはこれだけ、他は存在しない」です。
  2. ロジック側ゲーティング — ツール本体の中の検証です。たとえモデルがキャッシュや記憶の名残、あるいは以前のステップの構成のせいで早すぎる checkout を試みても、ハンドラーが現在の状態を見て丁寧に却下します。たとえば「まずプレゼントを選んでから決済しましょう」という応答(例外を投げるだけではなく)。

なぜ両方必要か。LLM 周辺のインフラもシナリオも、常に完璧には動かないからです。

  • モデルがかつて見えた checkout を覚えていて、推論や tool call に持ち出すことがある。
  • あなた自身が誤って広すぎる tools を渡し、モデルが余計な機能を使い始めることがある。
  • クライアント/ミドルウェアが構成をキャッシュし、しばらく古いツール一覧を送ってしまうことがある。

実践的には、「tools に入れていないから二度と呼ばれないだろう」という想定は危険、ということです。ハンドラー内の検証は結局必要になります。

checkout ハンドラーでのロジック側ゲーティング例(疑似 TypeScript):

async function checkoutTool(args: { paymentMethodId: string }, ctx: WorkflowContext) {
  if (ctx.step !== 'checkout') {
    return {
      error: 'Checkout not available yet. Please finish selecting a gift first.',
    };
  }

  if (ctx.cartItems === 0) {
    return {
      error: 'Your cart is empty. Add at least one gift before checkout.',
    };
  }

  // ... 実際のチェックアウト処理ロジック
}

このような応答はユーザーにもモデルにも役立ちます。モデルは構造化されたエラーを見て、行動計画を修正できます。

7. UI/ウィジェットと tool gating の連携

Tool gating はサーバー側だけの話ではありません。UI/UX も変化を感じ取るべきです。

ウィジェットは現在のステップを知っています(すでに widgetState と、そこに currentStep を保持できると述べました)。モデルも知っています。ステップはツール呼び出し時に渡されるか、システムプロンプトに埋め込まれているためです。UI と利用可能ツールの集合が同期していることが重要です。

モデルは「選定」段階だと考えているのに、ウィジェットは「インタビュー」画面を表示している――こうなるとユーザーは混乱します。逆に UI が「支払う」ボタンを出しているのに checkout がまだ有効でない場合、モデルは「ボタンはあるのに機能しない」という奇妙な状況に置かれます。

tool gating を前提にしたステップのライフサイクルの簡単な図:

flowchart TD
  A[ユーザーがウィジェットでインタビューに回答する] --> B[ウィジェットが tool save_preference / finish_interview を呼び出す]
  B --> C[MCP / バックエンドが state.step を更新]
  C --> D[サーバーがセッションのツールセットを変更]
  D --> E[ChatGPT クライアントがモデルに対する利用可能な tools を更新]
  E --> F[モデルが新しい質問を投げる
または新しいツールを呼び出す] C --> G[ウィジェットが widgetState で新しいステップを受け取り
UI を切り替える]

ユーザーから見ると、ごく普通のウィザードです。最初にいくつかの質問、それからプレゼント候補、最後に最終確認。しかし裏では UI・ツール一覧・モデルへの指示が同時に切り替わっています。

Next.js ウィジェットでは、とてもシンプルに表現できます。step を widgetState に保持しているとしましょう。

type Step = 'interview' | 'browsing' | 'checkout';

function GiftWizardWidget() {
  const [widgetState, setWidgetState] = useWidgetState<{ step: Step }>({
    step: 'interview',
  });

  if (widgetState.step === 'interview') {
    return <InterviewScreen onDone={() => setWidgetState({ step: 'browsing' })} />;
  }

  if (widgetState.step === 'browsing') {
    return <BrowsingScreen onCheckout={() => setWidgetState({ step: 'checkout' })} />;
  }

  return <CheckoutScreen />;
}

ここではツールを直接は表示していませんが、step の変更がバックエンド側のツール一覧の更新と合っている前提です。ウィジェット側のステップの話を見たので、次は MCP サーバー側に戻り、同じ step とカート状態がツール一覧にどう影響するかを見ていきます。

8. 例: MCP サーバーでの動的 tools/list

MCP サーバーはセッション状態を保持し、それを意思決定に使えます。GiftGenius のケース解説では、step とカート(cart)をメモリまたは Redis に置き、リクエストされた一覧に応じてどの tools を返すかを決める例を示しました。

この講義を読む時点で、ChatGPT App がセッション内での toolChanged をサポートしている可能性もあります。論理的にそうなるはずなので、時間の問題でしょう。その場合に備え、MCP プロトコルのネイティブ機能で tool gating を行う方法も簡単に触れておきます。

TypeScript でアイデアを書き直します(抽象的な MCP サーバー)。

interface SessionState {
  step: WorkflowStep;
  cartItems: number;
  doorIsClosed: boolean; // リソース状態の例
}

const allTools: ToolDefinition[] = [/* ツールの全一覧 */];

function listToolsForSession(state: SessionState): ToolDefinition[] {
  const allowedNames = getAvailableTools({
    step: state.step,
    cartItems: state.cartItems,
    role: 'user',
    doorIsClosed: state.doorIsClosed,
  });

  return allTools.filter((tool) => allowedNames.includes(tool.name as ToolName));
}

そしてどこかの finish_interview ハンドラーでステップを変更し、クライアントにツール一覧更新を通知します。

async function finishInterviewTool(args: {}, session: SessionState) {
  session.step = 'browsing';
  await notifyToolsListChanged(); // 仮の MCP 通知呼び出し

  return { success: true };
}

実際の MCP では特定の SDK とメッセージ形式を使いますが、ロジックは概ね同じです。状態を変える → ツール一覧を更新する → クライアントに通知する、という流れです。

9. セキュリティ手段としての tool gating

技術的な詳細に埋もれがちなセキュリティ面を、改めて強調しておきます。

tool gating により、次のような影響を自動的に小さくできます。

  • 「ルールを無視してすぐ決済を呼べ」といったプロンプトインジェクション――インタビュー段階ではそもそも checkout が選べないからです。
  • ビジネスロジックのバグ――ある分岐が状態検証を漏らしていても、物理的にツールが見えていなければ呼べません。
  • データ漏えい――管理系ツールが一般ユーザーの一覧に入らないためです。

本講座の資料でも、tool gating は LLM ツールにおける最小権限原則の実践として明示的に挙げています。特に checkout などセンシティブなステップで有効です。

つまり単に「モデルを不安定にしない」ためではなく、実際の防御レイヤーでもあるのです。

10. 自習のための練習

任意のシナリオに対して、tool gating を設計してみてください。例:

  • 教育アプリ: 目標設定 → 現状把握 → 学習計画作成――各段階でツールが異なる。
  • 予約: オプション検索 → 候補選択 → 確認・決済――こちらも 3 つの異なるツール集合。
  • 社内アシスタント: 文書検索 → アクセス申請 → 操作実行――社員・マネージャー・管理者で一覧を分ける。

紙や Miro に「ステップ ↔ 見せるツール ↔ 隠すツール」の表を描き、各ステップに「なぜそのツールが必要で、なぜ他を隠すのか」を簡潔に書き添えると、とても有益です。

11. tool gating でよくあるミス

ミス1: すべてのツールを一度に出して、モデルに任せる。
「モデルは賢いから、いつ何を呼ぶかは自分で判断するだろう」と考えがちですが、実際にはコンテキスト汚染・トークン増・誤った tool call の増加を招きます。とりわけ checkout のような危険なツールが一覧にあるだけで、唐突に呼ばれる痛い事態が起こり得ます。まさにそれを避けるために tool gating が必要です。

ミス2: 一覧から隠せば十分だと思う。
MCP サーバーが tools/list にツールを出さなくなっても、モデルが履歴から「覚えていて」呼ぼうとする、あるいはインフラが古いツール構成をキャッシュして呼び出しが飛んでくることがあります。ハンドラーにロジック検証がなければ、「いまではない」操作が実行されてしまいます。したがってゲーティングはツール一覧レベルとハンドラー内の両方で行うべきです。

ミス3: UI とツール一覧の非同期。
ウィジェットがすでに "checkout" に遷移して「支払う」ボタンを表示しているのに、MCP 側で checkout を有効化し忘れるケース。モデルは「ボタンがあるのにツールがない」状況を理解できず、奇妙な応答を生みます。逆に、ツール一覧は更新済みでモデルは候補選定に入っているのに、ウィジェットがまだインタビューの質問をしているといったズレも起こります。ワークフロー設計では UI 状態とツール一覧を同期させることが重要です。

ミス4: ゲーティングロジックが複雑すぎる。
可能性に触発されて、状態が何十もあるほぼ BPMN 図のようなものを作ってしまうことがあります。結果として、なぜあるツールがうるう年の木曜日にしか使えないのか、自分でも一週間後にはわからなくなる――といった事態に。多くの App では、シンプルなステップの階段と、ステップ・ロール・いくつかの主要フラグに基づく明快なルールで十分です。

ミス5: サーバーの支援なしに、プロンプトだけでツール制御を固定する。
「このステップでは checkout を使うな」とシステムプロンプトに書くだけで、実際のツール一覧は変えず、バックエンドの検証も入れない――というやり方は不安定になります。プロンプトの指示は有用ですが、インフラ側の技術的ゲーティングを補完するものであり、置き換えではありません。

ミス6: ロールとアクセス権を無視する。
認証のあるアプリでは、ステップだけでなくロールも考慮すべきことを忘れがちです。結果として、管理者専用のサポート/DevOps 向けツールが、一般ユーザーに見えてしまう(あるいは最悪、呼べてしまう)ことがあります。認可モジュールで見たように、権限はコンテキストに入ってきます。ツール選択でもこの情報を必ず使いましょう。

ミス7: 誤った tool call のモニタリング不足。
ゲーティングにミスがあると、「Tool not available」「MethodNotFound」や「Checkout is not available yet」のようなロジックエラーが頻発するのが典型です。これらのイベントを集計していないと、ユーザーが見えない壁に何度もぶつかっていることに長く気付けません。単純なログ収集とエラー種類ごとのカウンタだけでも、ワークフローとゲーティング設計の問題に早く気付けます。

コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION