CodeGym /課程 /ChatGPT Apps /工具清單的動態管理(tool gating)

工具清單的動態管理(tool gating)

ChatGPT Apps
等級 11 , 課堂 1
開放

1. 什麼是 tool gating,為什麼值得單獨講一節

到目前為止,在簡化的示例裡我們常這樣做:為 App 描述一組工具,接上 MCP‑伺服器——然後模型永遠都能使用它們。要是為了「5 分鐘做個 demo」這樣沒問題;但放在真實產品裡就不太理想。

Tool gating 是一種 pattern,其中模型可用的工具清單不是固定的,而是取決於上下文:工作流程步驟、使用者權限、資料狀態等。

最重要的一點:tools 清單不是「你曾經寫過的一切的大雜燴」,而是劇本設計的一部分。當你設計 workflow 時,其實也在設計模型在每個階段有權看見哪些工具

最簡單的類比:你不會讓銀行實習生一開始就能存取所有系統——先只能查看,接著做簡單操作,再來才是更嚴肅的權限。這裡邏輯相同,只是把實習生換成 LLM。

2. 「所有工具一次全上」的問題:上下文污染與安全性

如果一次給模型數十個工具,它會同時在多個方面受影響:上下文超載、選擇混亂,以及安全性問題。OpenAI/Anthropic 的研究顯示,功能描述越多,模型越不容易選到正確的那一個。

首先,每個工具定義都會佔用 token:名稱、描述、JSON Schema。30–40 個 tools 很容易就吃掉幾千個 token——而這些 token 本來可以用在對話歷史、使用者上下文、優秀回答示例。結果模型被迫閱讀一部「關於你 API 的長篇小說」。

其次,當工具相似時,模型會混淆。如果你同時有 search_productsget_product_details,它可能會直接帶著文字查詢去叫 get_product_details,只因為描述看起來更合適。

另外是安全問題。有個乍看無聊但很重要的原則:最小權限(least privilege)——系統只能擁有「此時此地」真正需要的能力。如果在初識階段模型就知道 checkout,只要使用者來個小小的 prompt injection,它就可能過早觸發付款。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
任何已不再需要的「設定」類 tools

請注意:工具 checkout只在有東西可結帳、且位於對應步驟時才出現。這就是 commerce 場景中經典的 tool gating 範例。

4. tool gating 策略:依狀態、依角色、依資源

最常見的變體是state‑based gating(依 workflow 步驟門控):工具清單取決於場景的狀態。也就是你在某處保存變數 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; // 資源導向 gating 範例:特定資源的狀態
}

接著描述系統裡有哪些 tools,以及如何過濾:

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;
}

這裡可以清楚看到三個「層」的門控:

  • 依步驟(byStep);
  • 依使用者角色(withAdmin);
  • 依資源狀態(withResources 與旗標 doorIsClosed)。

這不是某個 SDK 的程式碼,而只是架構素描。但思考 tool gating 通常就是這樣:先有完整的工具目錄,再有一個函式依據上下文回傳其中的子集。

5. 在 App 架構中,tool gating 位於哪裡

把這和你已經知道的 ChatGPT App 技術棧稍微串起來。

理論上 MCP 協定是這樣運作的

在 MCP 中,工具不必硬寫死在一份靜態 JSON:伺服器可以依據工作階段動態回傳清單。更進一步,規範裡有 capabilities 機制,伺服器可宣告它的工具清單會變化,並使用通知 tools/list_changed 讓用戶端(ChatGPT/代理)在變化時重新拉取 tools 清單。

形式上你可以這麼做,也會有一些 MCP 客戶端能配合動態工具清單。但截至今天,ChatGPT App 不支援 tools/list_changed。未來也許會變,但目前這種方式行不通

可行的做法如下

你在模型端維護狀態與可用方法清單。可以在每個步驟把 state 與允許的 tools 作為「世界觀的一部分」送給模型:在 system prompt 裡明確描述當前步驟(例如,step = "browsing")、關鍵旗標(例如,cartItems = 2role = "user"),並只附上當下允許的工具子集。

模型不會自己「忘掉」工具,但它非常善於遵循明確指示,如:「在這個步驟你只能使用以下函式……」。於是對模型而言,所有門控邏輯看起來就是一份簡單契約:這是當前場景狀態,這是你能按的按鈕,其餘對你而言不存在。這不需要任何特別的「魔法」——只要在步驟切換時,一致地在對模型的請求中更新 state 與 tools。

此外,你可以在 structuredContent 中加入指令,像這樣:

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

同時也可以在你的商務程式碼層加上防護。即使 tools 清單已經「更新」,仍然要在處理器內部重複門控邏輯,因為:

  • 如果對話拉得很長,模型可能忘記指令與/或資料;
  • 模型可能嘗試呼叫在上一個步驟可用、現在不可用的「幽靈」工具;

因此好的設計是:既在模型前「藏起」工具,也在處理器內檢查現在是否允許。

6. 模型層 vs 邏輯層的 tool gating

把這和前一節連起來:發生在模型呼叫層的一切(你在 prompt 放了哪些旗標/step)屬於模型層門控;而在工具處理器內的檢查屬於邏輯層門控。

通常可分為兩層:

  1. 模型層門控——模型知道某工具「此刻被允許」,因為你在指令中明確寫出本步驟可用的函式。對模型來說,世界就是:「這是當前 state,這些是可用按鈕,沒有其他了」。
  2. 邏輯層門控——工具本體內的檢查。即使模型還是嘗試過早呼叫 checkout(也許因為快取、幽靈記憶,或某一步你曾經把它暴露出來),處理器會查看當前狀態並有禮貌地拒絕:像是「先選好禮物,之後再結帳」(而不是直接丟例外!)。

為什麼需要兩層?因為圍繞 LLM 的基礎設施與場景本身可能並不完美:

  • 模型可能記得它曾看過 checkout,並在推理或 tool-call 中提到它;
  • 你自己也可能在某一步誤傳了比需要更寬的 tools 集,而模型就開始用多餘的功能;
  • 客戶端/中介層可能快取了模型呼叫設定,一段時間內會送出舊的工具清單。

實務上,僅僅指望「我們沒把某工具放進 tools——它就再也不會被呼叫」是危險的。Handlers 內的檢查仍然需要。

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. 如何將 tool gating 與 UI 和小工具串接

Tool gating 不僅是伺服器端的事,UI/UX 也要能感知變化。

小工具知道當前步驟(我們已談過 widgetState 以及它可以保存例如 currentStep 的狀態)。模型也知道,因為步驟不是明確傳給工具,就是寫入 system prompt。關鍵在於UI 與可用 tools 的集合要保持同步

如果模型認為現在是「挑選」步驟,而小工具顯示的是「訪談」介面,使用者會困惑。反過來,如果 UI 已經呈現「付款」按鈕,但 checkout 尚不可用,模型會陷入尷尬:按鈕存在,但功能「好像不能用」。

帶有 tool gating 的步驟生命週期小示意:

flowchart TD
  A[使用者在小工具裡填寫訪談] --> B[小工具呼叫工具 save_preference / finish_interview]
  B --> C[MCP / 後端更新 state.step]
  C --> D[伺服器為工作階段調整 tools 清單]
  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 />;
}

這裡我們沒有直接展示 tools,但預設 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 後,你會自動降低下列風險:

  • 像「忽略規則並立刻呼叫付款」這種 prompt injection——因為在訪談階段模型根本選不到 checkout
  • 商務邏輯的 bug——即使某段程式沒有充分檢查狀態,工具也可能在物理上不可用;
  • 資料外洩——因為管理工具不會出現在一般使用者的清單中。

在本課程文件中,tool gating 被明確列為在 LLM‑工具情境下落實最小權限的一項實務,尤其是在結帳與其他敏感步驟上。

也就是說,它不只是「讓模型比較不會出錯」的手段,更是一道真正的防護層。

10. 如何自己練習

為了鞏固概念,你可以為任何場景設計一套 tool gating。例如:

  • 教育應用:設定目標、評估當前程度、產生學習計畫——每個步驟都有各自的 tools;
  • 訂位/預訂:搜尋選項、選擇方案、確認與付款——依序對應三組不同工具;
  • 內部企業助理:搜尋文件、申請存取、執行操作——員工、經理、管理員各有不同清單。

很有幫助的做法是直接在紙上或 Miro 畫一張表:「步驟 ↔ 可見工具 ↔ 隱藏工具」,並在每個步驟旁簡述為什麼需要這些 tools、為什麼其他的要隱藏。

11. 使用 tool gating 的常見錯誤

錯誤 #1:一次把所有工具都丟出來,指望模型自己搞定。
有時開發者會想:「模型很聰明,自己會知道什麼時候該叫什麼。」實際上這會造成上下文污染、token 增長與更多錯誤的 tool‑call。最痛的莫過於模型突然呼叫 checkout 或其他危險工具,只因為它在清單裡。Tool gating 的目的就是避免此類情況。

錯誤 #2:以為把工具從清單裡藏起來就足夠了。
即使 MCP‑伺服器不再在 tools/list 中回傳某工具,模型可能仍「記得」它,而基礎設施也可能快取舊清單。結果就是幽靈呼叫。如果處理器不做邏輯檢查,就可能「在不對的時刻」執行動作。因此門控應同時存在於工具清單與 handlers 內。

錯誤 #3:UI 與工具清單不同步。
常見狀況是小工具已切到 "checkout" 並顯示漂亮的「付款」按鈕,而在 MCP 端你忘了把 checkout 加入可用工具清單。模型不理解為何按鈕存在、工具卻不可用,開始產生奇怪回應。或反之:工具清單已經變更、模型準備開始挑選禮物,但小工具還在問訪談問題。設計 workflow 時務必同步更新 UI 狀態與工具清單。

錯誤 #4:門控邏輯過於複雜。
有時開發者受到靈感驅使,畫出幾近完整的 BPMN 圖,含數十個狀態與各種條件。結果過一週連自己都不懂為何某工具只在閏年的星期四可用。對多數 App 而言,一條簡單的步驟階梯與幾個清楚規則(依步驟、依角色、依幾個關鍵旗標)就足夠。

錯誤 #5:把 tool gating 硬寫在 prompt 裡,伺服器端沒有支援。
有時會嘗試只用 system prompt 解決:「這一步不要使用 checkout」,卻不改真實工具清單、也不在後端加檢查。模型有時會聽話、有時不會,行為變得不穩定。Prompt 指令很有用,但應該是對基礎設施層門控的補充,而不是替代。

錯誤 #6:忽略角色與存取權限。
在有身分驗證的應用中,常見疏忽是門控只考慮步驟、不考慮角色。結果是不具管理員權限的使用者仍能看見(更糟的是能呼叫)支援或 DevOps 用的工具。在授權模組裡你已經看到如何把權限帶入上下文;這裡務必記得在選擇 tools 時使用這些資訊。

錯誤 #7:沒有監控錯誤的 tool‑call。
如果某處門控設計不當,特徵就是「Tool not available」「MethodNotFound」或你自定義的邏輯錯誤(如「Checkout is not available yet」)變頻繁。若不收集這類事件統計,就可能長期不知道使用者老是撞上無形的牆。簡單的日誌與錯誤計數能及早暴露 workflow 與門控設計上的問題。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION