AI 技术
#MCP#工具调用#上下文工程#Agent#JSON Schema

MCP 工具定义注入如何挤占上下文:工具 schema 的 token 开销与按需加载机制

接入多个 MCP Server 的 Agent 为什么越接越笨?本文从工具名、描述与 JSON Schema 被序列化进系统提示并随每轮请求重复计费讲起,分析工具数量增长带来的上下文挤占与选择准确率下降,对比全量注入、检索式筛选与动态发现三种加载策略的延迟与可靠性权衡,并给出可观测指标与适用边界。

一个越接越慢的 Agent

假设你在维护一个企业内部 Agent,它通过 MCP 接入了四个 Server:代码仓库、工单系统、数据仓库元数据、部署平台。每个 Server 暴露 20~40 个工具,合计约 120 个。上线第一周一切正常,第二周开始有人反馈:让它查一个工单,它却去调了部署接口;让它跑一次 SQL,它把参数填成了另一个工具的字段名。同时账单上的输入 token 数稳定上涨,而用户的问题并没有变复杂。

直觉上的解释是“模型不够聪明”。但把同一批问题发给一个只接入 8 个工具的 Agent,选择准确率明显更好。模型没变,工具没变,变的只是系统提示里那段工具定义的长度。这提示我们:问题可能不在推理能力,而在上下文预算的分配方式。

MCP(Model Context Protocol,模型上下文协议)是 Anthropic 在 2024 年 11 月开源的开放标准,用来把 AI 助手连接到数据源和业务系统。协议规定 Server 通过 tools/list 暴露工具清单,每个工具包含 name、可选的 titledescriptioninputSchema(参数的 JSON Schema)以及可选的 outputSchemaannotations。客户端拿到这份清单后,通常把它序列化进系统提示,让模型“看见”有哪些工具可用。

关键在于:这份清单不是一次性成本。它进入的是每一轮请求都要重新发送的系统提示前缀,因此它的 token 开销按轮次重复计费,并且和对话历史、检索到的文档、用户输入争夺同一个上下文窗口。

工具定义到底占了多少 token

一个工具定义被序列化后包含哪些字段,直接决定了它的体积。以协议文档中的天气工具为例:

{
  "name": "get_weather",
  "title": "Weather Information Provider",
  "description": "Get current weather information for a location",
  "inputSchema": {
    "type": "object",
    "properties": {
      "location": {
        "type": "string",
        "description": "City name or zip code"
      }
    },
    "required": ["location"]
  }
}

这段定义里,真正影响模型决策的是三部分:工具名、描述、以及每个参数的名称、类型和参数级描述。title 主要给人看,outputSchemaannotations 对调用决策帮助有限,但它们同样会被序列化,同样消耗 token。

工程上通常观察到的规律是:一个参数简单、描述克制的工具,序列化后大致在几十到一百多个 token;一旦参数变成嵌套对象、枚举、数组,或者描述写成一段使用说明,单个工具轻松超过几百 token。120 个工具的工具清单,量级上足以吃掉一个中等上下文窗口的相当一部分。这里不给具体数字,因为 token 数取决于分词器、字段是否精简、描述长度,任何脱离具体清单的估算都不可靠。

更麻烦的是结构性问题,而不只是总量:

  • 重复计费。工具定义位于系统提示,每轮请求都要重发。对话轮次越多,同一份定义被计费的次数越多。
  • 不可压缩的语义密度低。JSON Schema 的括号、引号、字段名占了不少 token,但对模型判断“该不该用这个工具”贡献很小。
  • 描述质量参差。不同 Server 由不同团队维护,描述风格不统一,有的写“查询数据”,有的写三段使用注意事项。模型要在这种不均匀的信号里做选择。

工具变多为什么选择会变差

上下文被挤占只是成本问题,选择准确率下降才是质量问题。这两者往往同时出现,但机制不同。

第一种机制是注意力稀释。当系统提示里并列着 120 个结构相似的工具定义时,模型需要在其中定位与当前意图匹配的那一个。工具名相近(get_ticketget_ticket_detaillist_tickets)、描述重叠、参数名雷同,都会增加混淆概率。工具数量少的时候,这种混淆不明显;数量上去之后,边界模糊的工具对开始互相干扰。

第二种机制是上下文预算挤压。工具定义占用的空间,本可以留给检索到的文档、对话历史或用户提供的长输入。当工具清单膨胀到一定程度,客户端可能被迫截断历史或减少检索结果条数,模型因此丢失了判断所需的信息。表现上像是“模型忘了前面说过什么”,根因却在工具清单。

第三种机制是schema 复杂度带来的参数错误。参数越多、嵌套越深,模型漏填必填字段、把字符串填进整型、把枚举值写错的概率越高。腾讯云开发者社区的一篇文章在描述 Agent 三件套时明确提到,Agent 调用工具时“参数一多就丢字段,复杂嵌套就误填”,因此需要在调用链路上加一层参数校验与补全。这从侧面说明,schema 复杂度本身就是一类失败来源,而它和工具数量是叠加的。

需要区分的是:这三种机制中,哪些有直接证据、哪些是工程经验。协议文档只规定了工具定义的结构,并没有规定它必须全量注入系统提示,也没有给出数量与准确率的关系曲线。上面关于注意力稀释和预算挤压的描述,属于工程上常见的解释框架,具体阈值必须由你自己的评估集测出来,不能照搬。

三种加载策略的机制差异

既然全量注入有代价,替代方案就出现了。下面三种策略在工程上都有实际使用,它们的差别不在“好不好”,而在把复杂度放在了哪一层。

全量注入

启动时向所有已配置的 Server 发送 tools/list,把完整清单注入系统提示。这是最直接的实现,也是很多框架的默认行为。腾讯云那篇文章描述 CodeBuddy 框架时写道:Agent 启动时向所有 MCP Server 发送 tools/list 拿到完整工具清单,工具定义被注入 LLM 的 system prompt。

它的优点是确定性:模型在任何一轮都能看到全部工具,不存在“该用的工具没被检索出来”的问题;实现也最简单,不需要额外的检索组件或路由逻辑。缺点是成本随工具数量线性增长,且选择准确率在工具多、边界模糊时下降。

检索式工具筛选

在每轮请求前,用当前用户输入去检索一个工具索引,只把 Top-K 个相关工具注入系统提示。索引可以基于工具名和描述的文本嵌入,也可以基于关键词或规则。

它的核心假设是:当前意图只和少数几个工具相关。这个假设在工具按业务域清晰分组时成立得比较好;当用户意图跨域、或者需要组合多个工具完成一个流程时,检索可能漏掉关键工具。检索组件本身也引入延迟和新的失败模式——检索错了,模型再强也调不到对的工具。

动态工具发现

不预先注入完整清单,而是让模型先通过一个元工具(例如“搜索可用工具”)按需查询,拿到候选工具定义后再决定调用。MCP 协议本身支持 tools/list 分页,也支持 listChanged 能力:当工具列表变化时,Server 会发送 notifications/tools/list_changed 通知。这为动态发现提供了协议基础。

腾讯云文章在对比 MCP 三种原语时给出了一个相关观察:Resources 采用惰性拉取,Agent 在对话中按需发起 resources/read,而不是启动时全量拉取,因此“量大不占 prompt 空间”。工具的动态发现是类似思路,但代价是多一次往返:模型要先决定“我需要找工具”,再发起查询,再拿到定义,再调用。

贯穿场景:四个 Server 的按需加载流程

把上面的策略放回开头那个四 Server 的场景。假设我们采用“检索式筛选 + 动态发现兜底”的混合方案:启动时只注入每个 Server 的工具数量摘要和少量高频工具,其余工具通过检索按需拉取。

flowchart TD
    A[用户提问] --> B[提取意图关键词]
    B --> C[检索工具索引]
    C --> D{命中数是否足够}
    D -->|是| E[注入 Top-K 工具定义]
    D -->|否| F[调用元工具搜索可用工具]
    F --> G[向相关 Server 发 tools/list]
    G --> H[返回候选工具定义]
    H --> E
    E --> I[模型生成工具调用]
    I --> J[客户端执行 tools/call]
    J --> K{调用是否成功}
    K -->|参数错误| L[返回错误信息给模型]
    K -->|成功| M[返回结果]
    L --> I

这个流程的关键转折点在 DK 两处。D 处如果检索命中不足,就必须走动态发现,否则模型会“看不到”需要的工具;K 处如果参数校验失败,错误信息要回传给模型让它重试,而不是直接失败——这是 schema 复杂场景下的必要兜底。

流程里没有画出来但同样重要的是缓存:同一个会话内,已经拉取过的工具定义应该缓存,避免每轮都重新检索或重新请求。工具列表变化时,listChanged 通知会触发缓存失效。

三种策略的权衡对比

维度全量注入检索式筛选动态发现
每轮注入的 token随工具总数线性增长只含 Top-K,基本恒定初始极小,按需增长
首轮延迟低,无额外步骤增加一次检索增加一次元工具往返
工具召回可靠性最高,不存在漏检依赖检索质量,可能漏检依赖模型是否主动搜索
选择准确率工具多时下降命中时较好候选少时较好
实现复杂度最低需要索引与检索组件需要元工具与缓存失效处理
对工具列表变化的响应重新拉取全量更新索引依赖 listChanged 通知
主要失败模式上下文挤占、工具混淆检索漏召回、跨域意图模型不搜索、多轮往返

这张表里的“较好”“下降”是定性描述。没有资料给出这三种策略在统一评估集上的量化对比,因此任何具体百分比都不应被引用。实际选型要靠自己的评估集测量。

从工程经验看,一个常见的折中是:把高频、边界清晰的工具全量注入,把长尾、低频、参数复杂的工具放进检索或动态发现。这样既保住了大部分场景的确定性,又限制了上下文开销的上界。

可观测指标与失败定位

要判断问题是否真的出在工具定义上,需要几个可测量的信号:

  • 每轮请求的工具定义 token 数。把它和对话历史、检索文档的 token 数分开统计,才能看出上下文预算被谁占用。
  • 工具选择错误率。按“调用了错误工具”“该调用却没调用”“参数校验失败”分类统计,而不是只看总成功率。这三类的根因不同。
  • 检索命中率(使用检索式筛选时)。记录每轮检索返回的 Top-K 中,最终被调用的工具是否在列。命中率低说明索引或查询构造有问题。
  • 动态发现触发频率。如果模型频繁发起元工具搜索,说明初始注入的工具集覆盖不足;如果从不触发,可能是提示没有引导它去搜索。
  • 工具列表变更事件listChanged 通知的频率和缓存失效次数,能反映 Server 端工具是否稳定。

出现“模型调错工具”时,先看工具定义 token 数和错误类型分布。如果 token 数很高且错误集中在名称相近的工具对,优先考虑精简描述、合并语义重叠的工具,而不是换模型。

适用边界与仍未解决的问题

三种策略都不是通用解,它们的适用条件不同:

全量注入适合工具总数可控(工程上常说的经验是几十个以内,但具体阈值取决于模型和描述长度)、工具边界清晰、延迟敏感的场景。工具数量一旦超出模型能稳定区分的范围,它的收益就开始反转。

检索式筛选适合工具按业务域分组、用户意图相对集中的场景。它的风险在于跨域意图和组合流程——用户一句话可能同时需要查工单和查部署记录,检索只返回其中一类。

动态发现适合工具数量大、变化频繁、且能接受多一轮往返的场景。它的前提是模型有足够的能力判断“我需要先找工具”,这一点在弱模型上不一定成立。

还有几个问题目前没有稳定答案:工具定义的 token 开销如何在不同分词器和不同模型上换算,没有通用公式;工具数量与选择准确率的关系曲线,取决于工具之间的语义距离,无法脱离具体工具集讨论;MCP 协议本身没有规定客户端必须如何注入工具定义,因此不同框架的行为差异很大,跨框架的可比性有限。

回到开头那个 Agent。真正要做的不是把 120 个工具一次性塞进系统提示,也不是简单地砍掉一半工具,而是先测量工具定义占了多少预算、错误集中在哪一类,再决定用哪种加载策略。工具定义是上下文预算的一部分,和检索文档、对话历史一样需要被管理,而不是默认它免费。

资料来源

  1. Model Context Protocol Specification - Tools
  2. Anthropic - Introducing the Model Context Protocol
  3. OpenAI - Function Calling Guide
  4. 一文讲透Agent三件套:MCP、Skill、Hook如何给大模型装上护栏-腾讯云开发者社区-腾讯云