返回 文章 build CMS 文章

使用 MCP 进行代码执行:构建更高效的 AI 智能体

用代码执行替代直接工具调用,让 AI 智能体以更少 token 和更低延迟操作 MCP 工具。

MCP代码执行AI智能体Anthropic
成长分 / 100 76 综合收获、行动、留存与影响

使用 MCP 进行代码执行:构建更高效的 AI 智能体
为什么值得读了解 MCP 规模扩大后工具定义和中间结果如何拖慢智能体并增加成本。

学习将 MCP 服务器呈现为代码 API 的具体实现方法,包括文件树和 search_tools 模式。

关键洞察
  1. 传统 MCP 客户端预先加载所有工具定义,当连接数千个工具时,模型需处理数十万 token,增加响应时间和成本。
  2. 将 MCP 服务器呈现为代码 API,智能体通过探索文件系统按需加载工具定义,可将 token 使用量从 150,000 减少到 2,000,节省 98.7%。
  3. 代码执行允许智能体在返回结果前过滤和转换大型数据集,例如从 10,000 行电子表格中仅返回 5 行,避免上下文膨胀。
转成行动

深入阅读

正文与原文对照

原文保真覆盖:全文原文字符:13430

获取开发者新闻通讯

产品更新、操作指南、社区聚焦等。每月发送至您的收件箱。

模型上下文协议(MCP) 是一个用于将 AI 智能体连接到外部系统的开放标准。传统上,将智能体连接到工具和数据需要为每一对组合进行自定义集成,这造成了碎片化和重复工作,使得真正互联的系统难以扩展。MCP 提供了一个通用协议——开发者只需在其智能体中实现一次 MCP,即可解锁整个集成生态系统。

自 2024 年 11 月推出 MCP 以来,其采用速度非常快:社区已构建了数千个 MCP 服务器SDK 已支持所有主流编程语言,业界已将 MCP 采纳为连接智能体与工具和数据的事实标准。

如今,开发者经常构建能够访问数十个 MCP 服务器中数百或数千个工具的智能体。然而,随着连接工具数量的增长,预先加载所有工具定义并通过上下文窗口传递中间结果会拖慢智能体速度并增加成本。

在本博客中,我们将探讨代码执行如何使智能体更高效地与 MCP 服务器交互,处理更多工具的同时使用更少的 token。

随着 MCP 使用规模的扩大,有两种常见模式会增加智能体成本和延迟:

大多数 MCP 客户端预先将所有工具定义直接加载到上下文中,使用直接工具调用语法将其暴露给模型。这些工具定义可能如下所示:

gdrive.getDocument
描述:从 Google Drive 检索文档
参数:
documentId(必填,字符串):要检索的文档的 ID
fields(可选,字符串):要返回的特定字段
返回:包含标题、正文内容、元数据、权限等的文档对象
salesforce.updateRecord
描述:更新 Salesforce 中的一条记录
参数:
objectType(必填,字符串):Salesforce 对象的类型(Lead、Contact、Account 等)
recordId(必填,字符串):要更新的记录的 ID
data(必填,对象):要更新的字段及其新值
返回:带有确认信息的已更新记录对象

工具描述会占用更多上下文窗口空间,增加响应时间和成本。当智能体连接到数千个工具时,它们需要在读取请求之前处理数十万个 token。

大多数 MCP 客户端允许模型直接调用 MCP 工具。例如,你可能会向你的智能体提出:“从 Google Drive 下载我的会议记录,并将其附加到 Salesforce 潜在客户。”

模型会进行如下调用:

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个token。更大的文档甚至可能超出上下文窗口限制,导致工作流中断。

对于大型文档或复杂的数据结构,模型在工具调用之间复制数据时可能更容易出错。

随着代码执行环境在智能体中越来越普遍,一种解决方案是将MCP服务器呈现为代码API,而不是直接的工具调用。智能体随后可以编写代码与MCP服务器交互。这种方法同时解决了这两个挑战:智能体可以只加载所需的工具,并在将结果传回模型之前在执行环境中处理数据。

有多种方法可以实现这一点。一种方法是从连接的MCP服务器生成所有可用工具的文件树。以下是使用TypeScript的实现:

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 */
export async function getDocument(input: GetDocumentInput): Promise<GetDocumentResponse> {
return callMCPTool<GetDocumentResponse>('google_drive__get_document', input);
}

我们上面那个 Google Drive 到 Salesforce 的示例就变成了这样的代码:

// 从 Google Docs 读取转录内容并添加到 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 }
});

代理通过探索文件系统来发现工具:列出 ./servers/

目录以查找可用的服务器(如 google-drive

salesforce

),然后读取它需要的特定工具文件(如 getDocument.ts

updateRecord.ts

)以了解每个工具的接口。这使代理只加载当前任务所需的定义。这将令牌使用量从 150,000 个令牌减少到 2,000 个令牌——节省了 98.7% 的时间和成本

Cloudflare 发表了类似的发现,将使用 MCP 的代码执行称为“代码模式”。核心见解相同:LLM 擅长编写代码,开发人员应利用这一优势来构建更高效地与 MCP 服务器交互的代理。

使用 MCP 的代码执行使代理能够更高效地使用上下文,通过按需加载工具、在数据到达模型之前过滤数据,并在单一步骤中执行复杂逻辑。使用这种方法还有安全和状态管理方面的好处。

模型非常擅长导航文件系统。将工具作为文件系统上的代码呈现,允许模型按需读取工具定义,而不是预先读取所有定义。

或者,可以向服务器添加一个 search_tools

工具来查找相关定义。例如,在使用上面假设的 Salesforce 服务器时,代理搜索“salesforce”并仅加载当前任务所需的那些工具。在 search_tools

工具中包含一个详细级别参数,允许代理选择所需的详细级别(例如仅名称、名称和描述,或带有模式的完整定义),也有助于代理节省上下文并高效地查找工具。

在处理大型数据集时,代理可以在返回结果之前在代码中过滤和转换结果。考虑获取一个 10,000 行的电子表格:

// 无代码执行 - 所有行都流经上下文
TOOL CALL: gdrive.getSheet(sheetId: 'abc123')
→ 在上下文中返回 10,000 行以供手动筛选
// 有代码执行 - 在执行环境中筛选
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

智能体看到的是五行,而不是一万行。类似的模式也适用于聚合、跨多个数据源的连接,或提取特定字段——所有这些都不会使上下文窗口膨胀。

循环、条件判断和错误处理可以用熟悉的代码模式来完成,而不是串联单个工具调用。例如,如果你需要在 Slack 中收到部署通知,智能体可以编写:

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

这种方法比通过智能体循环在 MCP 工具调用与 sleep 命令之间交替执行更高效。

此外,能够写出一个会被执行的条件树也节省了“首 token 时间”延迟:智能体无需等待模型来评估 if 语句,而是可以让代码执行环境来完成这件事。

当智能体将代码执行与 MCP 结合使用时,中间结果默认保留在执行环境中。这样,智能体只能看到你显式记录或返回的内容,这意味着你不希望与模型共享的数据可以在你的工作流中流转,而无需进入模型的上下文。

对于更敏感的工作负载,智能体框架可以自动对敏感数据进行令牌化。例如,假设你需要将电子表格中的客户联系方式导入 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`);

MCP 客户端会拦截数据,并在其到达模型之前对 PII 进行标记化:

// 如果代理记录了 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 客户端中的查找进行去令牌化。真实的电子邮件地址、电话号码和姓名从 Google Sheets 流向 Salesforce,但从不经过模型。这可以防止智能体意外记录或处理敏感数据。你还可以使用它来定义确定性的安全规则,选择数据可以流向何处以及从何处流入。

具有文件系统访问权限的代码执行允许智能体在操作之间保持状态。智能体可以将中间结果写入文件,使它们能够恢复工作并跟踪进度:

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

智能体还可以将自己的代码持久化为可复用的函数。一旦智能体为某项任务开发出可运行的代码,它就可以保存该实现以供将来使用:

// 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 文件,就能创建一个结构化的技能,供模型引用和使用。随着时间推移,这能让你的 agent 构建起一个更高层能力的工具箱,逐步演化出它高效工作所需的脚手架。

请注意,代码执行本身也带来了复杂性。运行 agent 生成的代码需要一个安全的执行环境,配备适当的沙箱、资源限制和监控。这些基础设施要求带来了额外的运维开销和安全考量,而直接工具调用则能避免这些问题。代码执行的好处——降低 token 成本、减少延迟、改善工具组合——应当与这些实现成本进行权衡。

MCP 为 agent 连接众多工具和系统提供了一个基础协议。然而,一旦连接的服务器过多,工具定义和结果就可能消耗过多 token,降低 agent 的效率。

尽管这里的许多问题看似新颖——上下文管理、工具组合、状态持久化——但它们在软件工程中已有已知的解决方案。代码执行将这些成熟的模式应用于 agent,让它们能够使用熟悉的编程结构,更高效地与 MCP 服务器交互。如果你实现了这一方法,我们鼓励你与 MCP 社区分享你的发现。

本文由 Adam Jones 和 Conor Kelly 撰写。感谢 Jeremy Fox、Jerome Swannack、Stuart Ritchie、Molly Vorwerck、Matt Samuels 和 Maggie Vo 对本文草稿的反馈。

产品更新、操作指南、社区聚焦等。每月发送到你的收件箱。