docs: add MCP, plugin/LSP tool, and RAG notes

- notes/mcp-overview.md: MCP 协议概览,跟 tool use 的分层差异
- notes/mcp-client-internals.md: MCP client 内部实现(发现/连接/catalog)
- notes/plugin-tools.md: plugin tool 与 native/MCP 的对比
- notes/lsp-tools.md: LSP 协议科普 + OpenClaw LSP tool 包装
- notes/rag-overview.md: RAG 与向量化(向量只用来检索,prompt 用原文)
- notes/blog/mcp-101.md: 面向开发者的 MCP 科普,LSP 类比引入
- README 索引更新
This commit is contained in:
clz
2026-06-16 00:59:32 +08:00
parent bc510c269b
commit 3c0cd86352
14 changed files with 2009 additions and 6 deletions
+1
View File
@@ -0,0 +1 @@
{}
+1
View File
@@ -0,0 +1 @@
{}
+33
View File
@@ -0,0 +1,33 @@
{
"file-explorer": true,
"global-search": true,
"switcher": true,
"graph": true,
"backlink": true,
"canvas": true,
"outgoing-link": true,
"tag-pane": true,
"footnotes": false,
"properties": true,
"page-preview": true,
"daily-notes": true,
"templates": true,
"note-composer": true,
"command-palette": true,
"slash-command": false,
"editor-status": true,
"bookmarks": true,
"markdown-importer": false,
"zk-prefixer": false,
"random-note": false,
"outline": true,
"word-count": true,
"slides": false,
"audio-recorder": false,
"workspaces": false,
"file-recovery": true,
"publish": false,
"sync": true,
"bases": true,
"webviewer": false
}
+181
View File
@@ -0,0 +1,181 @@
{
"main": {
"id": "c5399a9acbb77689",
"type": "split",
"children": [
{
"id": "983bb3f03ba0267b",
"type": "tabs",
"children": [
{
"id": "d73d0bfd1d682067",
"type": "leaf",
"state": {
"type": "empty",
"state": {},
"icon": "lucide-file",
"title": "新标签页"
}
}
]
}
],
"direction": "vertical"
},
"left": {
"id": "65df11f83c8b6769",
"type": "split",
"children": [
{
"id": "78cc3f055f0cbebc",
"type": "tabs",
"children": [
{
"id": "77a689742a641c6d",
"type": "leaf",
"state": {
"type": "file-explorer",
"state": {
"sortOrder": "alphabetical",
"autoReveal": false
},
"icon": "lucide-folder-closed",
"title": "文件列表"
}
},
{
"id": "bfd0dd36c851aa00",
"type": "leaf",
"state": {
"type": "search",
"state": {
"query": "",
"matchingCase": false,
"explainSearch": false,
"collapseAll": false,
"extraContext": false,
"sortOrder": "alphabetical"
},
"icon": "lucide-search",
"title": "搜索"
}
},
{
"id": "368ed5ad7ec0d3ee",
"type": "leaf",
"state": {
"type": "bookmarks",
"state": {},
"icon": "lucide-bookmark",
"title": "书签"
}
}
]
}
],
"direction": "horizontal",
"width": 300
},
"right": {
"id": "8b36ffcad1340b80",
"type": "split",
"children": [
{
"id": "cb25124ebc9d68ef",
"type": "tabs",
"children": [
{
"id": "d2956a3ad0a6ce8e",
"type": "leaf",
"state": {
"type": "backlink",
"state": {
"collapseAll": false,
"extraContext": false,
"sortOrder": "alphabetical",
"showSearch": false,
"searchQuery": "",
"backlinkCollapsed": false,
"unlinkedCollapsed": true
},
"icon": "links-coming-in",
"title": "反向链接"
}
},
{
"id": "8dcc2f42bc2674ca",
"type": "leaf",
"state": {
"type": "outgoing-link",
"state": {
"linksCollapsed": false,
"unlinkedCollapsed": true
},
"icon": "links-going-out",
"title": "出链"
}
},
{
"id": "91e1edfbd442848d",
"type": "leaf",
"state": {
"type": "tag",
"state": {
"sortOrder": "frequency",
"useHierarchy": true,
"showSearch": false,
"searchQuery": ""
},
"icon": "lucide-tags",
"title": "标签"
}
},
{
"id": "33675859567823b1",
"type": "leaf",
"state": {
"type": "all-properties",
"state": {
"sortOrder": "frequency",
"showSearch": false,
"searchQuery": ""
},
"icon": "lucide-archive",
"title": "添加笔记属性"
}
},
{
"id": "4d537f742f8042ed",
"type": "leaf",
"state": {
"type": "outline",
"state": {
"followCursor": false,
"showSearch": false,
"searchQuery": ""
},
"icon": "lucide-list",
"title": "大纲"
}
}
]
}
],
"direction": "horizontal",
"width": 300,
"collapsed": true
},
"left-ribbon": {
"hiddenItems": {
"switcher:打开快速切换": false,
"graph:查看关系图谱": false,
"canvas:新建白板": false,
"daily-notes:打开/创建今天的日记": false,
"templates:插入模板": false,
"command-palette:打开命令面板": false,
"bases:新建数据库": false
}
},
"active": "d73d0bfd1d682067",
"lastOpenFiles": []
}
+6
View File
@@ -8,3 +8,9 @@
Function Call 是什么、LLM 如何学会使用工具:从一个问题入手,
解释模型不执行工具、只输出结构化调用请求的本质,
并以 OpenClaw 源码为例说明工具声明、调用、执行的完整链路。
- [mcp-101.md](./mcp-101.md)
MCP 是什么、为什么 Function Call 之后还需要它:用 LSP 解决编辑器 × 语言
M × N 问题的类比引入,说清 MCP 跟 Function Call 是不同层(模型↔应用
vs 应用↔工具),澄清三个常见误解,并用 OpenClaw 源码示意 MCP server
怎么被发现、何时连接、模型如何看到扁平 tool 列表。
+370
View File
@@ -0,0 +1,370 @@
# MCP 是什么?为什么 Function Call 之后还需要它
> 本文以 [OpenClaw](https://github.com/openclaw/openclaw) 开源源码为例
> (基于 2026.6.2 版本),结合实际代码说明 MCP 的工作原理和定位。
> 涉及源码路径均可在仓库中直接查阅。
如果你看过上一篇 [Function Call 101](./function-call-101.md),应该已经
知道 LLM 怎么"用工具"了:开发者把工具的 JSON Schema 塞进请求,模型决定
调哪个、传什么参数,代码负责真正执行。
那为什么 2024 年 Anthropic 又搞了一个叫 **MCP(Model Context Protocol)**
的东西?它跟 Function Call 是同一个东西吗?还是说 Function Call 不够用?
本文回答这两个问题。
---
## 从一个老问题说起
先看一个跟 AI 没关系的故事。
2016 年微软在做 VSCode 的时候,碰到一个头疼的事:
```
编辑器(M 个) 语言(N 种)
┌────────────┐ ┌──────────┐
│ VSCode │ ↔↔↔↔ │ Python │
│ Vim │ ↔↔↔↔ │ Go │
│ Emacs │ ↔↔↔↔ │ TypeScript│
│ Sublime │ ↔↔↔↔ │ Rust │
│ JetBrains │ ↔↔↔↔ │ Java │
└────────────┘ └──────────┘
```
每个编辑器都要给每种语言写一遍"hover 显示类型"、"跳转到定义"、
"查找所有引用"、"实时报错"...M 个编辑器 × N 种语言 = M × N 份
重复实现。这显然不可持续。
微软的解法是 **LSP(Language Server Protocol)**:把语言能力抽出来
变成一个独立的服务器进程,编辑器跟服务器用一套标准协议通信:
```
┌────────┐ LSP ┌──────────────┐
│ VSCode │ ←──JSON-RPC→│ tsserver │ ← TypeScript
└────────┘ └──────────────┘
┌────────┐ ┌──────────────┐
│ Vim │ ←──────────│ rust-analyzer│ ← Rust
└────────┘ └──────────────┘
```
现在变成 M + N:每个编辑器实现一次 LSP client,每种语言实现一次
LSP server。**`rust-analyzer` 一份能给所有支持 LSP 的编辑器用。**
记住这个故事。
---
## AI 工具集成的同样问题
现在回到 2024 年。当时已经有不少 AI 应用支持 Function Call,但每个
应用都在重复造轮子:
```
AI 应用(M 个) 工具(N 种)
┌────────────────┐ ┌──────────────┐
│ Claude Desktop │ ↔↔↔│ 读文件 │
│ ChatGPT App │ ↔↔↔│ 查数据库 │
│ Cursor │ ↔↔↔│ 调 GitHub API │
│ Codex │ ↔↔↔│ 操作 Notion │
│ 你的 agent │ ↔↔↔│ 控制浏览器 │
└────────────────┘ └──────────────┘
```
每个 AI 应用都要给每种工具写一遍 Function Call 的接入代码:
读文件、调 API、解析响应、错误处理。M × N 又来了。
你想要的是这样:把"读文件"这个能力写一次,Claude Desktop、Cursor、
你的 agent 都能用。
Anthropic 给的答案就是 MCP——**它对 AI 工具集成做的事,跟 LSP 对
编辑器语言支持做的事是一模一样的**。
---
## MCP 是什么
一句话:**MCP 是一个开放协议,让任何 AI 应用都能用一套标准的方式
接入任何外部工具。**
具体来说,MCP 定义了两个角色之间怎么通信:
- **Host**(主机):跑 LLM 的那一侧——Claude Desktop、Cursor、你写的
agent 框架
- **Server**(服务器):提供工具的那一侧——可以是本地子进程
(读文件、控浏览器),也可以是远端 HTTP 服务(Notion 集成、
GitHub 集成)
通信用 JSON-RPC 2.0,跑在 stdio 或 HTTP 之上。MCP server 暴露三类能力:
- **tools**:模型可以调的函数
- **resources**:可读的数据源
- **prompts**:可被引用的预制 prompt 模板
我们今天主要讲 tools。
跟 LSP 长得很像对吧?**stdio + JSON-RPC + 服务器自报能力**——某种意义上
MCP 就是 LSP 思路在 AI 时代的延续。
---
## 但是……Function Call 不是已经能干这件事了吗?
这是新手最容易卡住的地方。我们仔细看看。
### Function Call 是什么层面
Function Call 是**模型和应用之间的约定**:模型怎么"申请"调用一个
函数,应用怎么"执行"它再把结果回传。
```
┌─────────────────────────────┐
│ LLM (Claude / GPT 等) │
│ 输出 tool_use 请求 │ ← Function Call 层
└──────────────┬──────────────┘
┌─────────────────────────────┐
│ 你的应用 │
│ 接到请求,跑函数,回传结果 │
└─────────────────────────────┘
```
这一层只回答了"模型怎么表达调用意图"。**它没规定函数本身是怎么
来的、是谁写的、怎么真正执行**。
### MCP 是什么层面
MCP 是**应用和工具实现之间的协议**:这个函数从哪儿来,代码在哪个
进程里跑,参数怎么传过去。
```
┌─────────────────────────────┐
│ LLM │
│ 输出 tool_use 请求 │ ← Function Call 层(没变)
└──────────────┬──────────────┘
┌─────────────────────────────┐
│ 你的应用 │
│ - 收 tool_use │
│ - 转成 MCP 调用 │
└──────────────┬──────────────┘
│ JSON-RPC over stdio/HTTP
┌─────────────────────────────┐
│ MCP server (子进程/远端) │ ← MCP 层
│ 跑函数,回结果 │
└─────────────────────────────┘
```
注意:**模型完全不知道 MCP 的存在**。它看到的还是一个普通的 tool 定义,
还是按 Function Call 的方式输出 `tool_use`。MCP 是中间那一层应用代码
的事——它把 tool 的"实现"从应用进程里挪到了独立的 MCP server 里。
### 一个类比
- Function Call ≈ "我要打个电话"——这是个抽象动作
- MCP ≈ "电话怎么拨通对方公司的总机,问到分机号,再接到具体的人"
——这是个具体协议
或者:
- Function Call ≈ 函数调用这个概念本身
- MCP ≈ gRPC / OpenAPI——让函数能跨进程跨语言被发现和调用的具体协议
**MCP 不替代 Function Call,它给 Function Call 加了一个"工具供应链"。**
---
## MCP 实际带来了什么
讲了这么多概念,看看实际效果。
### 1. 工具一次写好,所有 AI 应用都能用
`@modelcontextprotocol/server-filesystem` 是社区的一个 MCP server,
让 AI 能读文件、列目录、搜文件。装一次:
```bash
npx @modelcontextprotocol/server-filesystem ~/Documents
```
然后 Claude Desktop、Cursor、OpenClaw 都能直接用上它,不用各自实现
一遍文件操作工具。**这就是 M + N 而不是 M × N。**
### 2. 工具的提供者跟 AI 应用作者解耦
Notion 想给 AI 助手提供集成?以前要等每家 AI 应用主动接入,或者发
SDK 给开发者集成。现在 Notion 自己跑一个 MCP server,任何支持 MCP
的应用都能用。
### 3. 工具能跨语言、跨进程
MCP server 可以是 Python、Go、Rust、任何能讲 JSON-RPC 的语言写的。
你的 AI 应用是 Node.js,但工具实现可以是 Python——只要双方说 MCP 就行。
### 4. 故障隔离
MCP server 跑在独立进程里(或者远端),一个 server 崩了不会拖死整个
AI 应用。可以单独重启、单独限流、单独配置超时。
---
## 看看 OpenClaw 怎么做的
OpenClaw(一个开源 AI agent 框架)的代码可以当作 MCP 的具体落地示例。
### MCP server 怎么"加进来"
OpenClaw 不去网络扫描发现 MCP server,**全靠声明式 config**。两个来源:
**1. 用户写在 `~/.openclaw/openclaw.json` 里:**
```json
{
"mcp": {
"servers": {
"filesystem": {
"command": "npx",
"args": ["@modelcontextprotocol/server-filesystem", "/home/user"]
},
"github": {
"url": "https://mcp.github.com",
"transport": "streamable-http",
"auth": "oauth"
}
}
}
}
```
**2. 插件作者预置:** 装一个 OpenClaw plugin 时,它可能自带几个
MCP server 配置(`src/plugins/bundle-mcp.ts`)。
两个来源在 `src/agents/bundle-mcp-config.ts` 里合并,**用户 config
能覆盖 plugin 默认**。
### 什么时候真的连接 MCP server?
这是个有意思的设计选择。OpenClaw 的策略:**进程启动时不连接、每次
模型轮次开始时连接**。
为什么?因为模型必须在请求里就看到全部 tool 才能决定调哪个,所以
连接不能拖到模型真的调用某个 tool 时再做(那时候 tool 列表都没
组装好)。但又没必要在 OpenClaw 启动时就把所有 MCP server 都拉起来
(可能配了 20 个,这次对话其实只用 2 个)。
折衷点是 **agent 一轮模型请求开始时**:这时候必须把所有 MCP server
都 spawn、`tools/list`、拿到工具目录,然后把工具塞进发给模型的
tool 列表里。
```ts
// 简化版,真实代码在 src/agents/embedded-agent-runner/run/attempt.ts
const mcpRuntime = await materializeBundleMcpToolsForRun({...});
// 这里 MCP server 已经 spawn 完了,tool 也都拿到了
const tools = [...nativeTools, ...mcpRuntime.tools];
// 发给模型
sendRequest({ tools, ... });
```
### 模型看到的是什么
**一个扁平的 tool 数组,跟 native tool 长得完全一样:**
```json
[
{ "name": "Read", "description": "...", "input_schema": {...} },
{ "name": "Bash", "description": "...", "input_schema": {...} },
{ "name": "filesystem__read_file", "description": "...", "input_schema": {...} },
{ "name": "github__list_issues", "description": "...", "input_schema": {...} }
]
```
注意 `filesystem__read_file` 这种 `serverName__toolName` 的命名是为了
防止重名。模型完全不知道哪些来自 MCP、哪些是 OpenClaw 写死的;它只
根据 `description` 和 schema 做语义匹配,挑一个最合适的。
### 模型调用时发生什么
模型返回 `tool_use: filesystem__read_file`,OpenClaw 在 tool 列表里
找到对应条目,它的 `execute` 函数会调 `client.callTool("read_file", input)`,
通过早就建立好的 stdio 连接发给 MCP server,server 跑完返回结果,
OpenClaw 再把结果回传给模型。
整个过程模型一无所知——它只觉得自己"调了个工具,得到了结果"。
---
## 关键澄清:几个常见误解
### 误解 1:"MCP 是给模型用的协议"
**不是。MCP 是给应用用的协议。** 模型那一侧永远是 Function Call,
看到的就是普通 tool 定义。MCP 是应用怎么"获取这些 tool 定义、怎么
真正执行 tool"的事。
### 误解 2:"用了 MCP 就不用 Function Call 了"
**Function Call 永远在用。** MCP server 提供的 tool,最终还是要包装
成 Function Call 的格式发给模型。两者不是 A 替代 B,而是 A 之上又加
了一层 B。
### 误解 3:"native tool 落后了,MCP 才是未来"
**不一定。** 跟应用深度耦合的工具(比如 OpenClaw 的 `Read``Bash`)
做成 native tool 启动快、故障少、能直接用 host 内部能力;MCP 的优势在
**跨应用复用****可独立分发**。两者各有适用场景。
OpenClaw 自己就同时有 native tool、plugin tool、MCP tool,模型看见
的是同一个扁平列表,不区分。
---
## 那 MCP 现在能用来干什么
一些实际的应用方向:
- **本地工具**:文件系统、Git、Docker、SQLite——`@modelcontextprotocol/server-*`
系列已经有不少现成的
- **远端 SaaS 集成**:Notion、Linear、Slack、GitHub——越来越多 SaaS
在做官方 MCP server
- **企业内部工具**:把内部数据库查询、运维脚本包装成 MCP server,
团队所有 AI 助手都能用
- **跨工具协作**:同一个 agent 同时接文件系统 + GitHub + Notion 的 MCP server,
可以读本地代码、查 issue、写文档
---
## 总结
回到开头的问题:**MCP 跟 Function Call 是什么关系?**
```
Function Call:模型 ←→ 应用 之间怎么调函数
MCP: 应用 ←→ 工具 之间怎么提供函数
```
Function Call 解决"模型怎么用工具"。MCP 解决"工具从哪儿来、怎么跨应用
复用"。两者各管一层,组合起来才是完整的故事。
LSP 当年解决了编辑器 × 语言的 M × N 问题,让 `rust-analyzer` 这种
高质量语言服务器能服务所有编辑器。MCP 试图解决 AI 应用 × 工具的同样
问题,让一个 MCP server 能服务所有 AI 助手。
它能不能像 LSP 那样成为事实标准,还要看生态——但思路是清晰的:
**把工具实现从单个应用里抽出来,变成可复用的独立组件**
---
## 想深入了解?
- MCP 规范:<https://modelcontextprotocol.io>
- 看 OpenClaw 怎么实现 MCP client 的:
- 配置和发现机制
- 何时连接、catalog 怎么暴露给模型
- 缓存、失效、安全细节
这部分在 OpenClaw 仓库的 `src/agents/agent-bundle-mcp-*.ts`,
以及 `notes/mcp-client-internals.md`