减少 MCP 令牌消耗:我们如何把工具定义削减 60%
代理能调用的每个 MCP 工具,都是模型可能在每次请求时读取的一段 JSON。我们的工具大约有 95,000 个令牌。本文介绍如何在两分钟内测量你自己的工具,我们在不删除任何工具的前提下削减了什么,以及防止它再次膨胀的预算检查。
10 月 27 日,一位付费用户取消了 AgentsRoom 订阅,理由是令牌。反驳之前,我们先测了测应用给它启动的每个代理额外加了什么。
最大的数字出乎意料。启动指令很小。交给每个代理的 MCP 工具(待办列表、项目记忆、代理间邮件、终端、SSH、数据库等)共 191 个,约 380 KB JSON。按每个令牌约 4 字节算,光工具定义就有约 95,000 个令牌。一周前还是 78,000。没人这样决定过,只是每个新工具都加了自己的一段话。
我们开发的 AgentsRoom 是一款桌面应用,并排运行 Claude Code、Codex 等编码 CLI,并为它们提供这些工具。下面讲我们测到了什么、在不删任何工具的前提下削减了什么,以及防止反弹的检查。这些都适用于你自己的 MCP 服务器。
简短回答
- 工具定义是模型可能在每次请求时读取的 JSON:名称、描述、输入 schema。按每个令牌约 4 字节估算。
- 每轮是否都要付这笔费用,取决于 CLI。 Claude Code 默认延迟加载 MCP 工具,需要时才加载定义。有些 CLI 每次请求都发送全部 schema。
- 先测量,再削减。 用 Claude Code 的
/context,或用一个 40 行脚本向任意服务器索取工具列表。 - 缩短文字,保留工具。 描述只说做什么、何时用。使用手册移到代理按需读取的文档。我们的工具从约 95,000 个令牌降到约 38,000 个,只剩原来的 2.5 分之一。
- 在构建里设一个上限。 没有它,定义会自己反弹。
工具定义的成本,以及由谁来付
MCP 客户端连上服务器后,会请求工具列表。每一项包含名称、写给模型的描述和参数的 JSON schema。客户端把列表转成工具定义发给模型,模型由此知道工具存在、该怎样调用。
所以描述里的每个字都是模型的输入。读的频率取决于客户端:
- Claude Code 默认延迟加载 MCP 工具。 其文档(2026 年 11 月 3 日读取,已安装 Claude Code 2.1.314)说,会话开始时只加载工具名称和服务器说明,Claude 需要某工具时才通过工具搜索加载其定义。在
ENABLE_TOOL_SEARCH=false时、auto模式下低于上下文窗口 10% 阈值时、早于 Claude Opus 4 和 Sonnet 4 的模型上,或对标记了alwaysLoad的服务器,定义会预先加载。 - 其他 CLI 的做法不同。 在我们 10 月 28 日的测试中,让代理只回答“OK”:Claude Code 在我们的工具延迟加载时从 67,500 个令牌起步,Codex CLI 0.164 从 25,000 个令牌起步,没有整块发送我们的 schema,而 OpenCode 连接时就加载了我们的 175 个工具和 19 个浏览器工具,每次请求都发送。同一服务器,账单相差很大。
工具列表不变时,提示缓存能减轻成本:首次请求后,稳定的前缀以输入价格的一小部分从缓存读取。但它不会归还空间。每次请求,这些定义仍占着上下文窗口,紧挨着你的代码和对话。延迟加载模式也有代价:每个工具首次使用前要多一步搜索。
两分钟测量你的工具
在 Claude Code 中,运行 /context。它按来源拆分上下文窗口,并标出延迟或按需加载的 MCP 工具。在项目里开新会话,输入任何内容前运行,就能看到开工前配置占了多少。
对任何服务器、任何 CLI,直接问服务器。stdio 上的 MCP 服务器使用 JSON-RPC,每行一条消息:发送 initialize,再发送 tools/list,然后测量返回内容。下面的脚本就做这件事,只用 Node,零依赖:
// Usage: node mcp-tool-cost.mjs <command that starts the server> [args...]
import { spawn } from 'node:child_process';
const [cmd, ...args] = process.argv.slice(2);
const server = spawn(cmd, args, { stdio: ['pipe', 'pipe', 'ignore'], shell: process.platform === 'win32' });
const tools = [];
let buffer = '';
const send = (msg) => server.stdin.write(JSON.stringify({ jsonrpc: '2.0', ...msg }) + '\n');
const listTools = (cursor) => send({ id: 2, method: 'tools/list', params: cursor ? { cursor } : {} });
server.stdout.on('data', (chunk) => {
buffer += chunk;
let end;
while ((end = buffer.indexOf('\n')) >= 0) {
const line = buffer.slice(0, end).trim();
buffer = buffer.slice(end + 1);
if (line) handle(JSON.parse(line));
}
});
function handle(msg) {
if (msg.id === 1) {
send({ method: 'notifications/initialized' });
listTools();
} else if (msg.id === 2) {
tools.push(...msg.result.tools);
if (msg.result.nextCursor) listTools(msg.result.nextCursor);
else { report(); server.kill(); }
}
}
function report() {
const rows = tools
.map((tool) => ({ name: tool.name, bytes: Buffer.byteLength(JSON.stringify(tool)) }))
.sort((a, b) => b.bytes - a.bytes);
const total = rows.reduce((sum, row) => sum + row.bytes, 0);
for (const row of rows.slice(0, 10)) {
console.log(`${String(row.bytes).padStart(7)} bytes ~${Math.round(row.bytes / 4)} tokens ${row.name}`);
}
console.log(`${rows.length} tools, ${total} bytes, about ${Math.round(total / 4)} tokens`);
}
send({
id: 1,
method: 'initialize',
params: { protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 'tool-cost', version: '1.0.0' } },
});
用 CLI 启动服务器的同一条命令来运行它,例如 node mcp-tool-cost.mjs npx -y @playwright/mcp@latest。它会打印最重的十个工具和总量。我们在 2026 年 11 月 3 日得到的结果:
| 服务器 | 工具数 | JSON | 约合 |
|---|---|---|---|
@modelcontextprotocol/server-filesystem 2.1.0 | 14 | 12,554 字节 | 3,100 个令牌 |
@playwright/mcp 0.0.84 | 25 | 21,034 字节 | 5,300 个令牌 |
| AgentsRoom 工具,10 月 28 日削减前 | 191 | 约 380,000 字节 | 95,000 个令牌 |
| AgentsRoom 工具,10 月 28 日削减后 | 191 | 约 152,000 字节 | 38,000 个令牌 |
| AgentsRoom 工具,11 月 3 日 | 203 | 159,957 字节 | 40,000 个令牌 |
每个令牌 4 字节只是估算,够用来比较服务器、看趋势,不能用来预测账单。真实的分词器随模型和文本而变,我们在数了自己的 38 亿个令牌时就发现了。
上面的文件服务器都算合理。问题在总和:五个这种规模的服务器,不算我们这种,已有 20,000 个令牌。
我们削减了什么,又没动什么
规则是:不删除任何工具,不删除任何参数,不删除任何允许的取值或必填字段。 代理必须保留全部能力,能改的只有文字。
- 工具描述用一到三句话说明做什么、何时用。 不讲怎么做。字节都堆在“怎么做”上:规则、默认值、边界情况、示例,以及代理某次用错工具后补写的警告。
- 一个参数给一句话,或者什么都不写,前提是名称、类型和允许取值列表已说明一切。多个工具反复出现的参数(
project、author、工单 id)统一用一句标准说明。 - 使用手册是搬走,不是删掉。 每个工具族有一个说明文件,代理首次调用该工具族前会先读它。我们最大的工具(保存代理团队的那个)现在只在代理请求时通过工具调用返回完整字段参考,不再夹带在 schema 里。
- 第二遍核对了每个事实。 从描述删掉的每句话都在说明文件里查过,缺的就补上。
部分结果,按每个工具的 JSON 字节数:
| 工具 | 之前 | 之后 |
|---|---|---|
teams_save | 19,705 | 9,791 |
triggers_save | 16,014 | 4,071 |
backlog_update | 13,150 | 4,253 |
backlog_create | 11,565 | 4,399 |
总计 380 KB 变成了 152 KB。原有十三个工具超过 4 KB,现在还剩四个,因为它们字段太多。整件事的思路是:选工具时,模型只需知道它存在、何时调用。细节可以等到使用时再说。
写 MCP 服务器的话,Claude Code 还给了一个理由:一开始就加载的工具描述会在 4,096 个字符处截断,用搜索匹配描述时也只读这个限度内的部分。之后的重要内容可能永远不会被看到。
启动文件也用同样的办法
工具 schema 不是唯一的固定成本。代理在用工具前,还会读一份工具说明文件,已长到 57.7 KB,约 14,000 个令牌,每次启动和恢复都完整读取。有用户反馈,恢复后的 Codex 代理几分钟就触及工作上下文上限。
解决思路相同。文件变成一个约 11 KB 的索引:适用于所有工具的规则,加上每个工具族一行,指向它自己的文件。31 个工具族文件只在首次调用该族前读取,文字一字未改。只碰待办列表的代理,永远不会读到数据库部分。
这也适用于很长的 CLAUDE.md 或 AGENTS.md:始终加载的文件只保留每个任务都需要的内容,其余放链接。详见我们的 AGENTS.md 与上下文文件指南。
让推送失败的预算
最让我们担心的不是 95,000,而是一周前的 78,000。每个工具加一段话,每段都合情合理,总量却在七天里涨了 20%。
于是大小变成了一项构建检查。一个 JSON 文件记录预算:
{
"totalBytes": 160000,
"perToolBytes": 4000,
"oversized": {
"teams_save": 9791,
"triggers_save": 4071,
"backlog_update": 4253,
"backlog_create": 4399
}
}
脚本在沙箱里启动我们的 MCP 服务器,向每个服务器索取工具,按字节测量 JSON.stringify(tool),总数超过 totalBytes 或某个工具超过 perToolBytes 时报错退出。原本就过大的四个工具连同大小一起列出:一个只能单向转动的棘轮。它们可以变小,不能变大;其中一个变小时,脚本会要求调低数字。它在每次推送前和发布构建中运行。上限可以提高,但那是写进文件的决定,不是让检查通过的手段。
六天后的 11 月 3 日,同一检查读到 203 个工具、159,957 字节:多了十二个工具,离上限仅 43 字节。下一个工具得自己腾地方。这正是这项检查的用意。
关掉你用不到的
最便宜的令牌,属于代理从来看不到的工具。
- 只在需要的项目里启用服务器。 用户级别声明的服务器会跟着你进入每个仓库。在 Claude Code 中,放进项目的
.mcp.json,它就只待在有用的地方。 alwaysLoad只留给少数几个工具。 Claude Code 的文档说得很直白:每个预先加载的工具都会占用本可留给对话的上下文。- 除了定义,也要留意工具结果增加了什么。 返回整页或整张表的工具,一次调用的成本可能超过其定义一整天的开销。
在 AgentsRoom 中,设置 > AgentsRoom 工具列出我们的每个工具族及其大小,你可以针对所有项目、某个项目、某个文件夹或某个代理,关闭一个工具族或单个工具。关闭的工具根本不会列给代理,不占任何上下文。AgentsRoom MCP 页面介绍了这些工具,会话追踪则按大小排序显示填满代理上下文的内容,含 MCP 工具。
我们没有改动的部分
同一次测量还发现了另外两项成本,我们有意没动。
- 启动简报:告诉每个代理其角色和状态汇报方式,Claude 的中位数约 5,000 个令牌,约占 200,000 令牌窗口的 2.5%,从第二次请求起从缓存读取。
- 每个代理在一轮结束时写的状态文件:基于 71 份本地 Claude 转录测量,约占上下文令牌的 2.5%。
缩减任何一项,省下的都不到窗口的 3%,还可能损害代理的工作质量。工具定义有 95,000 个令牌,时间花在了那里。
大家真正会问的问题
MCP 工具会消耗多少令牌?
每个工具的成本是它的完整定义:名称、描述和输入 schema,以 JSON 发送。粗略按每个令牌 4 字节计。2026 年 11 月 3 日实测:官方 filesystem 服务器 14 个工具约 3,100 个令牌,Playwright MCP 25 个工具约 5,300 个。AgentsRoom 的工具缩短前约 95,000 个令牌,如今约 40,000 个。
Claude Code 会把所有 MCP 工具都加载到上下文窗口里吗?
默认不会。Claude Code 的工具搜索(tool search)会延迟加载 MCP 工具定义:会话开始时只加载工具名称和服务器说明,Claude 需要某个工具时才加载其定义。设置 ENABLE_TOOL_SEARCH=false、使用早于 Claude Opus 4 和 Sonnet 4 的模型,或给服务器标记 alwaysLoad,定义就会重新在一开始进入上下文。
MCP 服务器能减少令牌消耗吗?
可以,前提是工具返回简短答案,替代代理原本要完整读取的内容,比如用数据库查询代替整份导出。但定义占用上下文,结果也一样:Claude Code 默认在单个 MCP 结果超过 10,000 个令牌时警告,在 25,000 个令牌处截断。只有结果比它替代的内容更小,MCP 服务器才省令牌。
怎样查看我的 MCP 工具占用了多少上下文?
在 Claude Code 中运行 /context:它按来源拆分上下文,并标出延迟加载的 MCP 工具。对任何服务器、任何 CLI,都可以向服务器索取工具列表并测量 JSON,本文的 40 行脚本做的就是这件事。
好的 MCP 工具描述是什么样的?
用一到三句话说明工具做什么、何时调用,把代理会搜索的词放在前面。一个参数一句话;名称、类型和允许取值已说明一切时就不写。规则、边界情况和长示例放进代理按需读取的文档。况且 Claude Code 本就会把预先加载的描述截断在 4,096 个字符。
怎样防止工具定义再次膨胀?
给它们一个构建能检查的预算。我们有一个 JSON 文件,写着总上限和单工具上限,还有一个脚本:启动我们的 MCP 服务器,测量每个工具,超过任一上限就报错退出,每次推送前运行。原本就过大的工具可以变小,不能变大。
简而言之
你的 MCP 工具是模型要读的文字,却没人像审查代码那样审查它。先测一次:Claude Code 里用 /context,任何服务器都可用上面的脚本。然后让每条描述只说做什么、何时用,把“怎么做”挪到代理需要时才读的文件里,再在构建里放一个数字防止反弹。
我们没删任何工具,就从约 95,000 个令牌降到约 38,000 个。如果你用同一套工具运行多个代理,AgentsRoom 可免费下载,MCP 服务器页面介绍了如何只声明一次服务器,用于所有 CLI 和项目。
下载 AgentsRoom
在一个窗口中运行你所有项目的所有 AI 代理。
配套应用:随时随地监控你的 Agent
使用 Claude、Codex、Antigravity CLI 或其他 AI 提供商。
把 Bug 和需求直接发送到您的公开待办清单。
继续阅读
Claude 的令牌是什么,又是怎么运作的?我们数了 38 亿个
令牌是词的一个片段,Claude 会统计四种令牌:输入、缓存写入、缓存读取和输出。我们统计了自己 30 天的 Claude Code 会话,共 38 亿令牌:98% 是从缓存里重新读取的对话,Claude 写出的只占 0.3%。每种令牌的成本、为什么会话每多一轮就更重,以及 5 小时窗口到底在计量什么。
阅读全文Claude Code 到底有多快?每秒令牌数,基于 20,000 轮实测
Claude Code 从不显示输出速度,但每个会话转录里都有计算它所需的全部数据。我们用一个 40 行的脚本跑了自己的 319 个会话、20,408 轮、1,200 万输出令牌:按中位数,Opus 5 每秒输出 63 个令牌,Opus 5.5 为 95,Sonnet 5 为 77,而且短回答总是比长回答慢。本文给出方法、脚本、数据,以及快速模式会带来什么变化。
阅读全文如何看到 AI 编码代理在你的服务器上执行了什么
代理正在一台生产服务器上干活,终端里只显示它在调用一个工具。它到底执行了哪条命令,机器又返回了什么?会话转录事后会告诉你。本文讲我们如何让这一切在发生时就可见,最初试过什么,以及不用我们的应用也能做到同样效果的方法。
阅读全文