MCP工具:直接调用 vs. 代码执行

直接调用工具会消耗每个定义和结果的上下文信息。AI代理通过编写代码来调用MCP工具,可以更好地扩展。

MCP工具:直接调用 vs. 代码执行

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

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

如今,开发人员通常构建具有数百或数千个工具访问权限的代理,这些工具分布在几十个 MCP 服务器上。然而,随着连接工具数量的增长,一次性加载所有工具定义并传递中间结果会减慢代理的速度并增加成本。

在本文中,我们将探讨如何通过代码执行使代理更高效地与 MCP 服务器交互,在使用更少标记的同时处理更多工具。

1、过多的标记消耗使代理效率降低

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

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

2.1 工具定义使上下文窗口过载

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

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

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

1.2 中间工具结果消耗额外的标记

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

模型将进行如下调用:

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

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

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

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

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

2、使用 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 文档中读取转录本并添加到 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)以了解每个工具的接口。这使得代理仅加载当前任务所需的定义。这将标记使用量从 150,000 标记减少到 2,000 标记——节省了 98.7% 的时间和成本。

Cloudflare 发表了类似的研究,将使用 MCP 的代码执行称为“代码模式”。核心观点是一样的:LLM 在编写代码方面很擅长,开发者应该利用这一优势来构建与 MCP 服务器更高效交互的代理。

3、使用 MCP 的代码执行的好处

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

3.1 渐进式披露

模型擅长导航文件系统。将工具作为文件系统上的代码呈现,允许模型按需读取工具定义,而不是一开始就全部读取。

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

3.2 上下文高效的工具结果

在处理大型数据集时,代理可以在代码中过滤和转换结果后再返回。考虑获取一个 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(`找到 ${pendingOrders.length} 个待处理订单`);
console.log(pendingOrders.slice(0, 5)); // 仅记录前 5 个以供审查

代理看到的是五行而不是一万行。类似的模式适用于聚合、跨多个数据源的连接或提取特定字段——而不会使上下文窗口膨胀。

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

循环、条件语句和错误处理可以通过熟悉的代码模式完成,而不是链式调用单个工具调用。例如,如果需要在 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 工具调用和睡眠命令更高效。

此外,能够写出一个条件树并执行它也可以节省“首次标记时间”延迟:而不是等待模型评估 if 语句,代理可以让代码执行环境来处理这个。

3.4 隐私保护操作

当代理使用 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 Sheets 流向 Salesforce,但从未经过模型。这防止了代理意外记录或处理敏感数据。你还可以使用此方法定义确定性的安全规则,选择数据可以流向何处和来自何处。

3.5 状态持久化和技能

带有文件系统访问权限的代码执行使代理能够在操作之间保持状态。代理可以将中间结果写入文件,使其能够继续工作并跟踪进度:

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 文件会创建一个结构化的技能,模型可以引用和使用它。随着时间的推移,这允许您的代理建立一个更高层次能力的工具箱,从而进化其最有效地工作的支撑结构。

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

4、结束语

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

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


原文链接:Code execution with MCP: Building more efficient agents

汇智网翻译整理,转载请标明出处