Contents

Anthropic代码执行配合 MCP:构建更高效的智能体[译]

直接调用工具会消耗上下文。智能体(Agent)若改为编写代码来调用工具,扩展性会更好。模型上下文协议(MCP)为此提供了实现路径。

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

自 2024 年 11 月 MCP 发布以来,它被迅速采用:社区已构建了数千个 MCP 服务器,主流编程语言均已支持 SDK,业界已将 MCP 采纳为连接智能体与工具和数据的事实标准。

如今,开发者们平常构建的智能体动辄就能访问跨越几十个 MCP 服务器的成百上千个工具。然而,随着连接的工具数量增长,预先加载所有工具定义并将中间结果传入上下文窗口,会拖慢智能体并增加成本。

本文将探讨代码执行如何让智能体更高效地与 MCP 服务器交互,用更少的 Token 处理更多的工具。


🪫 工具带来的额外 Token 消耗,降低了智能体效率

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

  • 工具定义撑爆上下文窗口;
  • 中间的工具结果消耗额外 Token。

1. 工具定义撑爆上下文窗口

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

从 Google Drive 检索文档

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 中的一条记录

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 的潜在客户(lead)记录中。”

模型将进行如下调用:

工具调用:gdrive.getDocument(documentId: “abc123”) → 返回 “讨论了Q4目标…\n[完整纪要文本]” (加载到模型上下文中)

工具调用:salesforce.updateRecord( objectType: “SalesMeeting”, recordId: “00Q5f000001abcXYZ”, data: { “Notes”: “讨论了Q4目标…\n[完整纪要文本又写了一遍]” } ) (模型需要把完整的纪要又在上下文里写一次)

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

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

/images/image.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;
}

/* 从 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-drivesalesforce),然后读取它需要的特定工具文件(如 getDocument.tsupdateRecord.ts)来理解每个工具的接口。

这让智能体只加载当前任务所需的定义。Token 用量从 150,000 个降至 2,000 个——节省了 98.7% 的时间和成本。

Cloudflare 也发布了类似的发现,他们将 MCP 的代码执行称为“代码模式”(Code Mode)。核心见解是相同的:LLM 擅长编写代码,开发者应利用这一优势来构建能更高效地与 MCP 服务器交互的智能体。


💡 代码执行配合 MCP 的好处

代码执行使智能体能更有效地利用上下文,它可以按需加载工具、在数据到达模型前进行过滤,并能一步执行复杂逻辑。此外,这种方法在安全性和状态管理方面也有优势。

按需加载 (Progressive disclosure)

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

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

高上下文效率的工具结果

在处理大型数据集时,智能体可以在代码中过滤和转换结果,然后再将其返回。设想一下获取一个有 10,000 行的电子表格:

// 不用代码执行 - 所有行都流经上下文
TOOL CALL: gdrive.getSheet(sheetId: 'abc123')
        → returns 10,000 rows in context to filter manually

// 使用代码执行 - 在执行环境里过滤
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)); // 只打印前5个供审查

智能体最终只看到了 5 行,而不是 10,000 行。类似的模式(如聚合、跨多个数据源的连接或提取特定字段)都适用——全都不需要撑爆上下文窗口。

更强大、更省上下文的控制流

循环、条件判断和错误处理,都可以用熟悉的代码模式来完成,而不是一连串的单个工具调用。例如,如果你需要在 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 工具和睡眠命令要高效得多。

此外,能够写出一个可被执行的条件树,也节省了“首个 Token 响应时间”的延迟:智能体不必等待模型来评估一个 if 语句,而是让代码执行环境来做这件事。

保护隐私的操作

当智能体使用代码执行配合 MCP 时,中间结果默认保留在执行环境中。这样,智能体只看得到你明确打印或返回的内容,这意味着你不想与模型共享的数据可以在工作流中流转,而永远不会进入模型的上下文。

对于更敏感的工作负载,智能体“外壳”(harness)可以自动对敏感数据进行“脱敏”(tokenize)。例如,假设你需要将客户联系方式从电子表格导入 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 客户端会通过查找将其“反脱敏”(untokenized)。真实的电子邮件地址、电话号码和姓名从 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);

// 稍后的执行可以从上次中断的地方继续
const saved = await fs.readFile('./workspace/leads.csv', 'utf-8');

智能体还可以将自己的代码保存为可复用的函数。一旦智能体为某个任务开发了可用的代码,它可以将该实现保存起来以备将来使用:

// 存于 ./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`;
}

// 之后,在任何智能体执行中:
import { saveSheetAsCsv } from './skills/save-sheet-as-csv';
const csvPath = await saveSheetAsCsv('abc123');

这与“技能”(Skills)的概念紧密相关——“技能”是包含可复用指令、脚本和资源的文件夹,供模型用于提高特定任务的性能。在这些保存的函数中添加一个 SKILL.md 文件,可以创建一个模型可以引用和使用的结构化技能。随着时间的推移,这使你的智能体能建立起一个更高级别的能力工具箱,不断进化出它高效工作所需的“脚手架”。

请注意,代码执行本身也带来了复杂性。运行智能体生成的代码需要一个安全的执行环境,具备适当的**“沙盒”(sandboxing)**、资源限制和监控。这些基础设施要求增加了额外的运维开销和安全考量,而这些是直接工具调用所没有的。在权衡时,应充分考虑代码执行的好处(减少 Token 成本、降低延迟、改进工具组合)与这些实现成本。


总结

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

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

致谢

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