MCP 协议除了众所周知的 Tools(工具),还定义了 Resources(资源)------ 一种让 AI Agent 在对话开始前就"知道"上下文信息的机制。本文从概念到代码,深入解析 Resources 的设计思路和最佳实践。
问题:Agent 的"知识盲区"
每个 AI 编码 Agent 都会遇到同样的问题:模型很聪明,但对你的项目一无所知。
bash
你: "帮我写个新功能"
Agent: "好的,我先看看项目结构..."
→ list_directory
→ read_file (package.json)
→ read_file (src/...)
→ read_file (src/...)
(读了 5 个文件才理解项目)
→ 开始写代码
前几轮对话全花在"了解项目"上。更麻烦的是,有些知识文件里根本没有------比如团队的编码规范、API 鉴权规则、数据库命名约定。
你当然可以每次手动复制粘贴到任务描述里,但那就不是"自主 Agent"了。
MCP 的答案:Resources
MCP 协议定义了三种原语(Primitive):
| 原语 | 作用 | 方向 |
|---|---|---|
| Tools | 让 Agent 做事 | Agent → Server |
| Resources | 让 Agent 知道 | Server → Agent |
| Prompts | 让 Agent 说 | Server → Agent |
Resources 的设计目标很明确:把知识从 prompt 中分离出来,交给专门的服务器管理。
服务端:两种资源定义方式
静态资源 --- 固定 URI,内容不变
适合编码规范、API 约定、部署文档等内容固定的知识:
javascript
server.registerResource(
'编码规范',
'guide://coding-standards', // 固定 URI
{
description: '团队编码规范和最佳实践',
mimeType: 'text/markdown',
},
async (uri) => ({
contents: [{
uri: uri.href,
text: '# 编码规范 v1.0\n\n## 命名约定\n...',
}],
})
);
动态资源模板 --- 参数化 URI
适合项目文件、数据库 Schema 等按路径变化的资源:
javascript
server.registerResource(
'项目文件',
new ResourceTemplate('project://{path}', {
list: async () => ({ // 客户端发现有哪些资源
resources: [
{ uri: 'project://package.json', name: 'package.json' },
{ uri: 'project://CLAUDE.md', name: 'CLAUDE.md' },
],
}),
}),
{ description: '读取项目文件' },
async (uri, variables) => ({ // 客户端读取具体内容
contents: [{ uri: uri.href, text: readFile(variables.path) }],
})
);
客户端:预读 vs 按需,一个关键决策
获取到资源列表后,怎么给模型用?两种策略:
arduino
策略 A: 预读 (preRead: true)
┌──────────────────────────────────────┐
│ System Prompt │
│ ├── 基础指令 ~500 tokens│
│ ├── 编码规范全文 ~2000 tokens│
│ ├── API 文档全文 ~8000 tokens│
│ ├── 数据库 Schema ~5000 tokens│
│ └── 部署配置全文 ~3000 tokens│
│ 总计: ~18500 tokens │
│ │
│ 问题: 即使任务只是"修一个 typo" │
│ 这些资源也每轮都全额付费 │
└──────────────────────────────────────┘
策略 B: 按需读取 (preRead: false)
┌──────────────────────────────────────┐
│ System Prompt │
│ ├── 基础指令 ~500 tokens│
│ ├── 资源目录 ~150 tokens│
│ │ - 编码规范 (guide://...) │
│ │ - API文档 (doc://...) │
│ │ - ... │
│ └── 提示: 用 read_mcp_resource 读取 │
│ 总计: ~650 tokens │
│ │
│ 模型判断需要规范 → 调用工具读取 │
│ → 资源内容作为 ToolMessage 返回 │
│ → 仅本轮多 ~2000 tokens │
└──────────────────────────────────────┘
核心洞察 :Resources 和 Tools 可以互相转化。把"读取资源"包装成工具 read_mcp_resource,模型自己判断何时需要什么知识。这比把所有知识一股脑塞进 system prompt 要聪明得多。
实现:从 URI 到上下文
整条链路分四步:
javascript
// 1. 客户端列出所有服务器的资源
const resources = await client.listResources();
// → { serverA: [{ uri, name, description }], serverB: [...] }
// 2. 构建资源目录(只取元数据,不读内容)
const ctx = await buildResourcesContext(mcpManager, { preRead: false });
// → "## 可用 MCP 资源\n- 编码规范 (guide://...)\n..."
// 3. 注入 system prompt
const prompt = createSystemPrompt(cwd, {
toolNames: [...],
mcpResourcesContext: ctx,
});
// 4. 模型按需调用 read_mcp_resource 工具
// → read_mcp_resource({ serverName: "demo", uri: "guide://coding-standards" })
// → ToolMessage: "# 编码规范 v1.0\n..."
工程实践:三个关键原则
1. 上下文窗口是稀缺资源
System Prompt 中的每个 token 都会在每一轮对话中被计费。把内容放在 ToolMessage(历史消息)中,只在实际用到的那几轮占用窗口。
2. 错误不阻断
资源读取失败只是 warn,不阻止 Agent 启动。Agent 降级为无资源的模式也能工作:
javascript
async getResources() {
try {
return await this.#client.listResources();
} catch (e) {
logger.warn(`获取 resources 失败: ${e.message}`);
return {}; // 降级,不崩溃
}
}
3. 资源 = 元数据 + 内容,分两步给模型
第一步只给元数据(名称、描述、URI),模型不需要内容就能判断是否相关。第二步只在实际需要时才加载内容。这是典型的懒加载模式。
总结
MCP Resources 解决的是一个朴素的问题:如何让 AI Agent 在开始工作前就了解你的世界。
答案不是把整个世界塞进 prompt,而是给它一个目录和一把钥匙------让它自己决定打开哪些门。