通过代码执行提升AI代理效率:MCP协议实践指南

在人工智能快速发展的今天,AI代理已经能够完成越来越多复杂任务。然而,随着代理需要连接的外部工具和数据源不断增加,一个关键问题逐渐浮现:如何让代理在处理大量工具时仍保持高效运行?这正是Model Context Protocol(MCP)和代码执行方法要解决的核心问题。

什么是MCP协议?

Model Context Protocol(MCP)是一个连接AI代理与外部系统的开放标准。想象一下,如果每个AI代理与每个外部系统连接都需要定制开发,就像每部手机只能连接特定品牌的耳机一样,会带来极大的不便和资源浪费。MCP协议相当于提供了一个“通用插头”,开发者只需在代理中实现一次MCP,就能连接整个生态系统的集成工具。

自2024年11月发布以来,MCP协议已被广泛采用:社区构建了数千个MCP服务器,所有主流编程语言都有了相应的SDK,行业已将MCP视为连接代理与工具的实际标准。

为什么需要代码执行?

如今,开发者经常构建能够访问数百甚至数千个工具的AI代理,这些工具分布在几十个MCP服务器上。但随着连接工具数量的增长,传统方法暴露出两个明显问题:

工具定义占用过多资源

大多数MCP客户端会预先将所有工具定义加载到上下文窗口中,使用直接工具调用语法向模型暴露这些工具。每个工具定义都包含详细描述、参数说明和返回类型,看起来可能是这样的:

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

这些工具描述会占用大量上下文窗口空间,增加响应时间和成本。当代理连接到数千个工具时,它在读取请求前就需要处理数十万甚至上百万个token。

中间结果消耗额外资源

大多数MCP客户端允许模型直接调用MCP工具。例如,如果您要求代理“从Google Drive下载我的会议记录并将其附加到Salesforce潜在客户”,模型可能会进行如下调用:

工具调用:gdrive.getDocument(documentId: "abc123")
        → 返回“讨论了第四季度目标...\n[完整记录文本]”
           (加载到模型上下文中)

工具调用:salesforce.updateRecord(
			objectType: "SalesMeeting",
			recordId: "00Q5f000001abcXYZ",
  			data: { "Notes": "讨论了第四季度目标...\n[完整记录文本再次写出]" }
		)
		(模型需要将整个记录文本再次写入上下文)

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

MCP客户端与MCP服务器和LLM的交互示意图

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

代码执行如何提升效率?

随着代码执行环境在代理中变得越来越普遍,一个解决方案是将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;
}

/* 从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代码执行称为“代码模式”。核心观点是相同的:LLM擅长编写代码,开发者应该利用这一优势来构建能更高效地与MCP服务器交互的代理。

代码执行与MCP结合的优势

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

渐进式披露

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

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

上下文高效的工具结果

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

// 无代码执行 - 所有行都流经上下文
工具调用:gdrive.getSheet(sheetId: 'abc123')
        → 返回10000行到上下文中手动过滤

// 有代码执行 - 在执行环境中过滤
const allRows = await gdrive.getSheet({ sheetId: 'abc123' });
const pendingOrders = allRows.filter(row =>
  row["Status"] === 'pending'
);
console.log(`找到${pendingOrders.length}个待处理订单`);
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("收到部署通知");

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

此外,能够编写并执行条件树也节省了“首次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(`更新了${sheet.rows.length}个潜在客户`);

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表格流向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");

这与技能的概念紧密相连——可重复使用的指令、脚本和资源文件夹,供模型改进在专业任务上的性能。向这些保存的函数添加SKILL.md文件会创建一个结构化的技能,模型可以参考和使用。随着时间的推移,这允许您的代理构建一个更高级能力的工具箱,进化出它最有效工作所需的脚手架。

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

实际应用场景

为了更好地理解代码执行与MCP结合的实际价值,让我们看看几个常见的应用场景:

数据处理管道

假设您需要从多个来源提取数据,进行转换,然后加载到目标系统中。传统方法可能需要多次工具调用和大量中间数据传输,而代码执行方法可以这样实现:

// 从多个来源提取数据
const userData = await db.query("SELECT * FROM users WHERE created_at > ?", [lastSync]);
const paymentData = await stripe.getCharges({ limit: 100 });
const supportTickets = await zendesk.getTickets({ status: "open" });

// 在代码中转换数据
const enrichedData = userData.map(user => {
  const payments = paymentData.filter(p => p.customer === user.stripe_id);
  const tickets = supportTickets.filter(t => t.requester_id === user.id);
  
  return {
    user_id: user.id,
    email: user.email,
    total_spent: payments.reduce((sum, p) => sum + p.amount, 0),
    open_tickets: tickets.length,
    last_activity: user.last_login_at
  };
});

// 只将汇总结果发送到模型
console.log(`处理了${enrichedData.length}个用户`);
console.log("前5个用户的数据样本:", enrichedData.slice(0, 5));

// 将结果保存到数据仓库
await bigquery.insertRows("user_metrics", enrichedData);

这种方法避免了将原始数据全部发送到模型上下文,大大减少了token消耗。

复杂工作流自动化

对于涉及多个步骤和条件逻辑的复杂工作流,代码执行提供了更简洁的实现方式:

// 部署和监控工作流
async function deployAndMonitor() {
  try {
    // 开始部署
    const deployment = await k8s.startDeployment({
      image: "my-app:latest",
      replicas: 3
    });
    
    console.log(`部署已启动:${deployment.id}`);
    
    // 等待部署完成
    let isReady = false;
    let attempts = 0;
    
    while (!isReady && attempts < 30) {
      const status = await k8s.getDeploymentStatus(deployment.id);
      isReady = status.ready;
      
      if (!isReady) {
        await new Promise(r => setTimeout(r, 10000)); // 等待10秒
        attempts++;
      }
    }
    
    if (!isReady) {
      throw new Error("部署超时");
    }
    
    // 运行健康检查
    const health = await healthCheck.runAll();
    
    // 发送通知
    await slack.sendMessage("#deployments", 
      `部署 ${deployment.id} 已完成。健康检查:${health.passed ? "通过" : "失败"}`
    );
    
    return { success: true, deploymentId: deployment.id };
    
  } catch (error) {
    await slack.sendMessage("#deployments-alerts", 
      `部署失败:${error.message}`
    );
    return { success: false, error: error.message };
  }
}

// 执行工作流
const result = await deployAndMonitor();
console.log(`工作流完成:${result.success ? "成功" : "失败"}`);

实施考虑因素

在决定采用代码执行方法时,有几个重要因素需要考虑:

安全性和沙盒

运行代理生成的代码需要严格的安全措施:

  • 🍂
    资源限制:限制CPU、内存和运行时间
  • 🍂
    网络访问控制:限制可以访问的网络端点
  • 🍂
    文件系统沙盒:限制文件访问权限
  • 🍂
    代码审查:对敏感操作进行人工审查或审批流程

错误处理和调试

代码执行环境需要完善的错误处理机制:

// 错误处理示例
try {
  const result = await someMCPServer.someOperation(params);
  console.log("操作成功:", result);
} catch (error) {
  console.error("操作失败:", error.message);
  
  // 根据错误类型采取不同措施
  if (error.code === 'RATE_LIMITED') {
    await new Promise(r => setTimeout(r, 60000)); // 等待1分钟
    // 重试逻辑
  } else if (error.code === 'AUTH_ERROR') {
    // 重新认证逻辑
    await refreshAuthToken();
  }
}

性能监控

监控代码执行的性能至关重要:

  • 🍂
    执行时间跟踪
  • 🍂
    资源使用情况监控
  • 🍂
    Token使用量统计
  • 🍂
    错误率和成功率指标

常见问题解答

MCP代码执行适用于哪些场景?

MCP代码执行特别适合以下场景:

  • 🍂
    需要处理大量数据的任务
  • 🍂
    涉及多个工具调用的复杂工作流
  • 🍂
    需要数据转换或过滤的操作
  • 🍂
    涉及敏感数据的处理
  • 🍂
    需要状态持久化的长时间运行任务

代码执行会增加系统复杂性吗?

是的,代码执行确实会增加一些系统复杂性,需要安全沙盒、资源管理和监控基础设施。但对于处理复杂任务或大量工具的代理来说,这种复杂性通常会被显著的性能提升和成本节约所抵消。

如何平衡直接工具调用和代码执行?

一个实用的方法是采用混合策略:

  • 🍂
    简单任务使用直接工具调用
  • 🍂
    复杂任务、数据处理或涉及多个步骤的工作流使用代码执行
  • 🍂
    根据任务复杂性动态选择方法

代码执行对代理响应时间有何影响?

虽然代码执行可能增加“首次token时间”(因为代理需要编写代码),但它通常能显著减少总体任务完成时间,特别是对于复杂任务,因为它减少了模型需要处理的中间结果数量。

总结

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

虽然这里的许多问题感觉新颖——上下文管理、工具组合、状态持久化——但它们在软件工程中已有已知的解决方案。代码执行将这些已建立的模式应用于代理,让它们使用熟悉的编程构造来更高效地与MCP服务器交互。

通过采用代码执行方法,开发者可以构建能够处理更复杂任务、同时保持高效运行和合理成本的AI代理。随着AI代理在日常业务操作中扮演越来越重要的角色,这种效率提升将变得至关重要。

无论您是构建内部工具的自定义AI代理,还是开发面向客户的人工智能产品,结合MCP和代码执行都值得考虑。这种方法不仅降低了运营成本,还使代理能够处理更复杂、更有价值的任务,最终带来更好的用户体验和业务成果。