減少 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 編碼代理在你的伺服器上執行了什麼
代理正在一台生產伺服器上幹活,終端裡只顯示它在呼叫一個工具。它到底執行了哪條命令,機器又返回了什麼?會話轉錄事後會告訴你。本文講我們如何讓這一切在發生時就可見,最初試過什麼,以及不用我們的應用也能做到同樣效果的方法。
閱讀全文