用代码执行调用 MCP:把 agent 做得更省、更快

自从 Model Context Protocol(MCP) 作为「连接 agent 与外部工具/数据」的开放标准发布以来,生态长得飞快——如今已有数千个 MCP server,所有主流模型厂商和许多企业都支持它。但接的 server 一多,工具定义和工具结果就会迅速填满 context window(上下文窗口)。这篇讲怎么用一个老掉牙的工程套路——代码执行(code execution)——来化解它。

随着开发者给 agent 接上越来越多 MCP server 和工具,有两个模式尤其会撑爆上下文:

  1. 工具定义把 context window 占满
  2. 中间结果额外吃掉 token

问题在哪

工具定义把 context window 占满

大多数 MCP 客户端会一上来就加载全部工具定义,直接呈现给模型。每个定义都包含工具名、描述和完整的输入 schema。

当一个 agent 接了很多 MCP server,这可能意味着 agent 还没读到一条请求,就已经消耗掉几万 token。工具越加越多,这笔开销越大——而模型不得不处理所有这些定义,哪怕大多数任务其实只需要寥寥几个工具。

设想一个 agent 接了两个常见 server:一个 Google Drive、一个 Salesforce。模型看到的是 gdrive.getDocumentsalesforce.updateRecord 等等几十个工具定义,每一个都被加载进上下文窗口,加起来涨得飞快。

中间结果额外吃 token

来看一个常见任务:从 Google Drive 下载一份会议记录,再附到一条 Salesforce 销售线索上。

用传统 MCP 工具调用,agent 会:

  1. gdrive.getDocument 取记录,记录在工具结果里返回
  2. 这份记录在返回时流过模型的 context
  3. salesforce.updateRecord,把记录文本再传一遍

文档内容流经模型两次——一次从第一个工具返回,一次传给第二个工具。对一份两小时销售通话的记录来说,这可能是几万 token 在上下文窗口里走了两遍,数据重复、延迟和成本都上去了。这还只是一份记录的简单例子;实际里 agent 常常处理大得多的数据集、长得多的调用链,低效会层层叠加。


用代码执行调用 MCP

这些挑战其实有一个共同的解法。与其让模型直接调工具,不如把 **MCP server 当成「代码 API」**呈现,让 agent 通过写代码去跟它交互。

这套路在软件工程里早就成熟了:当开发者要跟很多 API 打交道,他们不会把所有文档一次性塞进脑子,而是只 import 自己需要的那部分。同样的办法可以搬到 MCP 上——把 MCP server 的工具表示成代码(比如文件系统里的文件)供模型探索,模型再写代码,只 import 并调用它需要的工具。

具体怎么做:不再直接呈现所有工具定义,而是生成一个代表「可用工具」的文件系统。比如用 TypeScript:

servers
├── google-drive
│   ├── getDocument.ts
│   ├── ...(其他工具)
│   └── index.ts
├── salesforce
│   ├── updateRecord.ts
│   ├── ...(其他工具)
│   └── index.ts
└── ...(其他 server)

每个工具变成一个文件,长这样:

// ./servers/google-drive/getDocument.ts
interface GetDocumentInput {
  documentId: string;
}
interface GetDocumentResponse {
  content: string;
}
export async function getDocument(input: GetDocumentInput): Promise<GetDocumentResponse> {
  return callMCPTool<GetDocumentResponse>('google_drive__get_document', input);
}

于是前面那个任务——下载记录再附到 Salesforce 线索——就变成模型写的一段代码:

const transcript = (await gdrive.getDocument({ documentId: 'abc123' })).content;
await salesforce.updateRecord({
  objectType: 'SalesMeeting',
  recordId: '00Q5f000001abcXYZ',
  data: { Notes: transcript }
});

会议记录流经执行环境,而不是模型的 context。模型只看到它写的代码,以及它显式选择打印或返回的结果。

⭐ 我们见过这种办法在某些场景里把 token 用量大幅压低——一个例子里从 150,000 token 降到 2,000 token,减少 98.7%,同时成本和延迟也跟着降。

下面逐一看它的好处。


好处一:渐进式披露(progressive disclosure)

最大的好处是:agent 可以按需加载工具定义。

把工具表示成文件系统里的文件后,模型就能用熟悉的 searchread 去探索可用工具,只在真正需要某个工具时才读它的定义。比如模型可以列出可用的 server 和工具:

const servers = await fs.readdir('./servers');
// ['google-drive', 'salesforce', 'jira', ...]
const salesforceTools = await fs.readdir('./servers/salesforce');
// ['updateRecord.ts', 'query.ts', ...]

模型还可以加一个 search_tools 工具,跨所有 server 找相关工具,并以可配置的详细程度返回(只要名字、名字加描述、或完整 schema)。这样它不必把所有东西都加载进 context,就能找到对的工具。


好处二:节省 context 的工具结果

工具结果里往往包含的数据,比 agent 真正需要的多得多。代码执行让 agent 在结果进入上下文窗口之前就过滤、转换。

设想取一张大表格,而 agent 只需要待处理(pending)的那几行:

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));

进 context 的不再是 10,000 行,而是过滤后的结果。进什么到 context,由 agent 精确掌控。


好处三:更强、更高效的控制流

代码执行让你用熟悉的编程结构写循环、条件、错误处理,而不必把一个个工具调用穿过模型来串联。

设想轮询一个状态变化。用直接工具调用,模型得反复调工具、逐个查看结果。用代码执行,循环就跑在执行环境里:

let found = false;
while (!found) {
  const issues = await jira.searchIssues({ query: 'status=resolved' });
  if (issues.length > 0) {
    found = true;
    console.log('Found resolved issues:', issues);
  }
  await new Promise(resolve => setTimeout(resolve, 5000));
}

好处四:保护隐私的操作

默认情况下,中间结果会流经 agent 的 context。代码执行可以让敏感数据完全不进模型的 context。

比如 agent 把数据从一个 MCP server 导到另一个,全程数据不流经模型 context:

const contacts = await salesforce.query({
  query: 'SELECT Id, Email FROM Contact LIMIT 100'
});
for (const contact of contacts) {
  await mailchimp.addSubscriber({ email: contact.Email });
}

agent 编排整个流程,但真正的联系人记录从不进它的 context。你还可以搭一层工具,自动对敏感数据做令牌化(tokenize),让 PII(个人身份信息)留在执行环境里,而 agent 拿占位符照样干活。


好处五:状态持久化与 skill

代码执行给了 agent 一个文件系统,而文件系统带来持久化。agent 可以把中间结果写进文件,下次从断点接着干:

const data = await fetchLargeDataset();
await fs.writeFile('./workspace/data.json', JSON.stringify(data));
// 之后 agent 可以把文件读回来

除了中间状态,agent 还能把可复用的代码持久化。当它摸索出怎么完成某个任务后,可以把那段代码存成函数,以后再用:

// ./skills/save-meeting-notes.ts
import * as gdrive from '../servers/google-drive';
import * as salesforce from '../servers/salesforce';

export async function saveMeetingNotes(documentId: string, recordId: string) {
  const transcript = (await gdrive.getDocument({ documentId })).content;
  await salesforce.updateRecord({
    objectType: 'SalesMeeting',
    recordId,
    data: { Notes: transcript }
  });
  return { success: true };
}

久而久之,agent 攒出一套更高层能力的工具箱——一组可复用的 skill。这正好接上了我们在 Agent Skills 上的工作:用一个个装着说明和代码的文件夹,帮 agent 在专门任务上越做越好。


需要权衡的取舍

代码执行解决了真问题,但它也引入自己的复杂度。

运行 agent 生成的代码需要一个安全的执行环境:沙箱(sandbox)、资源限制、监控——这些基础设施都增加运维负担,而直接工具调用完全不需要。安全顾虑是实打实的:执行模型生成的代码,必须配上恰当的防护,以防意外操作。

这些成本要跟好处掂量。

直接工具调用代码执行调用 MCP
工具定义全部前置加载按需探索、按需加载
中间结果来回穿过 context留在执行环境
控制流每步都回传模型原生循环/条件,跑一次
隐私数据流经 context敏感数据可不进 context
复杂度/安全简单,无需沙箱需要沙箱、限额、监控

对于只有寥寥几个工具、流程简单的 agent,直接工具调用仍然更简单、也够用。而对于大规模运转的 agent——工具多、中间结果大、编排复杂——代码执行的好处足以抵得上它增加的复杂度。


结语

MCP 已经成为连接 agent 与所需工具、数据的基础协议,背后是一个有数千个 server 的繁荣生态。当 agent 规模化地用上越来越多 server,代码执行提供了一条路,去化解随之而来的 context 与效率难题。

把 MCP server 当成代码 API 来呈现,agent 就能按需加载工具定义、在执行环境里处理中间结果、把 context window 聚焦在真正要紧的东西上。我们描述的这些模式——渐进式披露、节省 context 的结果、强大的控制流、隐私保护、状态持久化——全都源自一个朴素的想法:让 agent 写代码去跟它的工具打交道。

如果你正在用 MCP 构建 agent,不妨试试这些模式。MCP 生态还在持续演进,我们很期待看到社区如何用代码执行造出更强、更高效的 agent。

本译文采用 CC BY-NC-SA 4.0 协议发布,仅供学习交流。

原作品版权归 Anthropic(Anthropic Engineering)所有,原文请见 这里