Agent 接入 MCP 后上下文爆炸、工具选串?一种“按需激活“的工具加载方案(含实现)

文章目录

      • [1. 问题:MCP 工具一股脑全注册,代价是什么](#1. 问题:MCP 工具一股脑全注册,代价是什么)
      • [2. 思路:把"拥有的工具"和"该暴露的工具"拆开](#2. 思路:把"拥有的工具"和"该暴露的工具"拆开)
      • [3. 实现:双池 + 三个元工具](#3. 实现:双池 + 三个元工具)
      • [4. 兜底:模型没激活就调用怎么办](#4. 兜底:模型没激活就调用怎么办)
      • [5. 完整流程演示](#5. 完整流程演示)
      • [6. 什么时候该用、什么时候不必](#6. 什么时候该用、什么时候不必)

1. 问题:MCP 工具一股脑全注册,代价是什么

给 Agent 接 MCP(Model Context Protocol)的标准动作是:连上 server,把它返回的所有工具一次性注册进可调用列表。接一两个 server 没感觉,接到三五个、每个再带十几个工具,问题就来了。

代价一:上下文被工具 schema 撑大。 每个工具暴露给大模型,是一段包含名字、描述、参数定义的 JSON。一个中等复杂度的工具,schema 摊开大约两三百个 token(经验量级,随参数多少浮动)。二十个工具就是好几千 token,而且每一轮请求都要带着,不管这轮用不用得上。

代价二:工具一多,模型就开始选串。 工具列表越长,大模型挑错工具、调错参数的概率越高。尤其当工具名相近时(get_row_by_idget_rows_by_conditionsget_row_by_key_value),模型很容易混。工具不是越多越强,过了某个临界点反而掉准确率。

这两个问题在 MCP 生态爆发后特别突出------能接的 server 越来越多,全量注册的负担也越来越重。

2. 思路:把"拥有的工具"和"该暴露的工具"拆开

根因在于我们把两件事当成了一件:"Agent 能用的全部工具""这一刻该让模型看见的工具"

把它俩拆开,方案就出来了:工具都注册进来,但默认不暴露给模型;模型需要时,自己去发现、激活。下面用我的开源框架 milu 的实现来讲具体怎么落地。

3. 实现:双池 + 三个元工具

第一步,工具分两个池。 注册表里维护两个字典:活跃池(注入模型,可直接调用)和休眠池(已注册但不注入):

python 复制代码
class ToolRegistry:
    def __init__(self):
        self._tools: dict[str, ToolWrapper] = {}     # 活跃池:schema 进上下文
        self._dormant: dict[str, ToolWrapper] = {}   # 休眠池:不进上下文

关键在生成给大模型的 schema 时,只遍历活跃池

python 复制代码
def get_schemas(self) -> list[dict]:
    schemas = []
    for wrapper in self._tools.values():   # 只有活跃池,休眠工具一概不出现
        schemas.append({
            "type": "function",
            "function": {
                "name": wrapper.name,
                "description": wrapper.description,
                "parameters": wrapper.parameters_schema,
            },
        })
    return schemas

MCP 工具默认进哪个池?由一个开关决定,默认让它们休眠:

python 复制代码
mcp_tools_active_by_default: bool = False   # 默认 False:MCP 工具进休眠池

第二步,给模型三个"元工具"去自助。 既然工具休眠了,就得让模型能查、能激活。常驻三个轻量元工具在活跃池:

python 复制代码
# list_catalog ------ 列出所有休眠工具(按分类分组)
# search_tools ------ 关键词搜索休眠工具
# activate_tools ------ 激活指定工具,使其下一轮出现在工具列表里

休眠工具暴露给模型时只给摘要(名字 + 截断到 100 字的描述 + 分类),不给完整 schema,省得多:

python 复制代码
def list_dormant_tools(self) -> list[dict]:
    return [
        {"name": w.name, "description": w.description[:100], "category": w.category}
        for w in self._dormant.values()
    ]

分类是注册时从工具名前缀自动切的,fetch__htmlfetchmysql__querymysql,列目录时自然分组。

第三步,激活就是搬家。 把工具从休眠池字典挪到活跃池字典,O(1):

python 复制代码
def activate(self, name: str) -> ToolWrapper | None:
    wrapper = self._dormant.pop(name, None)
    if wrapper:
        self._tools[name] = wrapper   # 进活跃池,下一轮 get_schemas 就带上它
    return wrapper

4. 兜底:模型没激活就调用怎么办

模型有时会"抢跑"------在目录里看到 mysql__query,没激活就直接调。执行器拦一道,回一句友好提示:

python 复制代码
if self._registry.is_dormant(func_name):
    return ToolExecutionResult(
        output=f"工具 {func_name} 尚未激活。请先调用 activate_tools 激活它。",
        is_error=True,
    )

模型读到这条,补一步 activate_tools 就接上了,不会卡死。

5. 完整流程演示

用户说"查下数据库里有多少用户",内部是这样跑的:

  1. 模型没看到 mysql 工具,但它知道有 search_tools
  2. search_tools("mysql user") → 返回 mysql__query 等候选的摘要;
  3. activate_tools(["mysql__query"]) → 回"已激活: mysql__query";
  4. 下一轮 mysql__query 的完整 schema 进了工具列表,模型直接调它执行 SQL。

整个过程,上下文里始终只有 3 个元工具 + 真正被激活的那一两个,其余几十个工具全程休眠、不占上下文。

6. 什么时候该用、什么时候不必

这套方案不是无脑全上:

  • 工具就三五个:没必要,直接全激活(把开关设成 True)更省事。
  • 工具几十上百个、或上下文窗口吃紧:按需激活的收益明显,代价是首次用到某工具时多一两轮"搜索 → 激活"的往返(延迟换上下文)。
  • 它和"压缩历史消息"是两条互补的线:一个砍工具列表的常驻开销,一个砍对话历史的堆积,长对话两个一起上才稳。

完整实现在 milu 仓库的 src/milu/tools/registry.pycatalog.py,几百行,想抄思路或直接看代码都行。

项目地址:GitHub 搜 stephonGAO/milu(中文 LLM 一等公民的多用户 Agent 框架)

你在接 MCP 时遇到过工具太多导致模型选错、或上下文被 schema 撑爆的情况吗?评论区说说你的处理办法,我会一条条回。

相关推荐
TickDB2 小时前
统一行情 API 查 A 股、港股、美股和数字货币:code=0 不代表 symbol 一个没少
人工智能·python·websocket·mcp·行情数据 api
何忆清风4 小时前
如何绕过Cursor Pro限制直接调用本地模型
ai
滴图服务-七七7 小时前
滴滴地图:精准定位赋能企业数字化转型
大数据·人工智能·地图服务·甲级测绘资质·商业授权
爱学习的程序媛7 小时前
2026上半年大模型全景技术解读:推理融合、Agent 爆发与多模态统一
人工智能·ai
A.说学逗唱的Coke9 小时前
【大模型专题】向量数据库深度解析:从原理到实战,构建企业级 AI 知识检索底座
数据库·人工智能
果丁智能9 小时前
智能锁赋能网约房民宿数字化管控:身份核验+远程授权,筑牢安全防线、降本增效
网络·数据库·人工智能·安全·智能家居
V搜xhliang02469 小时前
AI智能体的数据安全与合规实践
人工智能·学习·数据分析·自动化·ai编程
大貔貅喝啤酒9 小时前
Python Requests库教程
自动化测试·python·requests库
PPIO派欧云9 小时前
PPIO登上贵州新闻联播,深化AI算力生态建设
人工智能