- 引言
- 什么是MCP
- 传统MCP的不足
- MCP的代码执行范式:让 agent 写代码
- 新范式的优点
最近anthropic公司写了一篇博客介绍如何解决 MCP 工具太多的问题,本质上也是通过上下文工程实现上下文管理,最终实现上下文的精简。
你有没有想过,当你的 AI agent 连接了上千个工具之后,它到底在干什么?
可能正在费力地读取几十万个 token 的工具定义,然后把一个 2 小时会议的转录文本在系统里传来传去——每传一次就要重新"吃"一遍。这就像让一个人背着一本电话黄页去办事,每次需要一个电话号码都要从头翻一遍。
问题的本质很简单 :传统的 MCP 使用方式太"笨"了。
Model Context Protocol 听起来很学术,但它解决的是个很实际的问题:怎么让 AI agent 连接外部系统?
以前你想让 agent 接入一个新工具,就得专门写一套集成代码。100 个工具就要写 100 次。这种碎片化让人抓狂。MCP 的思路是提供一个通用协议——就像 USB 接口一样,插上就能用。
从 2024 年 11 月发布到现在,社区已经造了几千个 MCP 服务器。但随之而来的问题是:当你的 agent 能调用上千个工具时,它反而变慢了。
为什么?
两个要命的浪费
第一个浪费:工具定义塞爆上下文
大多数 MCP 客户端的做法是:把所有工具定义一股脑塞进模型的上下文窗口。
想象一下,你要从 Google Drive 下载个文档,模型要先看一遍这样的定义:
gdrive.getDocument
描述:从 Google Drive 获取文档
参数:documentId(必填)、fields(可选)...
返回:包含标题、正文、元数据、权限的 Document 对象
如果你想要把这些表单更新到Salesforce,则还需要Salesforce 的定义:
salesforce.updateRecord
描述:在 Salesforce 中更新记录
参数:objectType、recordId、data...
工具多了之后,光是读取这些定义,模型可能就要先处理几十万 token。你还没开始干活,钱就已经花出去了。
第二个浪费:中间结果反复传递
更离谱的是数据传递的方式。
你让 agent "把 Google Drive 里的会议记录附加到 Salesforce 的销售线索上",它会这么做:
- 调用
gdrive.getDocument,拿到一个 5 万字的转录文本 - 把这 5 万字加载到模型上下文
- 调用
salesforce.updateRecord, 再把这 5 万字写一遍
模型会发起如下2次的Tool调用:
TOOL CALL: gdrive.getDocument(documentId: "abc123")
→ 返回 "Discussed Q4 goals...\n[full transcript text]"
(载入到模型上下文)
TOOL CALL: salesforce.updateRecord(
objectType: "SalesMeeting",
recordId: "00Q5f000001abcXYZ",
data: { "Notes": "Discussed Q4 goals...\n[full transcript text written out]" }
)
(模型需要再次将整段转录文本写入上下文)
同一份数据,在系统里传了两次。如果是个 2 小时的会议转录,你可能要为此多付 50,000 token 的钱。
更要命的是 :如果文档太大,可能直接超过上下文窗口限制,整个工作流就崩了。
原文见于:https://www.anthropic.com/engineering/code-execution-with-mcp
既然问题出在"所有东西都要过一遍模型",那解决方案也很直白:别让模型当传话筒,让它写代码来处理这些事。
这不是什么新鲜想法。LLM 本来就很会写代码,为什么不用呢?
具体怎么做?把 MCP 服务器呈现成代码 API,而不是工具列表。
你可以生成这样的文件结构:
servers/
├── google-drive/
│ ├── getDocument.ts
│ └── index.ts
├── salesforce/
│ ├── updateRecord.ts
│ └── index.ts
每个工具对应一个文件:
// ./servers/google-drive/getDocument.ts
import { callMCPTool } from"../../../client.js";
interface GetDocumentInput {
documentId: string;
}
interface GetDocumentResponse {
content: string;
}
/* Read a document from Google Drive */
exportasyncfunction getDocument(input: GetDocumentInput): Promise<GetDocumentResponse> {
return callMCPTool<GetDocumentResponse>('google\_drive\_\_get\_document', input);
}
然后 agent 可以写这样的代码:
// Read transcript from Google Docs and add to Salesforce prospects
import * as gdrive from './servers/google-drive';
import * as salesforce from './servers/salesforce';
const transcript = (await gdrive.getDocument({ documentId: 'abc123' })).content;
await salesforce.updateRecord({
objectType: 'SalesMeeting',
recordId: '00Q5f000001abcXYZ',
data: { Notes: transcript }
});
看出区别了吗?agent 只需要列出 ./servers/ 目录,找到需要的服务器,再读取具体的工具文件。 按需加载,而不是一开始就把所有定义读一遍。
Token 使用量能从 150,000 降到 2,000——节省 98.7%。
Cloudflare 也独立发现了这个思路,他们叫它 "Code Mode"。核心洞见是一样的:LLM 擅长写代码,那就让它多写代码。
- 按需加载工具
模型很会浏览文件系统。它可以先看看有哪些服务器可用,再根据任务需要去读具体的工具定义。
或者你可以提供一个 search\_tools 工具:agent 搜索 "salesforce",只加载相关的工具。你甚至可以让它选择需要多少细节——只要名称?还是要完整的 schema?
这就像去图书馆,你不需要把所有书都搬回家,只借你要看的那几本。
- 在代码里过滤数据
面对大数据集时,agent 可以先在执行环境里处理,再把结果返回给模型。
比如你要从一个 10,000 行的表格里找出待处理的订单:
// Without code execution - all rows flow through context
TOOL CALL: gdrive.getSheet(sheetId: 'abc123')
→ returns 10,000 rows in context to filter manually
// With code execution - filter in the execution environment
const allRows = await gdrive.getSheet({ sheetId: 'abc123' });
const pendingOrders = allRows.filter(row => row["Status"] === 'pending');
console.log(`Found ${pendingOrders.length} pending orders`);
console.log(pendingOrders.slice(0, 5)); // 只输出前 5 行
模型只会看到 5 行,而不是 10,000 行。 聚合、连接、提取字段——这些操作都可以在代码里完成,不会膨胀上下文窗口。
- 更自然的控制流
循环、条件判断、错误处理——用代码写起来多自然:
let found = false;
while (!found) {
const messages = await slack.getChannelHistory({ channel: 'C123456' });
found = messages.some(m => m.text.includes('deployment complete'));
if (!found) await new Promise(r => setTimeout(r, 5000));
}
console.log('Deployment notification received');
比起在 agent 循环里来回调用工具,这种方式效率高多了。而且能一次性执行完整的逻辑分支,降低"首 token 时延"——不用等模型逐条评估 if 语句。
- 保护隐私
中间结果默认留在执行环境里。只有你显式输出的内容才会被模型看到。
想象一下这个场景 :你要把客户联系信息从表格导入 Salesforce。agent 可以这样写:
const sheet = await gdrive.getSheet({ sheetId: 'abc123' });
for (const row of sheet.rows) {
await salesforce.updateRecord({
objectType: 'Lead',
recordId: row.salesforceId,
data: {
Email: row.email,
Phone: row.phone,
Name: row.name
}
});
}
console.log(`Updated ${sheet.rows.length} leads`);
MCP 客户端可以在数据到达模型之前对个人信息进行 token 化。真实的邮箱、电话、姓名从 Google Sheets 流向 Salesforce,但不会流经模型 。agent 看到的只是 [EMAIL\_1]、[PHONE\_1] 这样的占位符。
- 状态持久化和"技能"积累
agent 可以把中间结果写入文件,在之后恢复工作:
const leads = await salesforce.query({
query: 'SELECT Id, Email FROM Lead LIMIT 1000'
});
await fs.writeFile('./workspace/leads.csv', csvData);
// 之后可以继续
const saved = await fs.readFile('./workspace/leads.csv', 'utf-8');
更有意思的是,agent 可以把自己写的代码保存成可复用函数 :
// ./skills/save-sheet-as-csv.ts
export async function saveSheetAsCsv(sheetId: string) {
const data = await gdrive.getSheet({ sheetId });
const csv = data.map(row => row.join(',')).join('\n');
await fs.writeFile(`./workspace/sheet-${sheetId}.csv`, csv);
return `./workspace/sheet-${sheetId}.csv`;
}
这就是 Skills 的概念:随着时间推移,agent 逐步积累起一个工具箱,学会更高效地工作。
就像人类会总结经验、形成习惯一样。
代码执行需要基础设施:安全的沙箱环境、资源限制、监控机制。这些都有运营成本和安全风险。
直接工具调用虽然低效,但实现起来更简单。
你需要权衡 :更低的 token 成本、更快的响应速度、更强的工具组合能力——值不值得这些基础设施投入?
对于连接几十上百个工具的 agent 来说,答案可能是肯定的。
MCP 提供了协议,但协议只是基础设施。怎么用它,决定了你的 agent 是聪明还是笨拙。
这里讨论的许多问题——上下文管理、工具组合、状态持久化——在软件工程里早就有成熟解法。代码执行只是把这些既有模式应用到 agent 上,让它用熟悉的编程方式更高效地工作。
说到底,LLM 本来就会写代码。让它多写点,没什么不好。
