☰
MCP接入后Token消耗失控?抓包实测与六种压缩手段
2026/10/7 22:57:43 网站建设 项目流程

1. 从一次账单异常说起:MCP 接入后 token 消耗为何失控

事情的起因很简单。我把 OpenClaw 接上了 MCP(Model Context Protocol),想让本地工具链和模型之间打通,省去每次手动复制粘贴上下文的麻烦。配置跑通的那一刻确实很爽,工具调用顺畅,文件读写、命令执行、网页抓取都能自动完成。但用了不到一周,我注意到一个很不对劲的现象:明明每天对话轮次没增加多少,token 用量却翻了好几倍。

一开始我以为是模型本身的问题,换了个更小的模型测试,结果 token 消耗依然偏高。后来我把每一轮请求的原始 payload 抓出来逐条对比,才发现问题根本不在模型,而在 MCP 的工作机制本身。每一次对话,MCP 都会把工具定义、工具描述、参数 schema 全部塞进上下文,而这些内容在每一轮对话里都会重复发送。换句话说,你以为只是问了一句话,实际上背后附带了成百上千 token 的工具元数据。

这个发现让我意识到,很多人接 MCP 的时候只关注"能不能跑通",却忽略了"跑通之后每轮对话的真实成本"。这篇内容就是把我这段时间踩过的坑、抓包分析的过程、以及最终把 token 消耗压下来的具体做法完整梳理出来。如果你正在用 OpenClaw 接 MCP,或者准备接,那这些经验应该能帮你少走不少弯路。

提示:本文讨论的 token 消耗问题,适用于所有基于 MCP 协议的客户端与工具集成场景,不限于 OpenClaw 本身。

2. MCP 到底往上下文里塞了什么

2.1 工具定义是如何进入每一轮请求的

要理解 token 为什么烧得快,得先搞清楚 MCP 的通信结构。MCP 本质上是一套客户端与工具服务端之间的协议,客户端启动时会向 MCP Server 请求可用工具列表,拿到一份包含工具名称、功能描述、参数定义的清单。这份清单不会只存在本地,而是会被注入到模型的系统提示或工具调用区域,让模型知道"现在有哪些工具可以用、每个工具接受什么参数"。

关键在于,这份工具清单是随每一轮对话请求一起发送的。模型本身是无状态的,它不记得上一轮你告诉过它什么工具,所以客户端必须在每次请求时重新把工具定义带上。一个工具的描述加上参数 schema,少则几十 token,多则两三百 token。如果你接了五六个 MCP Server,每个 Server 暴露十几个工具,那光是工具定义就能轻松突破三千 token。

我实测过一组数据:接入一个包含 12 个工具的 MCP Server,工具定义部分稳定占用约 1800 token。这意味着哪怕你只发一个"你好",实际请求也是 1800+ token 起步。对话轮次越多,这部分固定开销被重复计费的次数就越多。

2.2 工具返回值与中间结果的累积效应

除了工具定义,第二个 token 黑洞是工具返回值。MCP 工具执行完会把结果返回给模型,模型再基于结果继续推理。问题在于,很多工具返回的内容非常冗长——比如读取一个文件返回全文、执行命令返回完整日志、抓网页返回整页 HTML。这些内容一旦进入对话历史,就会在后续每一轮里被反复携带。

我遇到过一个典型场景:让 OpenClaw 读取一个 500 行的配置文件,工具返回了完整内容,大约 4000 token。之后我又追问了几个问题,每一轮对话都带着这 4000 token 的历史。问了五轮,光这一个文件就被重复计费了五次,累计两万 token。而实际上,后面几轮根本不需要完整文件内容,只需要其中几个关键字段。

2.3 系统提示与工具描述的叠加

第三个容易被忽略的点是系统提示。OpenClaw 本身会带一段系统提示,MCP 接入后又叠加了工具使用说明、调用规范、错误处理指引。这些内容同样是每轮必带。有些 MCP Server 的描述写得极其啰嗦,一个工具的功能说明能写五六行,参数描述逐条展开,这些都会实打实变成 token。

把这三部分加起来,你就明白为什么"每次对话都在偷偷烧 token"了。工具定义是固定税,工具返回值是浮动税,系统提示是隐形税,三者叠加,token 消耗自然失控。

3. 抓包实测:一轮对话的真实 token 构成

3.1 抓取原始请求的方法

光靠猜没用,得拿到真实数据。我的做法是在 OpenClaw 和模型 API 之间加一层日志代理,把每次请求的完整 body 记录下来。具体操作是找到 OpenClaw 的模型配置项,把 base URL 指向本地一个转发服务,转发服务负责记录请求再原样转发出去。这样既不改变功能,又能拿到最原始的 payload。

记录下来的请求体是 JSON 格式,里面 messages 数组包含系统提示、历史对话、当前输入,tools 数组包含所有 MCP 工具定义。我用一个简单的脚本统计各部分字符数,再按经验比例换算成 token(英文约 4 字符 1 token,中文约 1.5 字符 1 token)。

3.2 各部分占比的真实数据

下面是我接入两个 MCP Server(一个文件操作类,一个命令执行类,共 18 个工具)后,一轮简单对话的 token 构成实测:

组成部分估算 token占比
系统提示6209%
MCP 工具定义(18 个)264038%
历史对话185027%
工具返回结果142020%
当前用户输入3806%
合计6910100%

这组数据很说明问题。用户实际输入只占 6%,而工具定义加工具返回结果占了 58%。也就是说,你花的钱里超过一半是在为 MCP 的元数据和中间结果买单,真正用于"对话"的部分反而是小头。

3.3 多轮对话下的放大效应

单轮看可能还不觉得夸张,但多轮对话会把这个效应放大。假设每轮固定开销(系统提示+工具定义)是 3260 token,历史对话每轮增长 500 token,那么第 10 轮对话的请求量大约是 3260 + 500×10 = 8260 token。而如果这 10 轮里每轮都触发一次工具调用,工具返回结果再叠加进去,轻松破万。

我统计过一整天的使用情况:实际有效对话内容约 8000 token,但总消耗接近 12 万 token,放大倍数达到 15 倍。这个数字才是"偷偷烧 token"的真相。

4. 把 token 压下来的六种实操手段

4.1 精简工具集:只挂当前任务需要的 Server

最直接有效的一招,是不要把所有 MCP Server 一直挂着。很多人图省事,配置一次就全开,结果每次对话都带着一堆根本用不到的工具定义。我的做法是按任务场景分组,比如写代码时只挂文件操作和命令执行,查资料时只挂网页抓取,需要哪个开哪个。

具体到 OpenClaw 的配置,可以在启动参数或配置文件里控制启用哪些 MCP Server。如果你用的是支持动态加载的客户端,甚至可以做到对话中途按需加载。实测下来,把 18 个工具精简到 6 个,工具定义部分从 2640 token 降到约 880 token,单轮省下近 1800 token。

4.2 压缩工具描述:改写 schema 里的冗余文字

MCP 工具的描述文字是可以改的。很多 Server 自带的描述写得非常啰嗦,什么"此工具用于在指定路径下执行文件读取操作,支持多种编码格式,返回文件完整内容……",其实完全可以压缩成"读取文件,参数:path"。模型理解工具用途并不需要那么多修饰词。

我把自己常用的几个 MCP Server 的工具描述全部重写了一遍,保留功能说明和参数定义,删掉所有客套话和重复解释。18 个工具的描述从平均 145 token 压到 55 token,整体省下约 1600 token。这个改动是一次性的,但收益是每轮对话都享受。

注意:改写工具描述时要保证参数名和类型准确,否则模型可能调用失败。功能说明可以精简,但参数 schema 不能动。

4.3 截断工具返回值:别把整个文件塞进上下文

工具返回结果是浮动开销的大头,必须控制。我的做法是在 MCP Server 侧或客户端侧加一层结果处理逻辑:文件读取只返回前 N 行加总行数,命令执行只返回最后 N 行加退出码,网页抓取只返回正文文本去掉 HTML 标签。

以文件读取为例,我设置默认只返回前 100 行,如果模型需要更多再让它显式请求指定行范围。这样一次读取从 4000 token 降到 400 token 左右。命令执行同理,日志动辄几千行,只保留尾部关键部分就够了。这一招对 token 的压缩效果最明显,因为工具返回值往往是单次请求里最大的块。

4.4 控制历史长度:滑动窗口与摘要结合

历史对话不能无限增长。我采用的是滑动窗口加摘要的策略:保留最近 6 轮完整对话,更早的内容用一段简短摘要替代。摘要是让模型自己生成的,比如"之前讨论了配置文件读取和命令执行问题,已解决路径错误"。这样既保留了上下文连贯性,又不会让历史无限膨胀。

OpenClaw 如果支持自定义上下文管理,可以直接配置窗口大小。如果不支持,可以在客户端侧做预处理,把超出窗口的历史替换成摘要再发送。实测把历史从 20 轮压到 6 轮加摘要,历史部分 token 从 8000 降到 2500 左右。

4.5 关闭不必要的系统提示叠加

系统提示部分也有压缩空间。检查一下 OpenClaw 的默认系统提示和 MCP 注入的说明是否有重复。有些客户端会把工具使用规范写两遍,一遍在系统提示里,一遍在工具定义区。删掉重复部分,系统提示能从 620 token 降到 350 token 左右。

另外,如果你的使用场景比较固定,可以把系统提示里那些"通用助手"式的客套描述删掉,只保留必要的角色设定和输出格式要求。模型不需要你告诉它"你是一个乐于助人的助手"才能正常工作。

4.6 用缓存机制避免重复计费

部分模型 API 支持 prompt caching,对重复出现的系统提示和工具定义部分可以命中缓存,按更低的费率计费。如果你的模型服务支持这个特性,务必开启。MCP 的工具定义恰好是高度重复的内容,非常适合缓存。

开启缓存后,我实测工具定义部分的实际计费降到原来的 10% 左右。这一招不需要改任何业务逻辑,只是配置层面的调整,性价比极高。具体开启方式取决于你用的模型服务,一般在请求头或请求体里加一个缓存标记即可。

5. 配置层面的具体操作与参数

5.1 OpenClaw 的 MCP 配置结构

OpenClaw 的 MCP 配置通常放在一个 JSON 或 YAML 文件里,结构大致如下:

{ "mcpServers": { "filesystem": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir"], "enabled": true }, "shell": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-shell"], "enabled": false } } }

关键在enabled字段。我建议默认全部设为 false,需要时再手动开启对应的 Server。这样启动时不会把所有工具定义都加载进来。

5.2 工具描述改写的落点

工具描述一般由 MCP Server 提供,客户端只是转发。要改写有两个位置:一是直接改 Server 源码里的工具注册部分,二是客户端侧加一层拦截,在发送请求前替换 tools 数组里的 description 字段。前者一劳永逸但需要维护 fork,后者灵活但每次请求都要处理。

我选的是客户端侧拦截,因为改动可控且不影响 Server 升级。拦截逻辑很简单:遍历 tools 数组,对每个工具的 description 做字符串替换或直接覆盖成精简版。参数 schema 保持原样不动。

5.3 返回值截断的实现

返回值截断最好在 MCP Server 侧做,因为客户端拿到结果时已经晚了。如果 Server 是开源的,直接改它的返回逻辑。如果改不了,就在客户端收到工具结果后、塞进对话历史前做截断。

以文件读取为例,客户端侧的处理逻辑:

def truncate_tool_result(result, max_lines=100): lines = result.split("\n") if len(lines) <= max_lines: return result head = lines[:max_lines] return "\n".join(head) + f"\n... (共 {len(lines)} 行,已截断)"

这段逻辑放在工具结果进入 messages 之前执行即可。命令执行结果同理,保留尾部而不是头部,因为错误信息通常在最后。

5.4 历史窗口的配置参数

如果 OpenClaw 支持上下文窗口配置,找到类似max_history_turns或context_window的参数,设成 6 到 8 之间比较合适。太小会丢失上下文,太大则 token 浪费。我实测 6 轮是个平衡点,大部分任务在 6 轮内能完成,超出部分用摘要衔接。

摘要的生成可以复用模型本身,在历史被截断时触发一次轻量调用,让模型把被截断的部分总结成两三句话。这次调用的成本远低于保留完整历史。

6. 那些文档里不会写的踩坑经验

6.1 工具定义重复加载的隐蔽问题

有一次我发现 token 消耗突然翻倍,排查半天才发现是 MCP Server 被重复注册了。配置里同一个 Server 写了两遍,客户端加载时把工具定义也加载了两遍,模型看到的是两套一模一样的工具。这种问题不会报错,功能也正常,但 token 白白翻倍。建议定期检查配置,确保没有重复项。

6.2 工具调用失败后的重试放大

MCP 工具调用失败时,有些客户端会自动重试,每次重试都把完整的工具定义和失败结果再发一遍。如果失败原因是参数错误,重试三次就是三倍的 token 消耗。我的做法是关闭自动重试,或者把重试次数限制在 1 次,失败后直接把错误返回给模型让它自己调整参数。

6.3 流式输出下的 token 统计偏差

用流式输出时,很多客户端的 token 统计是不准的,因为它只统计了最终输出,没算上工具定义和中间过程。我建议不要依赖客户端自带的统计,而是以模型服务商后台的账单为准。我一开始就是被客户端的统计误导了,以为消耗不高,直到看账单才发现问题。

6.4 不同模型对工具定义的 token 计算差异

同一个工具定义,在不同模型上的 token 数是不一样的。中文描述在中文优化模型上更省,英文描述在英文模型上更省。如果你的工具描述是中英混杂的,建议统一成一种语言,并且和模型的主要语言一致。我实测把工具描述从中文改成英文后,在英文模型上省了约 15% 的 token。

6.5 缓存命中的条件与陷阱

prompt caching 不是无条件命中的。它要求前缀部分完全一致,包括系统提示、工具定义的顺序和内容。如果你每次请求的工具顺序不一样,或者描述里有动态内容(比如时间戳),缓存就命中不了。我踩过的坑是在工具描述里加了当前日期,导致缓存全部失效。后来把动态内容移到用户消息里,缓存命中率才恢复正常。

7. 一套可复用的 token 控制清单

把上面的经验整理成一份可执行的清单,每次接入新的 MCP Server 时对照检查:

  • 工具集按需加载,默认关闭不常用的 Server
  • 工具描述精简到功能加参数,删掉所有修饰性文字
  • 工具返回值做截断,文件读头部、日志读尾部、网页去标签
  • 历史对话用滑动窗口加摘要,窗口控制在 6 到 8 轮
  • 检查系统提示是否有重复,删掉冗余部分
  • 开启 prompt caching,确保前缀内容稳定不变
  • 关闭或限制工具调用自动重试
  • 定期检查配置,避免 Server 重复注册
  • 工具描述语言与模型主语言保持一致
  • 以服务商账单为准统计消耗,不依赖客户端显示

这份清单我贴在配置目录旁边,每次改动配置都过一遍。坚持下来,我的 token 消耗从最初的每天 12 万降到了 3 万左右,功能没有任何损失。

8. 关于成本与体验的取舍

把 token 压下来之后,我也重新思考了一个问题:MCP 带来的便利和它带来的成本,到底怎么平衡。我的结论是,MCP 的价值在于自动化,但自动化不等于无脑全开。真正高效的做法是让工具在需要的时候出现,用完就收起来,而不是一直挂在上下文里。

我现在的工作流是:日常对话不挂任何 MCP,需要操作文件或执行命令时再临时开启对应的 Server,任务完成后关闭。这样既享受了 MCP 的便利,又不会为闲置的工具定义持续付费。听起来多了一步操作,但省下来的成本和时间,远比那一步操作值。

另外,工具返回值的截断策略也需要根据任务调整。有些任务确实需要完整文件内容,这时候截断反而会导致模型反复请求,总消耗更高。我的经验是,对于探索性任务用截断,对于精确编辑任务给完整内容,根据任务类型切换策略,而不是一刀切。

最后分享一个小技巧:如果你不确定某个 MCP Server 值不值得挂,先单独挂它跑一天,记录 token 消耗和实际使用次数。如果使用次数很少但消耗很高,那它就不值得常驻。这个简单的评估方法帮我砍掉了好几个"看起来有用但实际很少用"的 Server。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询