1. 什么是 tool gating,以及为何值得单独讲一课
到目前为止,在简化示例中我们通常这样做:为 App 描述一组工具,接上 MCP 服务器——然后这些都始终对模型可见。若目标是“5 分钟做个 demo”,这没问题;但放到真实产品里——就不太行了。
Tool gating 是一种模式,其中模型可用的工具列表不是固定的,而是依赖上下文:工作流步骤、用户权限、数据状态等。
最重要的是:tools 列表不是“你曾经写过的一切的大杂烩”,而是场景设计的一部分。当你设计 workflow 时,其实也在设计模型在每个阶段有权看到哪些工具。
一个最简单的类比:你不会让银行实习生一上来就接触所有系统——先只能查看,再是简单操作,最后才是更严肃的权限。这里也是同样的逻辑,只不过“实习生”换成了 LLM。
2. “一次性开放所有工具”的问题:上下文污染与安全性
如果给模型几十个工具,它会在多个方面同时受挫:上下文过载、选择混乱以及安全问题。OpenAI/Anthropic 的研究显示,你在上下文中描述的功能越多,模型越难选中正确的那个。
首先,每个工具的定义都会占用 token:名称、描述、JSON Schema。30–40 个 tools 的列表很容易吃掉上千 token。这些 token 本可以用于对话历史、用户上下文、优质回答示例;结果模型却在“读你的 API 小说”。
其次,当工具相似时,模型会混淆。如果你有 search_products 和 get_product_details,它可能会直接用文本查询去调用 get_product_details,因为它觉得那个描述更匹配。
还有安全问题。一个朴素但重要的原则是最小权限(least privilege):系统应当只拥有“此时此地”真正需要的能力。如果在“认识阶段”模型就已经知道 checkout,只要有一点用户的 prompt 注入,它就可能提前尝试发起支付。Tool gating 正是最小化权限的便捷做法:每一步只开放必要的工具。
最后,还有 UX。若模型突然做了用户没预期的“魔法”(例如人还在挑礼物,它就创建了订单),用户对你的 App 的信任会迅速下滑。
3. 以 GiftGenius 演示 tool gating
拿我们的 GiftGenius 案例,实事求是地看下步骤:
- 访谈:了解收礼人的年龄、性别、兴趣、预算等。
- 挑选:按目录搜索商品,给出点子。
- Checkout:当用户已经选定礼物,进入下单流程。
如果在访谈步骤模型就已经知道 search_products、add_to_cart 和 checkout,它可能会:
- 还没收集到足够偏好信息就开始过早地调用搜索;
- 因为用户说了“哦,这个不错,就它吧”,而立刻尝试“下单”。
正确的做法是——随着步骤推进,改变可见的 tools 列表。下面我们就分析这样的场景:访谈阶段只看得到保存偏好的工具;挑选阶段开放搜索和加购;checkout 阶段才开放 checkout。
把它汇总成一个小表:
| Workflow 步骤 | 步骤目标 | 模型可用的工具 | 此步骤模型“看不到”的内容 |
|---|---|---|---|
|
收集收礼人画像 | |
|
|
挑选并细化点子 | |
+ (如果购物车为空) |
|
完成下单 | |
任何已不再需要的“配置类”工具 |
注意:checkout 工具只在有东西可下单且处于对应步骤时才出现。这就是电商场景中典型的 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; // resource-based 门控示例:特定资源的状态
}
现在描述系统中有哪些 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/代理)在变化后重新拉取工具列表。
形式上你可以这么做,也会有一些 MCP 客户端支持动态工具列表。但截至目前,ChatGPT App 尚不支持 tools/list_changed。未来可能会变,但现在这种方式不可用。
可行的做法是这样的
你在模型侧维护状态和可用方法列表。可以在每个步骤把 state 和允许的 tools 作为“世界观的一部分”传给模型:在系统提示中明确写出当前步骤(例如 step = "browsing")、关键标志(例如 cartItems = 2、role = "user"),并仅附上当前允许的工具子集。
模型不会“自我遗忘”工具,但它很听显式指令,比如:“在此步骤你只能使用这些函数……”。最终,对模型而言,门控逻辑就是一个简单契约:这是当前场景状态,这是你能用的按钮,其他对你而言不存在。无需任何“魔法”——在步骤转换时持续更新请求里的 state 和 tools 列表即可。
此外,你可以在 structuredContent 中加入类似的指令:
{
"instructions": {
"current_step": "browsing",
"enabled_mcp_tools": ["search", "apply"]
}
}
同样也可以在业务代码层面加保护。即使 tools 列表已经“更新”,也务必在各个处理器中重复门控逻辑,因为:
- 若对话很长,模型可能会遗忘指令和/或数据;
- 模型可能尝试调用“幽灵”工具——那个在上一步还可用的工具;
所以一个好的设计是:既在模型侧“隐藏”工具,又在处理器内部检查当前是否允许执行。
6. 模型层门控 vs 逻辑层门控
承接上文:发生在调用模型这一层(你在提示中放入哪些标志/step)的是模型层门控;而在工具处理器内部的检查——是逻辑层门控。
通常把两层分开更合理:
- 模型层门控——模型之所以知道某工具“此刻被允许”,仅因为你在指令里明确写了该步骤可用的函数。对模型而言,世界就是:“这是当前 state,这是一组按钮,除此之外什么都没有。”
- 逻辑层门控——在工具内部做检查。即便模型还是过早地尝试调用 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. 如何将 tool gating 与 UI 和小部件对齐
Tool gating 不仅是服务端的事情。UI/UX 也要感知这些变化。
小部件知道当前步骤(我们已经讨论过 widgetState,它可以存储 currentStep)。模型也知道,因为步骤要么明确传给工具,要么写在系统提示中。关键是UI 与激活的 tools 集保持同步。
如果模型认为当前是“挑选”步骤,而小部件显示的是“访谈”界面,用户会迷惑。反过来——UI 已经画出了“支付”按钮,但 checkout 还不可用,模型就会很尴尬:有按钮,功能却“不可用”。
考虑 tool gating 的步骤生命周期小图:
flowchart TD A[用户在小部件中填写访谈] --> B[小部件调用 tool 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 小部件中,这可以很简单地表达。假设你在 widgetState 中存着 step:
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。服务器根据它们返回工具列表。
当你读到这节课时,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 注入——因为在访谈步骤,模型根本没有 checkout 可选;
- 业务逻辑中的缺陷——即便某条分支没有完全检查状态,工具也可能在物理上就不可用;
- 数据泄露——因为管理员工具不会进入普通用户的工具列表。
在本课程文档中,tool gating 被明确列为在 LLM 工具上下文中落实最小权限原则的实践之一,尤其是用于 checkout 和其他敏感步骤。
因此,它不只是“让模型更少犯错”的方式——还是一层真正的防护。
10. 如何自行练习
为巩固内容,可以为你自己的任何场景设计一套 tool gating。例如:
- 教育应用:目标设定、水平评估、计划制定——每一步都有各自的 tools;
- 预订:搜索选项、选择方案、确认与支付——同样是三套不同的工具集;
- 内部企业助理:文档搜索、访问请求、执行操作——员工、经理、管理员各有不同列表。
非常推荐直接在纸上或 Miro 上画一张表:“步骤 ↔ 可见工具 ↔ 隐藏工具”,并在每个步骤旁简要说明它为何需要这些 tools、以及为何要隐藏其他工具。
11. 使用 tool gating 时的常见错误
错误 1:“一次性把所有工具都‘堆出来’,指望模型自己搞定”。
有时开发者会想:“模型很聪明,会自己搞清楚何时调用什么。”现实是这会污染上下文、增加 token、让错误的 tool call 更多。尤其糟糕的是,模型可能因为列表里有 checkout 等危险工具而突然调用它。Tool gating 的目的正是避免这种情况发生。
错误 2:以为把工具从列表里隐藏就足够了。
即使 MCP 服务器不再在 tools/list 返回某个工具,模型可能还“记得”它,基础设施也可能缓存了旧工具集。结果就会收到对幽灵工具的调用。如果处理器没有逻辑检查,就可能“在不合时宜的时刻”执行。因此门控既要体现在工具列表层,也要体现在处理器内部。
错误 3:UI 与工具集不同步。
常见情况是小部件已经进入 "checkout" 步骤并显示了漂亮的“支付”按钮,但在 MCP 侧你忘了把 checkout 放到可用列表中。模型不理解为何有按钮但工具不可用,进而生成奇怪的回答。反过来也是:工具集已经切换,模型已准备好挑选礼物,UI 却仍在问访谈问题。设计 workflow 时要同步更新 UI 状态与工具列表。
错误 4:过度复杂的门控逻辑。
有时开发者被可能性所吸引,构建出接近完整 BPMN 的图,有几十个状态、覆盖各种情况。结果是一周后连他自己都搞不清为什么某工具只在闰年的周四可用。对大多数 App,简单的阶梯式步骤与清晰规则就足够:按步骤、按角色、加上少量关键状态位。
错误 5:把 tool gating 硬写在提示里,却没有服务端支撑。
有时有人尝试只靠系统提示解决:“此步骤不要使用 checkout”,但既不改变真实的工具列表,也不在后端加检查。模型有时会听话,有时不会,你会得到不稳定的行为。提示指令有用,但应当补充而不是替代基础设施侧的技术门控。
错误 6:忽视角色与权限。
带有认证的应用常常忘记,tool gating 不仅要考虑步骤,还要考虑角色。结果就是无管理员权限的用户仍然能看到(更糟糕的是可以调用)面向支持或 DevOps 的工具。在授权模块中你已经看到权限如何进入上下文;这里要记得在选择工具集时使用这些信息。
错误 7:没有监控错误的 tool call。
如果门控哪里出了问题,典型症状是“Tool not available”、“MethodNotFound”之类错误变多,或你自定义的逻辑错误如“Checkout is not available yet”。如果你不收集这类事件的统计,你可能很久都注意不到用户在频繁撞“隐形墙”。简单的日志与按错误类型的计数器能帮助你及时发现 workflow 与门控设计中的问题。
GO TO FULL VERSION