用 MCP 进行代码执行:构建更高效的 agents
原文: English original · Anthropic/OpenAI 官方
用 MCP 进行代码执行:构建更高效的 agents
发布于 2025 年 11 月 4 日
直接工具调用会为每个工具定义和每次调用结果消耗上下文。让 agents 编写代码来调用工具,扩展性会更好。下面看看它如何与 MCP 配合工作。
Model Context Protocol(MCP)是一项开放标准,用于将 AI agents 连接到外部系统。过去,把 agents 接入工具和数据,通常需要为每一组配对关系构建自定义集成;这会造成碎片化和重复劳动,也让真正互联的系统难以扩展。MCP 提供了一套通用协议:开发者只需在自己的 agent 中实现一次 MCP,就能解锁一整个集成生态。
自 2024 年 11 月 MCP 发布以来,其采用速度很快:社区已经构建了数千个 MCP servers,各主要编程语言都有可用 SDK,行业也已经把 MCP 作为连接 agents 与工具、数据的事实标准。
如今,开发者经常构建能访问数百甚至数千个工具的 agents,这些工具分布在数十个 MCP servers 上。不过,随着连接工具数量增长,预先加载所有工具定义、并通过上下文窗口传递中间结果,会拖慢 agents 并增加成本。
本文将探讨代码执行如何让 agents 更高效地与 MCP servers 交互,在使用更少 tokens 的同时处理更多工具。
工具造成的过量 token 消耗会降低 agents 效率
随着 MCP 用法扩展,有两种常见模式会增加 agent 成本和延迟:
- 工具定义压垮上下文窗口;
- 中间工具结果消耗额外 tokens。
1. 工具定义压垮上下文窗口
大多数 MCP clients 会预先把所有工具定义直接加载进上下文,并用直接 tool-calling 语法暴露给模型。这些工具定义可能长这样:
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.
salesforce.updateRecord
Description: Updates a record in Salesforce
Parameters:
objectType (required, string): Type of Salesforce object (Lead, Contact, Account, etc.)
recordId (required, string): The ID of the record to update
data (required, object): Fields to update with their new values
Returns: Updated record object with confirmation
工具描述会占用更多上下文窗口空间,增加响应时间和成本。当 agents 连接到数千个工具时,它们可能还没开始读取请求,就已经需要处理数十万 tokens。
2. 中间工具结果消耗额外 tokens
大多数 MCP clients 允许模型直接调用 MCP 工具。例如,你可能会让 agent:“从 Google Drive 下载我的会议文字记录,并把它附加到 Salesforce lead 上。”
模型会发出这样的调用:
TOOL CALL: gdrive.getDocument(documentId: "abc123")
→ returns "Discussed Q4 goals...\n[full transcript text]"
(loaded into model context)
TOOL CALL: salesforce.updateRecord(
objectType: "SalesMeeting",
recordId: "00Q5f000001abcXYZ",
data: { "Notes": "Discussed Q4 goals...\n[full transcript text written out]" }
)
(model needs to write entire transcript into context again)
每个中间结果都必须经过模型。在这个例子里,完整的电话会议文字记录会两次流经上下文。对于一场 2 小时的销售会议,这可能意味着额外处理 50,000 tokens。更大的文档甚至可能超过上下文窗口限制,导致流程中断。
面对大型文档或复杂数据结构时,模型在工具调用之间复制数据也更容易出错。
图片:展示 MCP client 如何与 MCP server 和 LLM 配合工作的图。
MCP client 会把工具定义加载到模型的上下文窗口中,并编排一个消息循环:每次工具调用和结果都会在操作之间经过模型。
用 MCP 进行代码执行可以提升上下文效率
随着代码执行环境在 agents 中变得越来越常见,一种解决方案是把 MCP servers 呈现为代码 API,而不是直接工具调用。这样 agent 就可以编写代码来与 MCP servers 交互。这个方法同时解决了两个问题:agents 可以只加载所需工具,并在执行环境中处理数据,再把结果传回模型。
实现方式有很多。一个做法是根据已连接 MCP servers 的所有可用工具生成一棵文件树。下面是一个使用 TypeScript 的实现:
servers
├── google-drive
│ ├── getDocument.ts
│ ├── ... (other tools)
│ └── index.ts
├── salesforce
│ ├── updateRecord.ts
│ ├── ... (other tools)
│ └── index.ts
└── ... (other servers)
然后每个工具对应一个文件,大致如下:
// ./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);
}
上面的 Google Drive 到 Salesforce 示例就变成了如下代码:
// Read transcript from Google Docs and add to Salesforce prospect
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/ 目录,找到可用 servers(例如 google-drive 和 salesforce),再读取它需要的具体工具文件(例如 getDocument.ts 和 updateRecord.ts),以理解每个工具的接口。这让 agent 只加载当前任务所需的定义。这样可以把 token 用量从 150,000 tokens 降到 2,000 tokens,节省 98.7% 的时间和成本。
Cloudflare 发布过类似发现,并把用 MCP 进行代码执行称为 “Code Mode”。核心洞察相同:LLMs 擅长编写代码,开发者应该利用这一优势,构建能更高效地与 MCP servers 交互的 agents。
用 MCP 进行代码执行的好处
用 MCP 进行代码执行,可以让 agents 通过按需加载工具、在数据抵达模型前先过滤数据、以及在单个步骤中执行复杂逻辑,更高效地使用上下文。采用这种方法也会带来安全和状态管理方面的好处。
渐进式披露
模型很擅长浏览文件系统。把工具作为文件系统上的代码呈现给模型,可以让模型按需读取工具定义,而不是预先全部读取。
另一种做法是在 server 中添加一个 search_tools 工具,用来查找相关定义。例如,在使用上文假设的 Salesforce server 时,agent 会搜索 “salesforce”,并只加载当前任务所需的那些工具。在 search_tools 工具中加入详细程度参数,让 agent 选择所需的详情级别(例如仅名称、名称和描述、或包含模式的完整定义),也有助于 agent 节省上下文并高效找到工具。
上下文高效的工具结果
处理大型数据集时,agents 可以先在代码中筛选和转换结果,然后再返回。设想要获取一张 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)); // Only log first 5 for review
agent 看到的是五行,而不是 10,000 行。类似模式也适用于聚合、跨多个数据源做 join,或提取特定字段,全都不会让上下文窗口膨胀。
更强大、也更节省上下文的控制流
循环、条件判断和错误处理都可以用熟悉的代码模式完成,而不必串联一个个工具调用。例如,如果你需要 Slack 中的部署通知,agent 可以写:
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 循环中来回切换 MCP 工具调用和 sleep 命令更高效。
此外,能够写出一棵会被执行的条件树,也能节省 “time to first token” 延迟:agent 不必等待模型来评估 if 语句,而是可以让代码执行环境完成这件事。
保护隐私的操作
当 agents 使用 MCP 进行代码执行时,中间结果默认留在执行环境中。这样,agent 只会看到你显式 log 或 return 的内容;也就是说,你不想分享给模型的数据,可以在整个流程中流动,却从不进入模型上下文。
对于更敏感的负载,agent harness 还可以自动对敏感数据进行 tokenization。例如,假设你需要把电子表格中的客户联系方式导入 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 client 会拦截数据,并在其到达模型之前对 PII 进行 tokenization:
// What the agent would see, if it logged the sheet.rows:
[
{ salesforceId: '00Q...', email: '[EMAIL_1]', phone: '[PHONE_1]', name: '[NAME_1]' },
{ salesforceId: '00Q...', email: '[EMAIL_2]', phone: '[PHONE_2]', name: '[NAME_2]' },
...
]
之后,当数据在另一个 MCP 工具调用中共享时,会通过 MCP client 中的查找表还原 token。真实的电子邮件地址、电话号码和姓名会从 Google Sheets 流向 Salesforce,但绝不会经过模型。这可以防止 agent 意外记录或处理敏感数据。你也可以用它定义确定性的安全规则,选择数据可以流向哪里、从哪里流出。
状态持久化与 skills
具备文件系统访问能力的代码执行,可以让 agents 在多次操作之间保持状态。Agents 可以把中间结果写入文件,从而恢复工作并跟踪进度:
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);
// Later execution picks up where it left off
const saved = await fs.readFile('./workspace/leads.csv', 'utf-8');
Agents 也可以把自己的代码作为可复用函数持久保存下来。一旦 agent 为某个任务开发出可用代码,就可以保存这份实现,供将来使用:
// In ./skills/save-sheet-as-csv.ts
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`;
}
// Later, in any agent execution:
import { saveSheetAsCsv } from './skills/save-sheet-as-csv';
const csvPath = await saveSheetAsCsv('abc123');
这与 Skills 的概念紧密相关。Skills 是由可复用说明、脚本和资源组成的文件夹,用来帮助模型提升在专门任务上的表现。给这些保存下来的函数添加一个 SKILL.md 文件,就能创建一个结构化的 skill,供模型引用和使用。随着时间推移,这会让你的 agent 建立一套更高层能力的工具箱,并演化出它最高效工作所需的脚手架。
请注意,代码执行也会引入自身复杂性。运行 agent 生成的代码,需要安全的执行环境,以及合适的沙盒化、资源限制和监控。这些基础设施要求会增加操作开销和安全考量,而直接工具调用则可以避免这些问题。代码执行的好处,包括降低 token 成本、降低延迟、改善工具组合能力,应当与这些实现成本一起权衡。
总结
MCP 为 agents 连接大量工具和系统提供了基础协议。不过,一旦连接了过多 servers,工具定义和结果就可能消耗过多 tokens,降低 agent 效率。
虽然这里的许多问题看起来很新,例如上下文管理、工具组合、状态持久化,但它们在软件工程中都有已知解法。代码执行把这些成熟模式应用到 agents 上,让它们使用熟悉的编程构造,更高效地与 MCP servers 交互。如果你实现了这种方法,我们鼓励你把发现分享给 MCP 社区。
致谢
本文由 Adam Jones 和 Conor Kelly 撰写。感谢 Jeremy Fox、Jerome Swannack、Stuart Ritchie、Molly Vorwerck、Matt Samuels 和 Maggie Vo 对本文草稿提供反馈。