Anthropic · 工程博客

使用MCP执行代码:构建更高效的AI agent

Code execution with MCP: building more efficient AI agents

二〇二六年七月三十日 · 英文原文

模型上下文协议(MCP)是一个开放标准,用于将AI agent连接到外部系统,避免为每对组合进行自定义集成。自2024年11月推出以来,社区已构建数千个MCP服务器,所有主流编程语言均有可用SDK。当agent连接数百至数千个工具时,一次性加载所有工具定义和传递中间结果会消耗大量token并增加延迟。解决方案是将MCP服务器作为代码API,让agent通过代码执行按需加载工具、在上下文中过滤数据,并使用循环、条件语句等编程结构。例如,agent通过浏览文件系统发现工具,仅加载当前任务所需定义,将token使用量从150,000减少至2,000(节省98.7%)。代码执行还带来数据隐私和状态管理优势,但需注意安全执行环境等基础设施开销。本文由Adam Jones和Conor Kelly撰写。

模型上下文协议(MCP)

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

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

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

在这篇博客中,我们将探讨代码执行如何让 agent 更高效地与 MCP 服务器交互,在消耗更少 token 的同时处理更多工具。

问题:MCP 使用规模扩大时的成本与延迟

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

1. 工具定义膨胀

大多数 MCP 客户端会一次性将所有工具定义直接加载到上下文中,使用直接工具调用语法将其暴露给模型。这些工具定义可能看起来像:

{
  name: "get_document",
  description: "从 Google Drive 获取文档内容",
  inputSchema: {
    type: "object",
    properties: {
      documentId: { type: "string" },
      mimeType: { type: "string" }
    }
  }
}

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

2. 中间结果传递

大多数 MCP 客户端允许模型直接调用 MCP 工具。例如,你可能会问你的 agent:"从 Google Drive 下载我的会议记录,并将其附加到 Salesforce 线索中。"

模型会进行如下调用:

call_tool("google-drive", "get_document", { documentId: "abc123" })
→ 返回 50,000 token 的会议记录

call_tool("salesforce", "update_record", { 
  object: "Lead", 
  id: "00Q123", 
  data: { transcript: "50,000 token 的会议记录..." } 
})

每个中间结果都必须经过模型。在这个例子中,完整的通话记录会经过两次。对于一个两小时的销售会议,这可能意味着额外处理 50,000 个 token。更大的文档甚至可能超出上下文窗口限制,导致工作流中断。

此外,在处理大型文档或复杂数据结构时,模型在工具调用之间复制数据时更容易出错。

解决方案:将 MCP 服务器作为代码 API

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

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

// 从 MCP 服务器生成工具文件
const serverConfig = {
  "google-drive": {
    tools: ["getDocument", "searchFiles", "listFolders"]
  },
  "salesforce": {
    tools: ["updateRecord", "queryRecords", "createRecord"]
  }
}

然后每个工具对应一个文件,例如:

// ./servers/google-drive/getDocument.ts
export async function getDocument(documentId: string, mimeType?: string) {
  return await mcpClient.callTool("google-drive", "get_document", {
    documentId, mimeType
  });
}

我们上面提到的 Google Drive 到 Salesforce 的例子变成了如下代码:

const transcript = await getDocument("abc123", "text/plain");
await updateRecord("Lead", "00Q123", { transcript });

agent 通过浏览文件系统来发现工具:列出 ./servers/ 目录以找到可用的服务器(如 google-drive 和 salesforce),然后读取它需要的特定工具文件(如 getDocument.ts 和 updateRecord.ts)来了解每个工具的接口。这让 agent 只加载当前任务所需的定义。这将 token 使用量从 150,000 个减少到 2,000 个——节省了 98.7% 的时间和成本。

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

代码执行的优势

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

按需加载工具

模型非常擅长浏览文件系统。将工具作为文件系统上的代码呈现,允许模型按需读取工具定义,而不是一次性全部读取。

或者,可以在服务器上添加一个 search_tools 工具来查找相关定义。例如,在使用上述假设的 Salesforce 服务器时,agent 搜索"salesforce"并只加载当前任务所需的那些工具。在 search_tools 工具中包含一个细节级别参数,允许 agent 选择所需的详细程度(如仅名称、名称和描述,或包含模式的完整定义),这也有助于 agent 节省上下文并高效地找到工具。

在上下文中过滤数据

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

const rows = await getSheetData("sheet123");
const recentLeads = rows.filter(row => 
  new Date(row.createdDate) > new Date("2024-01-01")
);
return recentLeads.slice(0, 5);

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

使用熟悉的编程结构

循环、条件语句和错误处理可以使用熟悉的代码模式完成,而不是链接单个工具调用。例如,如果你需要在 Slack 中发送部署通知,agent 可以编写:

const services = ["api", "web", "worker"];
for (const service of services) {
  const status = await deploy(service);
  await sendSlackMessage("#deployments", 
    `Deployed ${service}: ${status}`);
  await sleep(1000); // 避免速率限制
}

这种方法比通过 agent 循环在 MCP 工具调用和 sleep 命令之间交替更高效。

此外,能够编写一个可执行的条件树也节省了"首次 token 时间"的延迟:agent 可以让代码执行环境来评估 if 语句,而不是等待模型来评估。

数据隐私

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

对于更敏感的工作负载,agent 框架可以自动对敏感数据进行 token 化。例如,假设你需要将电子表格中的客户联系详情导入 Salesforce。agent 编写:

const customers = await getSheetData("contacts");
for (const customer of customers) {
  await createSalesforceRecord("Contact", {
    email: customer.email,
    phone: customer.phone,
    name: customer.name
  });
}

MCP 客户端在数据到达模型之前拦截数据并对 PII 进行 token 化:

// 实际数据被 token 化
const tokenizedEmail = "tok_abc123";
const tokenizedPhone = "tok_def456";

然后,当数据在另一个 MCP 工具调用中共享时,通过 MCP 客户端中的查找进行去 token 化。真实的电子邮件地址、电话号码和姓名从 Google Sheets 流向 Salesforce,但从未经过模型。这可以防止 agent 意外记录或处理敏感数据。你还可以使用它来定义确定性的安全规则,选择数据可以流向何处。

状态管理

具有文件系统访问权限的代码执行允许 agent 在操作之间维护状态。agent 可以将中间结果写入文件,从而能够恢复工作并跟踪进度:

// 保存进度
await writeFile("./progress.json", JSON.stringify({
  processed: 100,
  total: 1000,
  lastId: "rec_789"
}));

agent 还可以将自己的代码持久化为可重用的函数。一旦 agent 为某个任务开发了可工作的代码,它可以保存该实现以供将来使用:

// 保存为可重用的技能
await writeFile("./skills/lead-scoring.ts", `
export async function scoreLead(lead: Lead): Promise<number> {
  // 之前开发的评分逻辑
}
`);

这与技能(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 对本文草稿的反馈。

译自 Anthropic · 工程博客 · 录于 二〇二六年七月三十日