找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

389

积分

0

好友

53

主题
发表于 前天 23:50 | 查看: 5| 回复: 0

直接的工具调用会为每个定义和结果消耗上下文。通过编写代码来调用工具,智能体的扩展性会更好。本文将介绍如何通过 MCP 实现这一点。

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

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

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

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

工具带来的过度 token 消耗降低了智能体效率

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

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

1. 工具定义过载了上下文窗口

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

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

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

2. 中间工具结果消耗了额外的 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 进行代码执行可提高上下文效率

随着代码执行环境在智能体中变得越来越普遍,一种解决方案是将 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 的代码执行称为“代码模式 (Code Mode)”。其核心见解是相同的:LLM 擅长编写代码,开发者应该利用这一优势来构建能更有效地与 MCP 服务器交互的智能体。

使用 MCP 进行代码执行的好处

通过 MCP 进行代码执行,智能体能够按需加载工具,在数据到达模型前进行过滤,并能在单一步骤中执行复杂逻辑,从而更有效地利用上下文。这种方法在安全和状态管理方面也有优势。

渐进式披露 (Progressive disclosure)

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

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

上下文高效的工具结果

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

// 不使用代码执行 - 所有行都流经上下文
工具调用: gdrive.getDocument(documentId: '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 个以供审查

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

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

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

let found = false;
while (!found) {
  const messages = await slack.getChannelHistory({ channel: 'C123456' });
  found = messages.some(m => m.text.includes('部署完成'));
  if (!found) await new Promise(r => setTimeout(r, 5000));
}
console.log('已收到部署通知');

这种方法比在智能体循环中交替使用 MCP 工具调用和休眠命令更有效率。

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

隐私保护操作

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

对于更敏感的工作负载,智能体工具链可以自动对个人身份信息 (PII) 进行标记化 (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(`更新了 ${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 客户端中的查找进行反标记化 (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 社区 分享你的发现。




上一篇:串口通讯原理与实战:ASCII码、数据帧及波特率详解
下一篇:线程池性能调优实战:CPU密集与IO密集型任务配置指南
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-8 08:44 , Processed in 0.100008 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表