MCP工具吃Token太猛?3个实测方案砍掉70%消耗

用Claude Code或Cursor在大项目里干活,最头疼的不是模型不够聪明,是上下文窗口不够用。

你让它查一个认证模块怎么实现的,它把相关的20个文件全读进来。grep一遍、读一遍、分析一遍,Token哗哗地烧。还没开始写代码呢,上下文就快满了。

问题出在哪?MCP工具的返回设计。

我最近在自己的项目里做了一轮MCP工具的Token优化,效果很直观:同样的任务,Token消耗从平均4.2万降到了1.1万。方法不复杂,就三个方向。

问题到底多严重

先说数据。我用Claude Code跑了一个中等规模的Node.js项目(大约380个文件,总共12万行代码),记录了10次常见操作的Token消耗。

常见操作的Token消耗对比:

  • 查找某个函数的定义和用法:平均消耗3.8万Token
  • 理解一个模块的整体逻辑:平均消耗5.2万Token
  • 修改一个跨3个文件的功能:平均消耗6.7万Token

这些数字是什么概念?Claude的上下文窗口是200K Token。光是"理解一个模块"就吃掉了窗口的25%。连续做3个任务,上下文基本就满了。

根因很简单:默认的文件读取MCP工具是"全文返回"模式。你问它auth.js里有什么,它把整个文件内容原封不动地塞进上下文。380行的文件就是380行,不管你需要的是第52行的那个函数签名,还是整份代码。

方案一:摘要优先,按需展开

最直接的改法:MCP工具不要默认返回全文,返回结构化摘要。

我写了一个叫code-summary的MCP工具,用AST解析代码文件,返回的不是源代码,是这样的结构:

复制代码
{
  "file": "src/auth/middleware.js",
  "exports": ["authMiddleware", "validateToken", "refreshSession"],
  "functions": [
    {
      "name": "authMiddleware",
      "params": ["req", "res", "next"],
      "lines": [15, 48],
      "calls": ["validateToken", "getUserFromCache"],
      "description": "Express中间件,校验请求头中的Bearer Token"
    },
    {
      "name": "validateToken",
      "params": ["token"],
      "lines": [50, 82],
      "calls": ["jwt.verify", "checkTokenBlacklist"],
      "description": "校验JWT Token有效性,检查黑名单"
    }
  ],
  "imports": ["jsonwebtoken", "./cache", "../config"],
  "loc": 127
}

一个127行的文件,全文返回大约消耗800个Token。摘要返回只消耗120个Token左右------少了85%。

关键点在"按需展开"。智能体看完摘要后知道validateToken函数在第50到82行,需要看细节时,再调用一次read-lines工具,只读这33行。

实现代码不长,核心是AST解析那部分:

复制代码
// mcp-server/tools/code-summary.js
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;

function summarizeFile(filePath, source) {
  const ast = parser.parse(source, {
    sourceType: 'module',
    plugins: ['typescript', 'jsx']
  });

  const functions = [];
  const imports = [];
  const exports = [];

  traverse(ast, {
    FunctionDeclaration(path) {
      functions.push({
        name: path.node.id.name,
        params: path.node.params.map(p => p.name || '...'),
        lines: [path.node.loc.start.line, path.node.loc.end.line],
        description: extractLeadingComment(path)
      });
    },
    ImportDeclaration(path) {
      imports.push(path.node.source.value);
    },
    ExportNamedDeclaration(path) {
      if (path.node.declaration?.id) {
        exports.push(path.node.declaration.id.name);
      }
    }
  });

  return { file: filePath, functions, imports, exports, loc: source.split('\n').length };
}

Python项目也一样,用ast模块做解析就行。

注意一个坑:纯AST解析拿不到函数的"实际用途"。我的做法是读函数上方的注释作为description。如果没注释,就取函数体的前3行作参考。别试图让LLM来生成description,那样每个文件又多一次API调用,得不偿失。

方案二:给每次MCP调用设Token上限

第二个方案更粗暴但很有效:给MCP工具的返回值设一个Token硬上限。

在MCP server的响应层加一个中间件,超过指定长度就自动截断,并在末尾附上提示:

复制代码
// mcp-server/middleware/token-limit.js
const { encode } = require('gpt-tokenizer');

function withTokenLimit(handler, maxTokens = 2000) {
  return async (params) => {
    const result = await handler(params);
    const content = typeof result === 'string' ? result : JSON.stringify(result);
    const tokens = encode(content);

    if (tokens.length <= maxTokens) {
      return result;
    }

    // 按Token数截断,不是按字符数
    const truncated = tokens.slice(0, maxTokens);
    const truncatedText = truncated.map(t => t).join('');

    return {
      content: truncatedText,
      truncated: true,
      totalTokens: tokens.length,
      returnedTokens: maxTokens,
      hint: `内容已截断(${maxTokens}/${tokens.length} tokens)。用read-lines工具指定行号范围获取完整内容。`
    };
  };
}

我给不同类型的工具设了不同的上限:

复制代码
const TOOL_TOKEN_LIMITS = {
  'read-file': 2000,       // 文件读取:最多2000 Token
  'search-code': 3000,     // 代码搜索:最多3000 Token
  'list-directory': 500,   // 目录列表:最多500 Token
  'git-diff': 4000,        // Git差异:最多4000 Token
  'read-lines': 1500       // 行范围读取:最多1500 Token
};

踩坑提醒:Token上限不能设太低。我一开始把read-file设成了800 Token,结果智能体经常需要连续调用3-4次read-lines才能凑齐需要的信息,来回调用反而比一次读完消耗更多。2000是我测出来比较平衡的数值。

还有一个细节:截断的时候别在代码块中间切断。我加了个处理------如果截断点在代码块内,就往前退到代码块开始之前:

复制代码
function findSafeBreakpoint(text, position) {
  const codeBlockPattern = /```[\s\S]*?```/g;
  let match;
  while ((match = codeBlockPattern.exec(text)) !== null) {
    if (match.index < position && match.index + match[0].length > position) {
      return match.index; // 退到代码块开始前
    }
  }
  return position;
}

方案三:语义搜索替代全文grep

第三个方案投入最大,效果也最好。

传统的代码搜索MCP工具,底层就是grep或ripgrep。你搜"authentication",它把所有包含这个词的文件和匹配行全部返回。在大项目里,随便一搜就是几十个匹配,Token消耗轻松过万。

语义搜索的思路不同:先给代码库建向量索引,搜索时返回语义最相关的Top-K结果。

我的方案用的是本地的Embedding模型(nomic-embed-text,通过Ollama跑)。不依赖外部API,没有额外费用。

索引构建的流程:

复制代码
# 安装依赖
npm install @anthropic-ai/sdk vectordb

# 在项目根目录运行索引脚本
node build-index.js --dir ./src --extensions .js,.ts,.py

索引脚本的核心逻辑是把每个函数拆成独立的chunk,而不是按固定字符数切分:

复制代码
// build-index.js
async function buildIndex(projectDir) {
  const files = glob.sync(`${projectDir}/**/*.{js,ts}`, { ignore: '**/node_modules/**' });
  const chunks = [];

  for (const file of files) {
    const source = fs.readFileSync(file, 'utf-8');
    const functions = extractFunctions(source); // 用AST提取

    for (const fn of functions) {
      chunks.push({
        id: `${file}:${fn.name}`,
        text: fn.body,
        metadata: {
          file,
          name: fn.name,
          startLine: fn.startLine,
          endLine: fn.endLine
        }
      });
    }
  }

  // 批量生成embedding
  const embeddings = await generateEmbeddings(chunks.map(c => c.text));

  // 写入本地向量数据库
  await db.insert(chunks.map((c, i) => ({
    ...c,
    vector: embeddings[i]
  })));

  console.log(`索引完成: ${chunks.length} 个函数块, 来自 ${files.length} 个文件`);
}

搜索时,返回Top 5相关函数就够了:

复制代码
// mcp-server/tools/semantic-search.js
async function semanticSearch(query, topK = 5) {
  const queryEmbedding = await embed(query);
  const results = await db.search(queryEmbedding, topK);

  return results.map(r => ({
    file: r.metadata.file,
    function: r.metadata.name,
    lines: `${r.metadata.startLine}-${r.metadata.endLine}`,
    relevance: r.score.toFixed(3),
    preview: r.text.slice(0, 200) + '...'
  }));
}

实测数据对比:

搜索"用户登录后的Token刷新逻辑": - grep模式:返回34个匹配,消耗约8600 Token - 语义搜索:返回5个函数,消耗约1200 Token

准确率方面,语义搜索前5条命中目标函数的概率在我的项目里达到了92%。grep模式虽然不会遗漏,但34条里只有3-4条是真正需要的,噪声太多。

索引构建一次需要2-3分钟(380个文件),之后增量更新在秒级。我在项目的pre-commit hook里加了自动增量索引,改了哪个文件就重建那个文件的chunk。

三个方案组合的效果

回到开头的数据。三个方案全部上线后,同样10次操作的Token消耗:

  • 查找函数定义和用法:3.8万 → 0.9万(减少76%)
  • 理解模块整体逻辑:5.2万 → 1.4万(减少73%)
  • 修改跨文件功能:6.7万 → 2.1万(减少69%)

平均下来,Token消耗减少了约72%。上下文窗口的压力小了很多,同一个对话里能连续做更多事。

还有个附带好处:智能体的回答质量也上去了。以前全文塞进上下文,模型需要从一堆无关代码里找有用信息。现在只给相关片段,模型的注意力更集中,回答更准确。

部署清单

如果你想在自己的项目里试试,按这个顺序来:

  1. 先上方案二(Token上限中间件)------改动最小,5分钟搞定,立刻生效
  2. 再上方案一(摘要优先)------需要写AST解析逻辑,大约半天工作量
  3. 最后考虑方案三(语义搜索)------依赖Ollama和向量数据库,初次部署需要1-2天

方案二单独就能砍掉40%左右的Token消耗。三个方案叠加能到70%以上。

代码我放在GitHub上了,MCP server的完整实现包括上面提到的所有工具。有具体配置问题可以评论区聊。

相关推荐
Daydream.V20 小时前
主成分分析 PCA 详解
python·信息可视化·数据分析·pca
骑士雄师20 小时前
python 的列表和java中的集合有什么区别
java·windows·python
张忠琳21 小时前
【vllm】(v1 Attention)vLLM V1 Attention— Part3 MLA后端体系
ai·架构·vllm
MediaTea21 小时前
人工智能通识课:深度学习框架 PyTorch
人工智能·pytorch·python·深度学习·机器学习
我材不敲代码21 小时前
简单聊聊 Python 字典的基础用法
开发语言·python
这是谁的博客?21 小时前
PyTorch 深度学习框架核心机制解析:从动态图到编译优化的全面指南
人工智能·pytorch·深度学习·ai·分布式训练·autograd
Lucky_Turtle21 小时前
【m3u8】示例
python
我滴老baby21 小时前
Agent上线后不知道效果好不好?用Python搭建A/B测试+效果评估平台完整实战
开发语言·人工智能·python·ab测试
P-ShineBeam21 小时前
智能体-DeepAgent入门
人工智能·python·算法·语言模型·自然语言处理