一、引言:Agent 的效率瓶颈,很多时候不在模型,而在工具调用方式
当我们讨论如何让 AI Agent 更高效时,注意力往往集中在模型能力、提示工程或上下文窗口大小上。但 Anthropic 在 Code execution with MCP 这篇工程文章里指出了一个更底层的问题:传统的 MCP 工具调用方式,本身就会制造大量不必要的上下文开销和交互延迟。
核心洞察很直接:与其让模型在每一步都"看见工具定义、发起调用、再接收中间结果",不如让模型写一小段代码,由代码在受控执行环境中完成多步编排。按照文中的量化示例,这种方式可以把工具定义相关的上下文开销从 150,000 tokens 压缩到 2,000 tokens,也就是 98.7% 的降幅。
但这里必须先厘清一个边界:代码执行不是 MCP 协议本身的新能力,而是构建在 MCP 之上的一种客户端实现模式。 协议没有变,变的是客户端如何把 MCP 服务器呈现给 Agent,以及 Agent 如何消费这些能力。
本文重点回答四个问题:
- 传统 MCP 客户端到底慢在哪里?
- 代码执行模式的核心架构是什么?
- 为什么它既能提效,又能改善隐私保护?
- 这类系统落地时,安全沙箱应该怎样设计?
二、先把边界说清楚:这不是"改造 MCP",而是"重构客户端"
如果只看标题,很多人会下意识以为"Code execution with MCP"意味着 MCP 协议新增了一种代码执行语义。其实不是。
Anthropic 文章讨论的是:客户端如何把一组 MCP 工具重新组织成适合代码消费的 API 视图。也就是说,原来 Agent 面对的是"工具列表 + 参数 Schema";现在 Agent 面对的是"文件树 + 类型化函数 + 代码运行时"。
可以把它理解为一层客户端侧适配器:
- 先连接原有的 MCP 服务器
- 获取这些服务器暴露出来的工具定义
- 自动生成一套可浏览、可导入、可调用的代码接口
- 把这套接口挂到安全的执行环境里供 Agent 使用
从这个角度看,代码执行模式的本质并不是"给协议加能力",而是给 Agent 加了一个更高效的编排层。
下图展示了 MCP 协议层、客户端适配层和 Agent 执行层三者的分层关系——协议本身不变,变化只发生在客户端的呈现方式上:
三、问题诊断:传统 MCP 工具调用为什么昂贵
传统 MCP 客户端常见的做法是:连接服务器之后,把所有工具定义一次性注入上下文;之后每次需要做事,再由模型逐步决定调用哪个工具、传什么参数、如何处理返回值。
这套模式的主要问题有两个。
3.1 工具定义会过载上下文窗口
假设一个系统接了很多 MCP 服务器,每个服务器又暴露了很多工具,那么客户端往往会在会话开始时把整批定义喂给模型。类似下面这种描述,数量一多就会迅速膨胀:
gdrive.getDocument
Description: Retrieves a document from Google Drive
Parameters:
documentId (required, string): The ID of the document to retrieve
fields (optional, string): Specific fields to return
Returns: Document object with title, body content, metadata, permissions, etc.
当工具数量上千时,模型还没真正开始解决问题,上下文窗口就已经先被工具说明书占满了。这不是"推理成本高",而是准备阶段就已经浪费了大量令牌。
3.2 中间结果被迫经过模型,上下文成了数据中转站
第二个问题更隐蔽,但也更昂贵。
假设你要把 Google Drive 里的会议纪要写进 Salesforce。传统模式下,流程通常是:
- 模型调用
gdrive.getDocument - 完整文档内容回到上下文
- 模型再调用
salesforce.updateRecord - 文档内容作为参数再次经过上下文传出去
也就是说,模型并不需要真正理解整份文档,却被迫充当了一次数据搬运的中转站。
如果文档很长,比如一份 2 小时会议纪要,那么这些内容会在上下文里往返两次,成本自然居高不下。
下图把传统模式和代码执行模式的差别并排画了出来:
可以把两种模式的差异压缩成一句话:
- 传统工具调用:数据必须先经过模型,再流向另一个工具
- 代码执行模式:数据可以在执行环境里直接流动,模型只看代码和必要摘要
四、核心架构:把 MCP 服务器呈现为代码 API,而不是直接工具列表
Anthropic 给出的核心做法,是为所有可用的 MCP 工具生成一套可浏览的文件树。Agent 不是直接面对"几千个工具定义",而是像开发者那样去浏览目录、打开文件、理解接口,然后按需写代码。
4.1 文件树是工具发现界面
典型结构如下:
servers/
├── google-drive/
│ ├── getDocument.ts
│ ├── getSheet.ts
│ ├── searchFiles.ts
│ └── index.ts
├── salesforce/
│ ├── updateRecord.ts
│ ├── query.ts
│ └── index.ts
├── slack/
│ ├── postMessage.ts
│ ├── getChannelHistory.ts
│ └── index.ts
└── ...
这里最关键的不是"用了 TypeScript",而是把工具发现改造成了渐进式信息暴露:
- 先看有哪些服务器
- 再看某个服务器有哪些工具
- 最后只读取当前任务需要的那几个函数定义
也正因为如此,工具定义不再需要一次性占满上下文。
4.2 每个工具都变成一个类型化函数
文章里给出的示例大致如下:
// ./servers/google-drive/getDocument.ts
import { callMCPTool } from "../../../client.js";
interface GetDocumentInput {
documentId: string;
}
interface GetDocumentResponse {
content: string;
}
/* Read a document from Google Drive */
export async function getDocument(
input: GetDocumentInput
): Promise<GetDocumentResponse> {
return callMCPTool<GetDocumentResponse>(
'google_drive__get_document',
input
);
}
这段代码透露出几个很重要的设计点:
- MCP 协议细节被收敛到底层的
callMCPTool - 上层暴露给 Agent 的是稳定的代码接口,而不是原始工具协议对象
- TypeScript 类型信息本身就能帮助模型减少参数理解错误
- 工具文件是独立的,天然支持按需加载
4.3 index.ts 让工具既能细粒度读取,也能模块化导入
一个目录下的 index.ts 通常承担聚合导出的角色。这样做的好处是,Agent 既可以:
- 打开单个工具文件,查看精确签名
- 也可以直接
import * as gdrive from './servers/google-drive'
这就把"工具系统"变成了"可编程 SDK"。
4.4 渐进式工具发现,才是令牌下降的关键原因
很多人会把注意力放在"模型写代码"这件事本身,但从工程角度看,真正带来显著令牌下降的,是工具定义改成了按需读取。
可以把这个过程理解成下面三步:
Step 1: 只列出 servers/ 下有哪些服务器
Step 2: 只查看目标服务器有哪些导出函数
Step 3: 只读取当前要用的工具文件
这就是文中提到的 progressive disclosure(渐进式披露)——Agent 只拿到当前任务真正需要的信息,而不是先吞下一整本工具手册。
原文还提到一个补充思路:如果不想完全依赖文件遍历,也可以提供一个 search_tools 能力,让 Agent 先按名称、描述或完整定义级别去检索工具,再决定读取多少细节。这本质上仍然是在做同一件事——把工具发现做成按需展开,而不是全量注入。
下图直观展示了这种"信息漏斗"式的三步发现流程,以及与传统全量加载的巨大差异:
五、工作原理:代码执行模式下,Agent 实际是怎么做事的
如果把整套系统合在一起,它的工作方式可以概括为:模型只负责编写和理解代码,真正的数据搬运、过滤、轮询、聚合,都发生在安全的执行环境里。
下面这张图可以先帮助你建立整体心智模型:
从交互路径上看,完整流程大致是:
- 用户提出任务
- Agent 浏览
servers/文件树 - Agent 读取少量目标工具文件
- Agent 写出一段 TypeScript 代码
- 代码在受控沙箱中运行
- 运行时代码通过
callMCPTool()和各个 MCP 服务器交互 - 最终只把摘要、日志或显式结果返回给模型
也就是说,模型不再需要"亲自参与"每一跳数据传输,它只需要:
- 理解要做什么
- 写出怎样的程序
- 根据输出决定下一步是否继续
这是一种非常接近真实工程开发的工作方式。
六、为什么代码执行模式更强:它不是少调几次工具,而是换了执行位置
代码执行模式的优势,不只是"更省 tokens",更重要的是它把很多本来要靠模型一轮轮思考的事情,搬到了执行环境里直接完成。
6.1 跨系统数据编排可以在环境内直接完成
例如从 Google Drive 中读取会议纪要,再写入 Salesforce:
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 }
});
这里最重要的不是"代码比较简洁",而是 transcript 这份正文内容只在执行环境里存在。它不需要回到模型上下文里,也不需要被模型重新"看到"一遍。
下图展示了数据在沙箱内直接从源系统流向目标系统、而模型只接收代码和摘要的完整路径:
6.2 数据过滤和聚合发生在执行环境,而不是上下文窗口里
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));
这个例子非常典型:
- 原始表格可能很大
- 模型并不需要看完整表格
- 它只需要一个过滤后的摘要和少量样本
于是,数据处理发生在代码里,模型拿到的是计算后的结果,而不是待计算的原始材料。
6.3 循环、轮询、重试等控制流不再消耗多轮 LLM 推理
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');
在传统模式下,这类逻辑会变成多轮"推理 → 调工具 → 再推理"的循环;而在代码执行模式下,循环本身就在运行时完成了。
这还会带来一个经常被忽略的收益:更低的 time to first token 延迟。 因为很多 if/while 判断不再需要等模型重新思考一轮,而是直接由执行环境完成。
6.4 状态可以落盘,技能可以复用
const leads = await salesforce.query({
query: 'SELECT Id, Email FROM Lead LIMIT 1000'
});
const csvData = leads.map(l => `${l.Id},${l.Email}`).join('\n');
await fs.writeFile('./workspace/leads.csv', csvData);
const saved = await fs.readFile('./workspace/leads.csv', 'utf-8');
一旦执行环境带有工作目录,很多一次性操作就可以被积累成可复用资产。文章里还展示了把常见任务封装成 skill 的思路,例如把 Google Sheet 导出为 CSV:
import * as gdrive from './servers/google-drive';
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`;
}
这意味着 Agent 不只是"临时写一段脚本",而是开始形成自己的可复用执行资产。
Anthropic 原文进一步把这件事和 Skills 机制联系了起来:如果把这些可复用函数连同说明文档一起沉淀下来,例如补上一份 SKILL.md,它们就不再只是零散脚本,而会演化成 Agent 可长期复用的高阶能力模块。
七、隐私保护:真实数据在环境内流动,模型只看脱敏视图
这是整篇文章里我认为最容易被低估的一点。
传统工具调用模式下,只要模型要决定下一步,它通常就得先看到上一步的结果。于是敏感数据很容易进入上下文。代码执行模式则提供了另一种可能:让真实数据在执行环境内部完成读、写、转换,而模型只看到必要的日志和摘要。
例如下面这段代码,会把表格里的联系人数据直接写入 Salesforce:
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`);
如果这时 Agent 尝试把原始数据打印出来,客户端可以在输出层做拦截和令牌化,让模型看到的不是原值,而是类似下面的占位视图:
[
{ salesforceId: '00Q...', email: '[EMAIL_1]', phone: '[PHONE_1]', name: '[NAME_1]' },
{ salesforceId: '00Q...', email: '[EMAIL_2]', phone: '[PHONE_2]', name: '[NAME_2]' }
]
也就是说:
- 真实 PII 数据:在执行环境和目标系统之间流动
- 模型可见内容:是经过脱敏处理的日志或摘要
下面这张图把这层关系画得更直观:
这并不意味着"模型绝不可能接触敏感数据",而是意味着系统终于有机会通过确定性的输出控制,把敏感信息默认拦在上下文之外。对于企业场景,这一点非常关键。
八、安全沙箱:代码执行带来效率,也带来新的基础设施义务
Anthropic 在文章结尾明确提醒:运行 Agent 生成的代码,会引入新的复杂度。原文点名了三类基础要求:
- Sandboxing
- Resource limits
- Monitoring
这三个词很短,但背后的工程含义并不轻。下图展示了一个安全沙箱从外到内的多层防护结构:
一个真正可用的代码执行系统,至少要补齐下面这些控制面:
| 安全维度 | 工程做法 | 主要目的 |
|---|---|---|
| 执行隔离 | 使用受限运行时或容器化沙箱 | 防止代码逃逸影响宿主系统 |
| 资源限制 | CPU、内存、磁盘、执行时长配额 | 防止资源耗尽或死循环拖垮系统 |
| 行为监控 | 执行日志、异常检测、审计记录 | 及时发现异常模式 |
| 网络控制 | 仅允许通过受控出口访问指定能力 | 防止任意对外访问 |
| 输出控制 | 对日志、返回值、错误信息做脱敏或裁剪 | 防止敏感数据进入模型上下文 |
从这里也能看出,代码执行并不是"白拿性能红利"。它的代价是:把复杂度从上下文管理,转移到了运行时治理。
8.1 为什么直接工具调用依然有存在价值
这也是这篇文章非常诚实的地方。它没有说"代码执行一定全面替代工具调用",而是明确承认:直接工具调用规避了运行时代码的很多安全和运维负担。
所以两者更像是一个权衡:
- 直接工具调用:简单、直接、实现成本低,但上下文成本高
- 代码执行模式:令牌和延迟表现更好,但需要更强的基础设施兜底
如果任务只是一步查询、一步提交,那么直接工具调用依然很合理;但如果任务本质上是一个多步程序,那么让 Agent 写代码,往往更符合问题本身的结构。
九、量化视角:哪些数据来自原文,哪些是工程上的延伸估算
这里需要把口径分清楚。
9.1 原文明确给出的数据
Anthropic 原文明确给出的关键数字,是渐进式工具发现带来的上下文节省:
| 指标 | 数值 |
|---|---|
| 工具定义相关上下文开销(传统方式) | 150,000 tokens |
| 工具定义相关上下文开销(代码执行模式) | 2,000 tokens |
| 节省比例 | 98.7% |
这组数据对应的是:Agent 不再一次性读取全部工具定义,而是只按需读取当前任务需要的函数接口。
9.2 工程上的延伸估算
如果把数据搬运、轮询控制流、日志摘要等因素也纳入考虑,在复杂多步任务里,整体收益通常还会继续扩大。为了帮助理解,可以用一个典型场景做示例性估算:
| 指标 | 传统工具调用 | 代码执行 | 说明 |
|---|---|---|---|
| 工具定义加载 | ~150,000 tokens | ~2,000 tokens | 直接对应原文结论 |
| 中间结果传递 | 高 | 低 | 数据可留在执行环境内 |
| 轮询 / 重试 | 多轮推理 | 单次代码执行 | 控制流在运行时完成 |
| 长链路编排 | 逐步决策 | 程序化执行 | 更适合多步工作流 |
这部分表格不是原文逐项给出的 benchmark,而是基于文章机制做的工程推演。把这层区分说清楚,文章会更严谨。
十、实现启示:为什么说这是一种更像"软件工程"的 Agent 架构
MCP 代码执行模式真正有意思的地方,不只是它更快,而是它让 Agent 的工作方式更接近开发者本身。
传统模式下,Agent 像一个不断口头指挥的调度员:
- 看工具说明
- 决定下一步调哪个
- 读结果
- 再决定下一步
而代码执行模式下,Agent 更像一个真正写程序的工程师:
- 先发现可用接口
- 再写出一个小程序
- 把过滤、循环、聚合、错误处理写在代码里
- 最后只把结果摘要拿回来
下图把两种工作模式并排放在一起,可以直观看到从"调度员"到"工程师"的角色转变:
这种变化的意义在于:Agent 的"思考对象"从单步工具调用,变成了更高层的程序结构。 这正是复杂任务所需要的抽象层级。
从落地角度看,这种架构尤其适合下面几类任务:
- 需要跨多个系统搬运和转换数据
- 需要过滤大结果集,只回传摘要
- 需要轮询、等待、重试等控制流
- 需要在多次执行之间保存中间状态
- 对隐私隔离和上下文成本都较敏感
十一、与 Cloudflare “Code Mode” 的呼应
Anthropic 在文中还专门提到,Cloudflare 也发布了类似结论,并把这类"让 LLM 通过写代码来使用 MCP"的模式称为 Code Mode。
这个呼应很重要,因为它说明这不是某一篇文章里的孤例,而是行业正在逐步收敛出的共同认知:
- LLM 很擅长写小段程序
- 程序比逐轮工具调用更适合表达多步任务
- 文件系统和代码 API 是比大工具列表更自然的 Agent 交互界面
换句话说,代码执行并不是 MCP 体系里的偶发技巧,而是在 Agent 工程化过程中越来越自然的一条路线。
十二、总结:代码执行不是替代 MCP,而是在 MCP 之上增加一个更高效的编排层
如果要用一句话概括 Anthropic 这篇文章的价值,我会这样说:
它不是在教我们如何"多接几个工具",而是在提醒我们:当任务本质上是一个程序时,就应该让 Agent 以程序的方式完成它。
最后把全文收束成几个结论:
- 代码执行不是 MCP 协议改造,而是客户端实现升级
- 真正带来大幅节省的关键,是工具定义的渐进式披露
- 数据处理、控制流和状态管理被搬到了执行环境中
- 隐私保护因此有了"默认不进上下文"的可能性
- 与此同时,系统必须承担沙箱、资源限制和监控的基础设施成本
所以,代码执行模式并不是对所有场景都更好。对于简单的一步式操作,直接工具调用仍然高效而实用;但对于复杂的、多阶段的、跨系统的数据编排任务,它通常更贴近问题本身的真实结构。
从这个意义上说,Agent 不再只是"工具调用者",而是在逐步成为一个能写程序、能复用技能、能在受控环境中执行任务的软件型执行体。
参考资料
- Code execution with MCP: Building more efficient agents - Anthropic Engineering
- Model Context Protocol - Official Documentation
- Cloudflare Code Mode
「真诚赞赏,手留余香」
真诚赞赏,手留余香
使用微信扫描二维码完成支付