这个其实是skill理念的延续。包括:渐进式披露、脚本化/资源化、可重用脚本。
当你听到mcp的时候,你心中隐射到的对象是"工具",这没有问题;
但你听到skill的时候,你应该映射的并不是一个"技能",而是生成式大模型和脚本共同高效工作的范式。
换句话说,你应该从机制上理解skill,而不是从功效上理解。
另外,sam altman也提过,畅享未来的大模型,应该是只保留逻辑,不保留内容,参数量也大幅降低。这篇文章可以认为实在大模型底模的现状下,在应用层复用了同样的思路:
- 不要把内容直接给大模型
- 大模型写代码(也就是逻辑)来处理内容
发布时间:2025年11月4日
直接的工具调用会为每个定义和结果消耗上下文。通过编写代码来调用工具,agents 能够更好地扩展。本文将介绍这种方法如何与 MCP 配合工作。
Model Context Protocol (MCP) 简介
The Model Context Protocol (MCP) 是一个连接 AI agents 与外部系统的开放标准。传统上,将 agents 连接到工具和数据需要为每个配对进行自定义集成,这造成了碎片化和重复工作,使得扩展真正连接的系统变得困难。MCP 提供了一个通用协议------开发者在他们的 agent 中实现一次 MCP,就能解锁整个集成生态系统。
自 2024 年 11 月推出 MCP 以来,采用速度很快:社区已经构建了数千个 MCP servers,所有主要编程语言都有可用的 SDK,行业已经采用 MCP 作为连接 agents 与工具和数据的事实标准。
如今,开发人员通常构建的 agents 可以访问数十个 MCP servers 上的数百或数千个工具。然而,随着连接工具数量的增长,预先加载所有工具定义并通过上下文窗口传递中间结果会减慢 agents 的速度并增加成本。
在本文中,我们将探讨代码执行如何使 agents 更高效地与 MCP servers 交互,处理更多工具同时使用更少的 tokens。
工具的过度 token 消耗使 agents 效率降低
随着 MCP 使用的扩展,有两种常见模式会增加 agent 成本和延迟:
- 工具定义过载上下文窗口
- 中间工具结果消耗额外的 tokens
1. 工具定义过载上下文窗口
大多数 MCP 客户端预先将所有工具定义直接加载到上下文中,使用直接工具调用语法将它们暴露给模型。这些工具定义可能看起来像:
vbnet
gdrive.getDocument
Description: 从 Google Drive 检索文档
Parameters:
documentId (required, string): 要检索的文档的 ID
fields (optional, string): 要返回的特定字段
Returns: 包含标题、正文内容、元数据、权限等的文档对象
csharp
salesforce.updateRecord
Description: 更新 Salesforce 中的记录
Parameters:
objectType (required, string): Salesforce 对象类型(Lead、Contact、Account 等)
recordId (required, string): 要更新的记录的 ID
data (required, object): 要更新的字段及其新值
Returns: 带确认的更新记录对象
工具描述占用更多上下文窗口空间,增加响应时间和成本。在 agents 连接到数千个工具的情况下,它们需要在读取请求之前处理数十万个 tokens。
2. 中间工具结果消耗额外的 tokens
大多数 MCP 客户端允许模型直接调用 MCP 工具。例如,你可能会要求你的 agent:"从 Google Drive 下载我的会议记录并将其附加到 Salesforce lead。"
模型会进行如下调用:
php
TOOL CALL: gdrive.getDocument(documentId: "abc123")
→ returns "讨论了 Q4 目标...\n[完整记录文本]"
(加载到模型上下文中)
TOOL CALL: salesforce.updateRecord(
objectType: "SalesMeeting",
recordId: "00Q5f000001abcXYZ",
data: { "Notes": "讨论了 Q4 目标...\n[完整记录文本写出]" }
)
(模型需要再次将完整记录写入上下文)
每个中间结果都必须通过模型。在这个例子中,完整的调用记录流经两次。对于一个 2 小时的销售会议,这可能意味着处理额外的 50,000 个 tokens。更大的文档可能超过上下文窗口限制,破坏工作流程。
对于大型文档或复杂数据结构,模型在工具调用之间复制数据时可能更容易出错。

MCP 客户端将工具定义加载到模型的上下文窗口中,并编排一个消息循环,其中每个工具调用和结果在操作之间通过模型传递。
通过代码执行 MCP 提高上下文效率
随着 agents 的代码执行环境变得越来越普遍,一个解决方案是将 MCP servers 作为代码 API 而不是直接工具调用来呈现。然后 agent 可以编写代码与 MCP servers 交互。这种方法解决了两个挑战:agents 可以只加载他们需要的工具,并在将结果传递回模型之前在执行环境中处理数据。
有多种方法可以做到这一点。一种方法是从连接的 MCP servers 生成所有可用工具的文件树。这是一个使用 TypeScript 的实现:
scss
servers
├── google-drive
│ ├── getDocument.ts
│ ├── ... (其他工具)
│ └── index.ts
├── salesforce
│ ├── updateRecord.ts
│ ├── ... (其他工具)
│ └── index.ts
└── ... (其他服务器)
然后每个工具对应一个文件,类似于:
typescript
// ./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 的例子变成了这样的代码:
typescript
// 从 Google Docs 读取记录并添加到 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 }
});
agent 通过探索文件系统发现工具:列出 ./servers/ 目录以找到可用的服务器(如 google-drive 和 salesforce),然后读取它需要的特定工具文件(如 getDocument.ts 和 updateRecord.ts)以理解每个工具的接口。这让 agent 只加载当前任务需要的定义。这将 token 使用量从 150,000 个 tokens 减少到 2,000 个 tokens------节省了 98.7% 的时间和成本。
Cloudflare 发布了类似的发现,将使用 MCP 的代码执行称为"代码模式"。核心洞察是相同的:LLM 擅长编写代码,开发人员应该利用这一优势来构建更高效地与 MCP servers 交互的 agents。
通过代码执行 MCP 的好处
通过代码执行 MCP 使 agents 能够通过按需加载工具、在数据到达模型之前过滤数据以及单步执行复杂逻辑来更有效地使用上下文。使用这种方法还有安全性和状态管理的好处。
渐进式披露
模型擅长导航文件系统。将工具作为文件系统上的代码呈现,允许模型按需读取工具定义,而不是预先读取所有定义。
或者,可以向服务器添加 search_tools 工具以查找相关定义。例如,当使用上面假设的 Salesforce 服务器时,agent 搜索"salesforce"并只加载当前任务需要的那些工具。在 search_tools 工具中包含一个细节级别参数,允许 agent 选择所需的细节级别(如仅名称、名称和描述,或带有模式的完整定义),也有助于 agent 节省上下文并高效找到工具。
上下文高效的工具结果
在处理大型数据集时,agents 可以在代码中过滤和转换结果,然后再返回它们。考虑获取一个 10,000 行的电子表格:
typescript
// 没有代码执行 - 所有行流经上下文
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(`Found ${pendingOrders.length} pending orders`);
console.log(pendingOrders.slice(0, 5)); // 仅记录前 5 行供审查
agent 看到的是 5 行而不是 10,000 行。类似的模式适用于聚合、跨多个数据源的连接或提取特定字段------所有这些都不会膨胀上下文窗口。
更强大和上下文高效的控制流
循环、条件语句和错误处理可以使用熟悉的代码模式完成,而不是链接单个工具调用。例如,如果你需要在 Slack 中收到部署通知,agent 可以编写:
typescript
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');
这种方法比在 agent 循环中交替进行 MCP 工具调用和睡眠命令更高效。
此外,能够写出被执行的条件树也节省了"首个 token 时间"延迟:而不是等待模型评估 if 语句,agent 可以让代码执行环境来做这件事。
保护隐私的操作
当 agents 使用代码执行与 MCP 配合时,中间结果默认保留在执行环境中。这样,agent 只看到你明确记录或返回的内容,意味着你不希望与模型共享的数据可以在你的工作流程中流动,而无需进入模型的上下文。
对于更敏感的工作负载,agent harness 可以自动对敏感数据进行标记化。例如,假设你需要将客户联系详细信息从电子表格导入 Salesforce。agent 编写:
typescript
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 进行标记化:
typescript
// agent 会看到的内容,如果它记录了 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,但从不通过模型。这可以防止 agent 意外记录或处理敏感数据。你也可以使用它来定义确定性的安全规则,选择数据可以流经的来源和目的地。
状态持久化和技能
具有文件系统访问权限的代码执行允许 agents 在操作之间维护状态。Agents 可以将中间结果写入文件,使它们能够恢复工作和跟踪进度:
typescript
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');
Agents 也可以将自己的代码持久化为可重用函数。一旦 agent 为任务开发了工作代码,它可以保存该实现以供将来使用:
typescript
// 在 ./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`;
}
// 稍后,在任何 agent 执行中:
import { saveSheetAsCsv } from './skills/save-sheet-as-csv';
const csvPath = await saveSheetAsCsv('abc123');
这与技能的概念密切相关,技能是模型可重用指令、脚本和资源的文件夹,用于提高专门任务的性能。向这些保存的函数添加 SKILL.md 文件创建了一个结构化技能,模型可以参考和使用。随着时间的推移,这允许你的 agent 构建一个更高级能力的工具箱,演进它最有效工作所需的脚手架。
请注意,代码执行引入了自己的复杂性。运行 agent 生成的代码需要一个安全的执行环境,具有适当的沙盒化、资源限制和监控。这些基础设施要求增加了直接工具调用避免的操作开销和安全考虑。代码执行的好处------减少 token 成本、降低延迟和改进工具组合------应该权衡这些实现成本。
总结
MCP 为 agents 连接到许多工具和系统提供了基础协议。然而,一旦连接了太多服务器,工具定义和结果可能会消耗过多的 tokens,降低 agent 效率。
虽然这里的许多问题看起来很新颖------上下文管理、工具组合、状态持久化------它们在软件工程中有已知的解决方案。代码执行将这些既定模式应用于 agents,让它们使用熟悉的编程构造更高效地与 MCP servers 交互。如果你实施这种方法,我们鼓励你与 MCP 社区分享你的发现。
致谢
本文由 Adam Jones 和 Conor Kelly 撰写。感谢 Jeremy Fox、Jerome Swannack、Stuart Ritchie、Molly Vorwerck、Matt Samuels 和 Maggie Vo 对本文草稿的反馈。