MCP 代码执行:构建更高效的 AI 代理

74 阅读11分钟

MCP 代码执行:构建更高效的 AI 代理

原文链接: www.anthropic.com/engineering…

发布日期: 2025年11月4日

摘要: 直接工具调用会为每个定义和结果消耗上下文。代理通过编写代码来调用工具可以更好地扩展。本文介绍如何通过 MCP 实现这一点。


引言

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

自 2024 年 11 月发布 MCP 以来,采用速度非常快:社区已经构建了数千个 MCP 服务器SDK 覆盖了所有主流编程语言,行业也将 MCP 作为连接代理与工具和数据的事实标准。

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

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


工具的过度 token 消耗使代理效率降低

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

  1. 工具定义使上下文窗口过载;
  2. 中间工具结果消耗额外的 token。

💡 两大效率问题概览

问题类型问题描述影响
工具定义过载所有工具定义预先加载到上下文连接数千工具时需处理数十万 token
中间结果消耗工具结果必须通过模型传递大文档可能导致 token 翻倍甚至超限

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.
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

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


2. 中间工具结果消耗额外的 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 架构流程

graph TD
    A[MCP 客户端] --> B[加载所有工具定义到上下文]
    B --> C[LLM 接收请求]
    C --> D[LLM 调用工具]
    D --> E[工具返回结果]
    E --> F[结果加载到模型上下文]
    F --> G{需要更多工具调用?}
    G -->|是| D
    G -->|否| H[LLM 生成最终响应]
    
    style B fill:#ff9999
    style F fill:#ff9999

code-execution-mcp-architecture.png

MCP 客户端将工具定义加载到模型的上下文窗口中,并协调一个消息循环,其中每个工具调用和结果在操作之间都通过模型传递。


使用 MCP 的代码执行提高上下文效率

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

有多种方法可以做到这一点。一种方法是从连接的 MCP 服务器生成所有可用工具的文件树。以下是使用 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 }
});

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

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

💡 代码执行方案 vs 传统方案对比

方面传统直接调用代码执行方案
工具定义加载预先加载全部按需加载
Token 消耗150,000 tokens2,000 tokens
节省比例-98.7%
数据处理全部通过模型在执行环境预处理
错误风险数据复制易出错代码直接操作

MCP 代码执行的优势

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

💡 代码执行优势概览

graph TD
    Root((代码执行优势))
    Root --> A[渐进式披露]
    Root --> B[上下文高效的工具结果]
    Root --> C[更强大的控制流]
    Root --> D[隐私保护操作]
    Root --> E[状态持久化与技能]
    
    A --> A1[按需加载工具定义]
    A --> A2[文件系统导航]
    
    B --> B1[过滤和转换结果]
    B --> B2[减少上下文膨胀]
    
    C --> C1[循环和条件]
    C --> C2[降低延迟]
    
    D --> D1[中间结果留在执行环境]
    D --> D2[敏感数据令牌化]
    
    E --> E1[文件系统状态维护]
    E --> E2[可复用技能函数]

渐进式披露

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

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


上下文高效的工具结果

在处理大型数据集时,代理可以在代码中过滤和转换结果后再返回。考虑获取一个 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

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

💡 数据处理效率对比

场景传统方案代码执行方案减少量
10,000 行表格筛选10,000 行传入模型5 行传入模型99.95%
数据聚合全部原始数据仅聚合结果显著
多源数据连接多次往返传递执行环境内完成大幅减少

更强大且上下文高效的控制流

循环、条件和错误处理可以使用熟悉的代码模式完成,而不是链接单独的工具调用。例如,如果你需要 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 令牌化:

// 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 客户端中的查找来解除令牌化。真实的电子邮件地址、电话号码和姓名从 Google Sheets 流向 Salesforce,但从不通过模型。这防止了代理意外记录或处理敏感数据。你也可以用这个来定义确定性的安全规则,选择数据可以从哪里流向哪里。

💡 隐私保护机制流程

graph LR
    A[原始数据] --> B[MCP 客户端]
    B --> C{PII 检测}
    C -->|包含 PII| D[令牌化处理]
    C -->|无 PII| E[正常传递]
    D --> F[令牌化数据给模型]
    F --> G[模型处理]
    G --> H[代码调用工具]
    H --> I[MCP 客户端解令牌化]
    I --> J[真实数据到目标系统]
    
    style D fill:#90EE90
    style I fill:#90EE90

状态持久化和技能

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

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 的概念密切相关——可复用指令、脚本和资源的文件夹,用于提高模型在专门任务上的性能。在这些保存的函数中添加 SKILL.md 文件会创建一个结构化的技能,模型可以引用和使用。随着时间的推移,这允许你的代理构建一个更高级功能的工具箱,发展它最有效工作所需的脚手架。

请注意,代码执行引入了自身的复杂性。运行代理生成的代码需要一个具有适当沙箱、资源限制和监控的安全执行环境。这些基础设施要求增加了直接工具调用所避免的运营开销和安全考虑。代码执行的好处——减少 token 成本、降低延迟和改进工具组合——应该与这些实现成本进行权衡。

💡 代码执行方案权衡分析

方面优势挑战
Token 成本大幅减少(可达 98%+)-
延迟显著降低-
工具组合更灵活强大-
安全性需要沙箱环境增加基础设施复杂度
资源管理-需要资源限制和监控
运营开销-比直接调用更高

总结

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

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


📝 核心要点

关键发现

  1. 工具定义过载问题:预先加载所有工具定义会占用大量上下文空间,连接数千工具时可能需要处理数十万 token
  2. 中间结果消耗问题:工具结果必须通过模型传递,大文档可能导致 token 翻倍或超限
  3. 代码执行解决方案:将 MCP 服务器呈现为代码 API,代理编写代码与之交互,可减少 98.7% 的 token 消耗

代码执行的五大优势

  1. 渐进式披露:按需加载工具定义,支持通过文件系统或搜索工具发现
  2. 上下文高效:在执行环境中过滤和转换数据,避免上下文膨胀
  3. 强大控制流:使用代码实现循环、条件和错误处理,减少延迟
  4. 隐私保护:中间结果留在执行环境,支持敏感数据自动令牌化
  5. 状态持久化:支持跨操作维护状态,可保存可复用技能函数

实施考虑

  • 代码执行需要安全的沙箱环境
  • 需要考虑资源限制和监控
  • 应权衡减少 token 成本/延迟的收益与实现成本

致谢

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


原文作者: Adam Jones 和 Conor Kelly