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
+3 -1
View File
@@ -27,5 +27,7 @@
"file-recovery": true,
"publish": false,
"sync": true,
"webviewer": false
"webviewer": false,
"footnotes": false,
"bases": true
}
+13 -5
View File
@@ -13,12 +13,12 @@
"state": {
"type": "markdown",
"state": {
"file": "blog/function-call-101.md",
"file": "blog/mcp-101.md",
"mode": "source",
"source": false
},
"icon": "lucide-file",
"title": "function-call-101"
"title": "mcp-101"
}
}
]
@@ -142,13 +142,13 @@
"state": {
"type": "outline",
"state": {
"file": "blog/function-call-101.md",
"file": "blog/mcp-101.md",
"followCursor": false,
"showSearch": false,
"searchQuery": ""
},
"icon": "lucide-list",
"title": "function-call-101 的大纲"
"title": "mcp-101 的大纲"
}
}
],
@@ -160,6 +160,7 @@
},
"left-ribbon": {
"hiddenItems": {
"bases:新建数据库": false,
"switcher:打开快速切换": false,
"graph:查看关系图谱": false,
"canvas:新建白板": false,
@@ -170,9 +171,16 @@
},
"active": "d0cf7351ba09fde6",
"lastOpenFiles": [
"llm-autoregressive-kvcache.md",
"rag-overview.md",
"mcp-overview.md",
"mcp-client-internals.md",
"lsp-tools.md",
"blog/mcp-101.md",
"plugin-tools.md",
"blog/function-call-101.md",
"README.md",
"blog/README.md",
"llm-autoregressive-kvcache.md",
"function-call-flow.md",
"blog",
"skills-management-design.md",
+31
View File
@@ -37,4 +37,35 @@
LLM 自回归生成与 KV Cachetoken 逐步生成的机制、prefill vs decode 阶段、
首 token 慢的原因,以及服务端 Prompt Cache 的命中条件与 OpenClaw 中保证顺序确定性的做法。
- [mcp-overview.md](./mcp-overview.md)
MCPModel Context Protocol)概览:协议定位、OpenClaw 作为 MCP server
与 client 的两个方向、MCP 与 tool use 的分层差异(协议层 vs 实现层、
所有权与时机两根独立轴、plugin tool 反例)。
- [mcp-client-internals.md](./mcp-client-internals.md)
OpenClaw 作为 MCP client 的内部实现:server 发现(用户 config + plugin
manifest 合并)、连接时机(三种粒度的"启动"、attempt 引爆点)、
模型视角下的扁平 tool 列表、catalog 缓存与失效、JSON Schema 兼容/
content block 收敛/stdio env 过滤/失败退避等实现细节。
- [plugin-tools.md](./plugin-tools.md)
Plugin tool:OpenClaw 插件给 agent 注册的工具——TypeScript 写、同进程跑、
归插件作者拥有。`defineToolPlugin` 的 execute / factory 两种声明、
manifest-first 发现、Plugin SDK 边界、跟 channel/provider plugin 的关系、
跟 MCP tool 的关键对比,以及它在所有权 × 时机两根轴里"用户拥有 + 编译时确定"那一格的位置。
- [rag-overview.md](./rag-overview.md)
RAG(检索增强生成)概念整理:它解决什么问题、跟 fine-tuning/超长 context/
tool use 的对比、典型离线索引 + 在线检索两阶段流程,重点澄清两个常被混淆的问题
——向量化的结果是什么、拼进 prompt 的到底是向量还是原文(答案:数据库同时存
向量和原文,向量只用来"找",原文才是发给 LLM 的内容);常见坑(检索失败、切块
策略、embedding ≠ 相关性、评估),以及跟 MCP 的关系。
- [lsp-tools.md](./lsp-tools.md)
LSP tool:先科普 Language Server Protocol 是什么(IDE 世界里 M × N 问题
的标准解、跟 MCP 同源同构的 stdio + JSON-RPC 协议),再讲 OpenClaw 怎么
把 hover/definition/references 三个 LSP 方法包装成 agent tool、跟 MCP 和
plugin tool 平行的位置、以及当前只接受 plugin bundle 声明、用户 config
还没接通的现状限制。
完整官方文档:<https://docs.openclaw.ai>
+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`
+224
View File
@@ -0,0 +1,224 @@
# LSP Tool
基于 [OpenClaw](https://github.com/openclaw/openclaw) 2026.6.2 版本源码。
本文先讲 **LSP(Language Server Protocol)是什么**——它是 IDE 世界里
一个很重要的协议,但跟 AI 领域关系不大,所以单独介绍——再讲 OpenClaw
怎么把它包装成 agent 的 tool。
## LSP 是什么
### 它解决什么问题
写编辑器的人原来面临一个 M × N 问题:
```
编辑器 (M 个) 语言支持 (N 种)
┌────────────┐ ┌──────────┐
│ VSCode │ ↔↔↔↔ │ Python │
│ Vim │ ↔↔↔↔ │ Go │
│ Emacs │ ↔↔↔↔ │ TypeScript│
│ Sublime │ ↔↔↔↔ │ Rust │
│ JetBrains │ ↔↔↔↔ │ Java │
└────────────┘ └──────────┘
```
每个编辑器都得给每种语言写一遍 "hover 显示类型"、"跳转到定义"、
"查找所有引用"、"实时报错"...这些功能。M 个编辑器 × N 种语言 =
M × N 份重复实现。
LSP 是微软 2016 年提出的解法:**把"语言能力"抽出来变成一个独立的
服务器进程**,编辑器和服务器之间用一套标准协议通信。
```
编辑器 Language Server
┌────────┐ LSP ┌──────────────┐
│ VSCode │ ←─────JSON-RPC→│ tsserver │ ← TypeScript
└────────┘ └──────────────┘
┌────────┐ ┌──────────────┐
│ Vim │ ←──────────────│ gopls │ ← Go
└────────┘ └──────────────┘
┌────────┐ ┌──────────────┐
│ Emacs │ ←──────────────│ rust-analyzer│ ← Rust
└────────┘ └──────────────┘
```
现在变成 M + N:每个编辑器实现一次 LSP client,每种语言实现一次
LSP server。VSCode 装 Rust 插件 = 它在 VSCode 端走 LSP client,
背后跑一个 `rust-analyzer` 进程。
### 协议形态
LSP 用 JSON-RPC 2.0,跑在 stdio 之上(也支持其他 transport,但 stdio
是默认)。消息分帧用 HTTP 风格的 Content-Length header:
```
Content-Length: 213\r\n
\r\n
{"jsonrpc":"2.0","id":1,"method":"textDocument/hover","params":{...}}
```
典型方法名:
- `initialize` — 握手,client 告诉 server 自己支持什么,
server 回 capabilities(支不支持 hover、definition 等)
- `textDocument/didOpen` / `didChange` / `didClose` — 通知文件状态
- `textDocument/hover` — 给我光标位置的类型/文档
- `textDocument/definition` — 跳转到定义
- `textDocument/references` — 找所有引用
- `textDocument/completion` — 自动补全
- `textDocument/publishDiagnostics` — server 主动推:这里有错
每个方法的参数和返回值都在 LSP 规范里固定下来,所以一个 LSP server
可以同时被 VSCode、Vim、Emacs 用。
### 跟 MCP 的对照
| 维度 | LSP | MCP |
| ------------- | -------------------------------- | ------------------------------------ |
| 目标受众 | IDE / 编辑器 | LLM 应用 |
| 服务器干啥 | 提供代码分析(hover/跳转/补全) | 提供任意工具(读文件/查搜索/...) |
| 方法集合 | 固定一套 `textDocument/*` 等 | server 自己声明 `tools/list` |
| 协议层 | JSON-RPC 2.0 + Content-Length | JSON-RPC 2.0 |
| 提出者 | 微软 | Anthropic |
| 年份 | 2016 | 2024 |
两个协议**长得很像**(stdio + JSON-RPC),解决的是不同领域的同一种
M × N 问题——把可复用的能力(代码分析 / agent 工具)抽到独立进程。
某种意义上 MCP 是 LSP 思路在 AI 时代的延续。
## OpenClaw 怎么用 LSP
核心实现 `src/agents/agent-bundle-lsp-runtime.ts`,第一行注释:
> Minimal LSP JSON-RPC framing over stdio (Content-Length header + JSON body).
不是用现成的 LSP client 库,而是自己在 OpenClaw 里实现了 LSP 协议的
最小子集(Content-Length 分帧 + JSON-RPC),spawn 语言服务器子进程,
`initialize` 握手拿能力。
### 暴露的 tool
根据语言服务器 `initialize` 返回的 capabilities 动态生成
(`agent-bundle-lsp-runtime.ts:317` `buildLspTools`):
| Tool 名 | LSP 方法 | 用途 | capability 门槛 |
| -------------------------- | ------------------------- | ----------------- | --------------------- |
| `lsp_hover_<server>` | `textDocument/hover` | 看符号的类型/文档 | `hoverProvider` |
| `lsp_definition_<server>` | `textDocument/definition` | 跳到定义 | `definitionProvider` |
| `lsp_references_<server>` | `textDocument/references` | 找所有引用 | `referencesProvider` |
如果语言服务器没声明对应 capability(比如 `hoverProvider: false`),
对应 tool 就不会被注册。
工具参数都长一样(`uri: string, line: number, character: number`),
因为 LSP 协议本身就是这套 `textDocument + position` 的形态。
**注意没有 completion**:agent 不需要交互式补全,它直接看完整文件就行。
OpenClaw 只挑了"看完整代码不能直接得到的语义信息"做工具——hover 拿
推断类型,definition/references 需要跨文件 AST 跳跃。
### 跟 MCP / plugin 平行
| 维度 | LSP tool | MCP tool | plugin tool |
| -------------- | ----------------------- | --------------------------- | --------------------------- |
| 协议 | LSP(`textDocument/*`) | MCP(`tools/call`) | OpenClaw Plugin SDK |
| 进程 | stdio 子进程 | stdio 子进程 / HTTP | 同进程 |
| Tool 名 | hardcode 三个 + server 后缀 | server 自报 `tools/list` | plugin 代码静态声明 |
| 配置来源 | bundle plugin manifest | bundle plugin + 用户 config | plugin manifest |
| 用户能加吗 | 现在不能 | 能(`openclaw mcp add`) | 装新 plugin 即可 |
| Catalog 动态性 | **静态**(三个固定 tool) | **动态**(server 决定) | 静态 |
| 模型视角 | 跟其他 tool 没区别 | 跟其他 tool 没区别 | 跟其他 tool 没区别 |
跟 MCP 最大不同:**LSP 的 tool 集是 OpenClaw 写死的**(hover/definition
/references 三个),只是是否启用看 server capability。MCP 是 server
想暴露啥就 `tools/list` 出啥,完全运行时决定。
这是 LSP 协议本身决定的——LSP 的方法集就那么固定一套,不像 MCP
让 server 自由声明工具。OpenClaw 只是把 LSP 方法**翻译**成 agent
理解的 tool。
### 在 attempt 时序里
跟 MCP 完全平行(回看 [mcp-client-internals.md](./mcp-client-internals.md)
的 "Attempt 引爆点"):
```ts
// src/agents/embedded-agent-runner/run/attempt.ts:1502-1549
const bundleMcpRuntime = bundleMcpSessionRuntime
? await materializeBundleMcpToolsForRun({...})
: undefined;
bundleLspRuntime = bundleLspEnabled
? await createBundleLspToolRuntime({...}) // ← LSP 在这里 spawn + initialize
: undefined;
const allowedBundleMcpTools = applyEmbeddedAttemptToolsAllow(bundleMcpRuntime?.tools ?? [], ...);
const allowedBundleLspTools = applyEmbeddedAttemptToolsAllow(bundleLspRuntime?.tools ?? [], ...);
const allowedBundledTools = [...allowedBundleMcpTools, ...allowedBundleLspTools];
```
每次 attempt 开始时,两条 runtime 各自独立 spawn(MCP / LSP),
各自维护 session,各自 dispose;最后合到同一个扁平 tool list 给模型。
## 典型用例
模型要回答 "把 `runEmbeddedAttempt` 的所有调用方都列出来"。可能的工具组合:
1. `Grep "runEmbeddedAttempt"` ——快,但会被注释、字符串、相似名字误伤
2. `lsp_references_typescript` ——准,基于 AST 分析,但要先定位符号的
精确 `(uri, line, character)`
3. 常见组合:先 `Grep` 找出大致位置 → `Read` 确认 → `lsp_references_typescript`
拿精确引用列表
LSP tool 的价值在于 **精确度**——code intelligence 这件事 grep 替代不了:
- `Grep` 找的是文本匹配,会把字符串字面量、注释里的同名词都算进来
- `lsp_references` 走 AST + 类型系统,能区分 `foo.runEmbeddedAttempt`
调的是哪个 `foo`,跨 import 跳得对
类似地 `lsp_hover` 拿 inferred type 是 grep / read 替代不了的——
TypeScript 的 `const x = foo()` 你想知道 `x` 是啥类型,只有 tsserver
能告诉你。
## 配置怎么写
跟 MCP server 一样,LSP server 从 plugin bundle manifest 读
(`src/plugins/bundle-lsp.ts``src/agents/embedded-agent-lsp.ts`),
plugin 在自己的 manifest 里声明 `lspServers`,字段形态跟 MCP 的
stdio server 几乎一样(`command``args``env``cwd`)。
OpenClaw 实际上**复用了 MCP 的 stdio launch config 结构**——
`agent-bundle-lsp-runtime.ts` 里直接 import 了 `resolveStdioMcpServerLaunchConfig`
`describeStdioMcpServerLaunchConfig`,因为两者都是 "spawn 一个本地
stdio 子进程跑 JSON-RPC",launch 部分可以共享。
## 现状的限制
`src/agents/embedded-agent-lsp.ts:22` 有一条注释:
> User-configured LSP servers could override bundle defaults here in the future.
意思是**当前只接受 plugin bundle 声明的 LSP server**,用户 config 还
没接通。MCP 已经走得更远(用户 config 可以加自己的 server),LSP 还
停在 plugin 预置的阶段。
退避逻辑也比 MCP 简化——没有 "连续 3 次失败冷却 60s" 这种保护,
失败就直接报错给 agent。
## 心智模型一句话
LSP 是把 "编辑器智能" 抽出来的协议,跟 MCP 是把 "agent 工具" 抽出来的
协议同源同构(stdio + JSON-RPC + 服务器自报能力)。OpenClaw 借了 LSP
这套现成的代码分析基础设施,把 hover/definition/references 三个最有
agent 价值的能力包装成 tool,放进 agent 的扁平 tool 列表里。模型层面
跟其他 tool 没任何区别。
## 相关
- [mcp-overview.md](./mcp-overview.md) — MCP 协议本身
- [mcp-client-internals.md](./mcp-client-internals.md) — MCP client
的内部实现,LSP 在 attempt 时序里跟它平行
- [plugin-tools.md](./plugin-tools.md) — plugin tool,LSP server 的
配置来源就是 plugin bundle
+297
View File
@@ -0,0 +1,297 @@
# OpenClaw 作为 MCP client:内部实现
基于 [OpenClaw](https://github.com/openclaw/openclaw) 2026.6.2 版本源码。
本文专讲 OpenClaw 作为 MCP client 那一侧的实现:server 怎么被发现、
何时连接、tool catalog 怎么暴露给模型、怎么缓存与失效、有哪些值得注意的
安全/兼容细节。
MCP 是什么、和 tool use 的关系、`openclaw mcp serve` 那一侧的实现见
[mcp-overview.md](./mcp-overview.md)。
## 整体生命周期
```
[发现] 读 config + plugin manifest → 一份 server map
(进程启动 / session 创建时纯 JSON 读,不 spawn)
[连接] attempt 开始触发 getCatalog
→ resolveMcpTransport
→ connectWithTimeout (stdio spawn / http 握手)
→ client.listTools()
[暴露] buildBundleMcpToolsFromCatalog
→ MCP tool 包装成 AnyAgentTool(name 加 server 前缀)
→ 跟 native / plugin / LSP tool 合到同一个数组
→ 发给 LLM
[调用] 模型返回 tool_use → 找到 AnyAgentTool
→ execute → client.callTool()
→ 走早就建立好的 transport
[缓存与失效] 同 session 的后续 attempt 复用 catalog;失效条件:
idle TTL / listChanged / 连续失败冷却
```
## MCP server 的发现
简短答案:**没有"扫描发现",全靠声明式 config**。OpenClaw 从两个来源
合并出 MCP server 列表。
### 两个来源
**1. 用户 config:`openclaw.json` 的 `mcp.servers`**
类型定义在 `src/config/types.mcp.ts`:
```ts
export type McpConfig = {
servers?: Record<string, McpServerConfig>;
sessionIdleTtlMs?: number;
};
```
用户通过 CLI(`openclaw mcp add/set/configure/...`)或直接编辑文件,
`~/.openclaw/openclaw.json` 里写入条目。CLI 实现在
`src/cli/mcp-cli.ts`,读写在 `src/config/mcp-config.ts`
**2. 已启用的 bundled plugin 自带的 MCP 定义**
每个 plugin 在自己的 manifest 里可以声明附带的 MCP server,加载逻辑
`src/plugins/bundle-mcp.ts`:
- `loadEnabledBundleMcpConfig` 遍历 `manifestRegistry` 里所有
**enabled** 的 plugin
- 对每个 plugin 调 `loadBundleMcpConfig`:
1. 读 plugin 的 manifest(`claude` / `codex` / `cursor` 三种格式之一)
2. 解析其中 `mcpServers` 字段声明的相对路径列表
3. 如果 plugin 根目录存在 `.mcp.json` 也自动加入
4. 把这些文件里的 server 定义和 manifest 里的 inline `mcpServers`
merge 起来
- 路径里的 `${CLAUDE_PLUGIN_ROOT}` 占位符会被替换成 plugin 根目录
### 合并优先级
两个来源在 `src/agents/bundle-mcp-config.ts``loadMergedBundleMcpConfig`
里合并:
```ts
mcpServers: {
...enabledBundleMcp, // plugin 自带的(底)
...enabledConfiguredMcp, // 用户 config 的(覆盖在上)
}
```
规则:
1. **同名 server**,用户 config 覆盖 plugin 默认(注释里明确说
"OpenClaw config 是 owner-managed layer")
2. 用户 config 里 `enabled: false` 不仅排除自己,还能**屏蔽掉
同名的 bundled server**(`disabledConfiguredNames` 集合)
3. 一次性的 embedded agent run 通过 `loadEmbeddedAgentMcpConfig`
(`src/agents/embedded-agent-mcp.ts`)拿到最终 merged 结果
### 发现链路
```
启动 / 每次 session
├─ getRuntimeConfig() ← 读 ~/.openclaw/openclaw.json
│ └─ cfg.mcp.servers ← 用户层
├─ PluginManifestRegistry.plugins ← 已发现的 plugin manifest
│ └─ 每个 plugin 的 .mcp.json /
│ manifest mcpServers ← plugin 层
└─ loadMergedBundleMcpConfig({ cfg, manifestRegistry, workspaceDir })
{ mcpServers: { name → config } } ← 最终 server 列表(还没 spawn)
```
### 发现性质
1. **完全声明式**:OpenClaw 不去网络扫描、不去 ServiceDiscovery、
不去 mDNS——所有 server 必须显式写在某个 config 文件里
2. **plugin 是隐式来源**:用户装个 plugin,可能就"被"得到几个 MCP
server,这是 plugin 作者预置的
3. **Config 是 process-stable**:根 `AGENTS.md` 的架构约定——
MCP server 列表在进程启动后视为稳定,改了 config 要
`openclaw mcp reload` 或重启 agent
4. **server 列表的"发现"和 tool 的"发现"是两层**:
- server 列表:**静态**(读 config + plugin manifest)
- 每个 server 的 tool:**动态**(运行时 `tools/list`,还支持
`listChanged` 通知)
所以严格说,OpenClaw 作为 MCP client 对 **server** 是静态发现,
**tool** 是动态发现。
## 何时连接
关键澄清:**"懒"是相对"OpenClaw 启动"懒,不是相对"模型调用"懒**。
每次 agent attempt(模型一轮请求)开始时,MCP server 会**先全部
spawn + listTools**,然后 tool catalog 才被塞进发给模型的 tool 列表里。
### 三种粒度的"启动"
| 粒度 | 何时发生 | 状态 |
| ----------------------------- | ----------------------- | ----------------------------------------------------- |
| OpenClaw 进程启动 | `openclaw` 命令开始跑 | **不连接任何 MCP** |
| Session 创建 | 用户第一次说话 | **不连接**(只准备 runtime 壳子) |
| Attempt 开始(每轮模型请求) | 准备给模型发请求 | **必须连接 + listTools**(否则模型不知道有哪些 tool) |
| Tool 实际调用 | 模型返回 tool_use | **走已建立的连接** |
"懒到模型调用时再连接"是不行的——模型必须在请求里就看到全部 tool
才能决定调哪个。OpenClaw 的折衷点在 "attempt 开始" 这一步:对每个会话,
只在它真有 attempt 时才连(避免无对话的会话开销),但一旦要 attempt,
必须先把 catalog 凑齐。
### Attempt 引爆点
`src/agents/embedded-agent-runner/run/attempt.ts:1502` 这段是关键——
**模型还没被调用前**,这段已经把 MCP 全部 spawn 完了:
```ts
const bundleMcpSessionRuntime = bundleMcpEnabled
? await getOrCreateSessionMcpRuntime({...}) // 拿 runtime(可能复用)
: undefined;
bundleMcpRuntime = bundleMcpSessionRuntime
? await materializeBundleMcpToolsForRun({ // ← 这里触发 getCatalog
runtime: bundleMcpSessionRuntime,
reservedToolNames: [...],
})
: undefined;
```
紧接着 `attempt.ts:1535` 把 MCP tool 合到 `allowedBundledTools`,后面跟
native tool 一起发给模型。
### 完整时间线
```
进程启动 → 读 config,记下"理论上有哪些 server",不 spawn
session 创建 → runtime 壳子,sessions=空,catalog=null
attempt 开始 → getCatalog():
├─ for 每个 server:
│ - resolveMcpTransport
│ - connectWithTimeout (stdio spawn / http 握手)
│ - client.listTools()
└─ 拼成 catalog
buildBundleMcpToolsFromCatalog():
把每个 MCP tool 包装成 AnyAgentTool
(name = "serverName__toolName")
组装 LLM 请求 → tools = [...native, ...plugin, ...mcp]
LLM 收到请求 → 看到 N 个 function 定义,只看 name/description/schema
LLM 返回 → tool_use: "filesystem__read_file"
执行 → 在 tool 列表里找到对应 AnyAgentTool,
调它的 execute → runtime.callTool() → 已建立的 transport
```
## 模型视角:一个扁平的 tool 列表
**模型不知道"MCP"这个概念**。它只看到一个 tool 数组,每个 tool 形如:
```json
{
"name": "filesystem__read_file",
"description": "Read contents of a file. Returns text content...",
"input_schema": { "type": "object", "properties": { "path": {...} } }
}
```
`Read``Bash` 这种 native tool 在请求里**完全是同一形状**。区别仅在:
- **来源不同**:native 是代码里写的,MCP 是从 `client.listTools()` 拉来的
- **名字有前缀**:`buildSafeToolName` 给 MCP tool 加 `serverName__` 前缀
避免重名,native tool 没这个前缀
- **execute 走向不同**:native 在本进程跑,MCP 走 `client.callTool()`
到子进程/HTTP
`attempt.ts:1535-1549` 那段把所有来源 concat 起来:
```ts
const allowedBundleMcpTools = applyEmbeddedAttemptToolsAllow(
bundleMcpRuntime?.tools ?? [], ...
);
const allowedBundleLspTools = applyEmbeddedAttemptToolsAllow(
bundleLspRuntime?.tools ?? [], ...
);
const allowedBundledTools = [...allowedBundleMcpTools, ...allowedBundleLspTools];
```
native tool、plugin tool、MCP tool、LSP tool 全部以 `AnyAgentTool[]`
形式 concat,经过 provider adapter(Anthropic / OpenAI / Google)翻译
成各自的 tool schema,塞进同一个请求的 `tools` 字段。
模型挑哪个工具是**纯粹基于 `description` 和 schema 跟用户意图的语义
匹配**——跟它怎么挑 `Read` 还是 `Bash` 同一个机制。
### 扁平化的几个微妙后果
1. **模型可能"搞错来源"**——装了一个 `memory__write` 的 MCP 工具,
模型在该用 native `Write` 写文件时可能调了 `memory__write`,只因
后者 description 更像当下意图。这就是为啥 MCP server 的 `description`
写得好不好直接影响 agent 效果。
2. **名字前缀是唯一的"出身标签"**——`buildSafeToolName``serverName__`
前缀(`agent-bundle-mcp-materialize.ts`),既防重名,也无意中给
模型一点"分组信号"。但模型不会被告知 `__` 前面是 server 名,它只是
把整个名字当字符串看。
3. **工具排序影响行为**——`agent-bundle-mcp-materialize.ts` 显式
`tools.sort(...)` 是为了让同一份 catalog 在不同 turn 里给模型呈现的
顺序一致(对 prompt cache 命中重要,见根 `AGENTS.md`
"deterministic ordering" 那条)。不排序时顺序漂移会让 prompt
字节级别变化,KV cache 失效。
4. **tool filter 是预算工具**——`toolFilter.include/exclude` 在 catalog
阶段就过滤掉,模型完全感知不到"被屏蔽"的工具存在。这是抑制模型
乱调 MCP 工具的主要手段。
一句话:**对 LLM 来说,世界是扁平的一组 function;MCP 是 OpenClaw 这
一层的实现细节,跨过 provider boundary 之后就消失了。**
## Catalog 缓存与失效
`agent-bundle-mcp-runtime.ts``getCatalog` 有缓存——同一个 session 的
后续 attempt **不再重新 listTools**,直接复用 catalog。
失效条件:
- session 空闲超过 `mcp.sessionIdleTtlMs`(默认 10 分钟)被回收,
下次 attempt 时 catalog 重新拼
- MCP server 发了 `listChanged` 通知,`onChanged` callback 里
`catalog = null` 强制下次刷新
- 某个 server 连续 3 次失败,进入 60s 冷却期(后面 "Failure 退避" 段)
这就是"server 列表静态发现 + tool 列表动态发现 + tool catalog 缓存"
三层的实际落地。
## 几个值得注意的实现细节
**JSON Schema 兼容**(`agent-bundle-mcp-runtime.ts` `createBundleMcpJsonSchemaValidator`):
MCP 用 JSON Schema draft 2020-12,而 TypeBox/Ajv 对一些 keyword 支持
不一致,OpenClaw 用 `stripJsonSchemaFormats` + `normalizeJsonSchemaForTypeBox`
做 schema 归一化后再交给 TypeBox 编译。
**Content block 收敛**(`agent-bundle-mcp-materialize.ts` `mcpContentBlockToToolResult`):
MCP 的 `CallToolResult` 可以返回 text/image/audio/resource_link/resource 等
多种 block,但 OpenClaw 的 `AgentToolResult` 只支持 text/image。多余类型
被降级成 text,避免 provider(Anthropic)拒掉无效 image block 后整个
session 历史被污染(注释里点了 issue #90710)。
**Stdio env 安全过滤**(`docs/cli/mcp.md` "Stdio env safety filter"):
`NODE_OPTIONS``PYTHONSTARTUP``LD_PRELOAD` 这类能在 RPC 之前改变
解释器行为的 env 被禁止写入 stdio server 的 `env` 字段。
**Failure 退避**(`agent-bundle-mcp-runtime.ts` 常量 `BUNDLE_MCP_FAILURE_THRESHOLD`):
同一 server 连续 3 次失败会触发 60s 冷却,防止一个坏 server 阻塞整个
agent turn。
**Tool filter**:每个 server 可配 `toolFilter.include/exclude`(支持 `*` glob),
catalog 阶段就把不想暴露给模型的工具过滤掉。
## 相关
- [mcp-overview.md](./mcp-overview.md) — MCP 协议本身、OpenClaw 的两个
方向、MCP 与 tool use 的对比
- `docs/cli/mcp.md` — 官方面向用户的 CLI 文档
+241
View File
@@ -0,0 +1,241 @@
# MCPModel Context Protocol)概览
基于 [OpenClaw](https://github.com/openclaw/openclaw) 2026.6.2 版本源码。
本文讲 MCP 是什么、OpenClaw 怎么用它、它和 tool use 是什么关系。
OpenClaw 作为 MCP client 的内部实现(发现、连接、catalog、缓存等)
单独写在 [mcp-client-internals.md](./mcp-client-internals.md)。
## MCP 是什么
MCP 是一个开放协议,定义了 **LLM 应用(host)与外部工具/数据源(server)
之间的通信格式**。可以理解为"AI 应用的 USB-C 接口":一个 host 通过 MCP
可以同时接入文件系统、数据库、第三方 API 等任意 server,而不需要为每个
工具单独写 adapter。
通信用 JSON-RPC 2.0,MCP server 暴露三类能力:
- **tools** — 模型可以调用的函数(`tools/list``tools/call`)
- **resources** — 可读的数据源(`resources/list``resources/read`)
- **prompts** — 可被引用的预制 prompt 模板
## OpenClaw 里 MCP 的两个方向
OpenClaw 同时扮演 MCP server 和 MCP client(见 `docs/cli/mcp.md`):
```
┌──────────────────────┐
│ External MCP client │
│ (Codex / Claude Code)│
└─────────┬────────────┘
│ stdio JSON-RPC
┌──────────────────────────────────────┐
│ openclaw mcp serve │
│ (src/mcp/channel-server.ts) │ ← OpenClaw as MCP server
│ ChannelBridge → Gateway WS │
└──────────────────────────────────────┘
┌──────────────────────────────────────┐
│ OpenClaw agent runtime │
│ (src/agents/agent-bundle-mcp-*.ts) │ ← OpenClaw as MCP client
└─────────┬────────────────────────────┘
│ stdio / SSE / streamable-http
┌─────────▼──────────┐ ┌──────────────┐
│ third-party MCP │ │ remote HTTP │
│ server (filesystem,│ │ MCP server │
│ memory, ...) │ │ (OAuth) │
└────────────────────┘ └──────────────┘
```
### 方向 1:OpenClaw 当 server(`openclaw mcp serve`)
把 OpenClaw 的 channel 会话(Telegram/Discord/iMessage 等)通过 MCP
暴露给外部 client(Codex、Claude Code 等)。
入口在 `src/cli/mcp-cli.ts`,核心装配在 `src/mcp/channel-server.ts`:
```
McpServer(@modelcontextprotocol/sdk)
+ OpenClawChannelBridge (连到本地/远端 Gateway WebSocket)
+ registerChannelMcpTools(server, bridge)
```
`src/mcp/channel-tools.ts` 注册的 tool:
- `conversations_list` / `conversation_get`
- `messages_read` / `attachments_fetch`
- `events_poll` / `events_wait`
- `messages_send`
- `permissions_list_open` / `permissions_respond`
传输是 stdio:外部 client 启动 `openclaw mcp serve` 子进程,两边通过
stdin/stdout 跑 JSON-RPC。OpenClaw 内部再用 WebSocket 连到 Gateway 取
真实会话。
### 方向 2:OpenClaw 当 client
`openclaw mcp add/set/configure/probe/login/...` 把第三方 MCP server
保存到 `openclaw.json``mcp.servers` 下;OpenClaw 的 agent runtime
在跑的时候把这些 server spawn 起来,并把它们的 tool 合到模型可调用的
工具列表里。
这一侧的详细实现(server 怎么被发现、何时连接、catalog 怎么暴露给模型、
怎么缓存与失效、几个安全/兼容细节)单独写在
[mcp-client-internals.md](./mcp-client-internals.md)。
## MCP 与 tool use 的区别
这两个经常被混在一起,其实是 **不同层的概念**
一句话:
- **Tool use(function calling)**:模型 ↔ 应用之间的**约定**——
模型怎么"申请调用一个函数"
- **MCP**:应用 ↔ 工具实现之间的**协议**——
这个函数从哪儿来、怎么真正执行
模型不知道 MCP 的存在,MCP server 也不知道是哪个模型在调它。
### 分层看
```
┌─────────────────────────────────────────┐
│ LLM (Claude / GPT / Gemini) │
│ 只认 tool_use / function_call │ ← Tool Use 层
└─────────────────────┬───────────────────┘
│ provider API
┌─────────────────────▼───────────────────┐
│ OpenClaw agent runtime │
│ - 收集所有可用 tool │
│ - 打平成 provider 期望的格式 │
│ - 收到 tool_use 后分发执行 │
└──┬───────────────┬──────────────┬───────┘
│ │ │
┌──▼──────┐ ┌─────▼─────┐ ┌─────▼──────┐
│ native │ │ plugin │ │ MCP server │ ← Tool 来源
│ Read/ │ │ tool │ │ (stdio/HTTP)│
│ Bash... │ │ │ │ │
└─────────┘ └───────────┘ └─────────────┘
```
模型那一层永远是 tool use;MCP 只是 OpenClaw 拿到工具实现的一种方式。
所有 tool 最后都被归一化成同一个类型 `AnyAgentTool`
(`src/agents/tools/common.ts`):
- **Native tool**:代码里直接定义,例如 `src/agents/tools/read.ts`
- **MCP tool**:`buildBundleMcpToolsFromCatalog``Client.listTools()`
返回的远端 catalog 包装成 `AnyAgentTool`,`execute` 里调
`client.callTool(...)`
### 关键差异表
| 维度 | Tool use(原生 function calling) | MCP |
| ------------- | -------------------------------- | ---------------------------------------------- |
| 谁定义 | 应用代码内联定义 schema | 远端 MCP server 自己声明 |
| 谁执行 | 应用代码同进程里调用 | 远端 server(子进程 stdio,或 HTTP) |
| 发现方式 | 编译时静态注册 | 运行时 `tools/list` 动态拉取 |
| 跨语言 | 绑死宿主语言 | JSON-RPC 任何语言都能写 server |
| 跨应用复用 | 每个应用各写一遍 | 同一个 server 给 Claude Code、Codex、OpenClaw 都用 |
| 协议层 | 没有协议,就是个函数 | JSON-RPC 2.0 + 标准 schema |
| 能力 | 只有 function | tools / resources / prompts / notifications |
| 生命周期 | 跟着进程 | 独立进程,需要管 spawn/health/timeout/idle reclaim |
### 类比
- **Tool use** ≈ 编程语言里"函数调用"这个概念本身
- **MCP** ≈ gRPC / OpenAPI ——一种让函数能跨进程/跨语言被发现和调用的具体协议
或者:
- Tool use ≈ "我要打个电话"
- MCP ≈ "电话怎么拨通对方公司的总机,问到分机号,再接到具体的人"
### 实际后果
1. **不用 MCP 也能 tool use**:OpenClaw 的 `Read``Bash``Edit` 这些都是
原生 tool,模型直接调,没 MCP 什么事。
2. **同一个 MCP server 给多个 host 复用**:`@modelcontextprotocol/server-filesystem`
一次装好,Claude Code 和 OpenClaw 都能用;换成原生 tool 就得在每个
host 里各实现一遍。
3. **MCP server 挂掉不影响其他 tool**:远端调用有网络/进程/超时故障模式,
OpenClaw 用失败退避隔离(见 [mcp-client-internals.md](./mcp-client-internals.md)
的 "几个实现细节")。
4. **MCP tool 对模型完全透明**:模型看到的就是 `filesystem__read_file(path)`,
跟 native tool 长得一样,`__` 前缀只是为了避免重名,不是给模型暗示
这是 MCP。
### 编译时 vs 运行时:典型用法不是协议本质
常见直觉是 "native tool use 是编译时静态、开发者固定;MCP 是运行时
动态、用户自定义"。方向对,但这是 **典型用法**,不是 **协议本质**
实践中的分布确实如此:
| 维度 | Native tool use | MCP tool |
| ----------- | ------------------- | ---------------------------------- |
| 谁定义 | 开发者写在代码里 | 用户在 config 里加 |
| 何时确定 | 构建时 / 启动时 | 运行时 `tools/list` 拉来 |
| 变更成本 | 改代码、重发版 | 改 `openclaw.json` + `mcp reload` |
| 列表稳定性 | 一个版本里固定 | session 内可变(MCP `listChanged`)|
但严格说要分两层:
- **Tool use(协议层)** 没规定 tool 必须是静态的。应用完全可以每次请求
前动态生成 tool 列表(按用户权限、session 状态等)塞给模型。模型只看到
一个数组,不在乎是 hardcode 还是即时拼的。
- **MCP** 协议本身**强制动态**:server 自己声明 `tools/list`,host 必须
运行时去问——因为 tool 在另一个进程里,编译时无从知晓。
所以更准的说法:
- Tool use **允许**静态也**允许**动态,大多数应用选静态(因为简单)
- MCP **强制**动态(协议天然属性)
### 更本质的轴:所有权
真正的分水岭是 **谁拥有 tool 实现**:
- **Native tool** = 应用拥有 → 自然静态、自然由开发者掌控
- **MCP tool** = 第三方/用户拥有 → 必须动态发现、自然由用户配置
"编译时 vs 运行时"是这个所有权差异的**自然后果**,不是原因。
### 一个反例:plugin tool
OpenClaw 的 **plugin tool**(`extensions/*` 里那些)既不是 native 也不是
MCP,正好用来校准这两根轴:
- 像 native 一样:用 TypeScript 写,跟主进程同语言,**编译时**就在
- 像 MCP 一样:不在 core 里,由"插件作者"(广义的用户/第三方)拥有
它们走 `src/plugin-sdk/*`,跟 native tool 一样进 `AnyAgentTool`,跟 MCP tool
一起被打平。
这说明 **"动态/静态" 和 "开发者/用户" 是两个独立轴**,plugin tool 占了
"用户拥有 + 编译时确定" 那一格:
```
开发者拥有 用户/第三方拥有
┌──────────────────┬──────────────────┐
编译时确定 │ native tool │ plugin tool │
├──────────────────┼──────────────────┤
运行时发现 │ (动态生成的 │ MCP tool │
│ native tool) │ │
└──────────────────┴──────────────────┘
```
## 心智模型一句话
Tool use 是模型层的调用约定;native / plugin / MCP 是这个约定下
**tool 实现的几种供应来源**,一部分由应用开发者编译时固化,一部分由
用户或第三方运行时供应。MCP 不替代 tool use,它给 tool use 加了一个
"工具供应链":模型继续按 tool use 协议出请求,应用按 MCP 协议去远端
把工具 *供应* 回来,放到模型看到的 tool 列表里。
## 继续阅读
- [mcp-client-internals.md](./mcp-client-internals.md) — OpenClaw 作为
MCP client 的内部实现:server 发现、连接时机、catalog 暴露、缓存与
失效、安全/兼容细节
+226
View File
@@ -0,0 +1,226 @@
# Plugin Tool
基于 [OpenClaw](https://github.com/openclaw/openclaw) 2026.6.2 版本源码。
本文讲 OpenClaw 的 **plugin tool**:跟 native tool、MCP tool 同列,
但归插件作者拥有、用 TypeScript 写、跑在 OpenClaw 同进程里的那一类工具。
## 定位
OpenClaw agent 看见的工具一共有三种来源(详见
[mcp-overview.md](./mcp-overview.md) 的 "MCP 与 tool use 的区别"):
| | 拥有者 | 在哪儿 | 进程 | 协议 |
| ------------- | ----------------- | -------------------------------- | -------------- | -------------------------- |
| **native tool** | OpenClaw core | `src/agents/tools/*` | 同进程 | 无 |
| **plugin tool** | 插件作者 | `extensions/<name>/`(可第三方) | 同进程 | OpenClaw Plugin SDK |
| **MCP tool** | 第三方/用户 | 任何 MCP server | 子进程/远端 | MCP JSON-RPC |
plugin tool 占的格子是:**用 TypeScript 写、走 OpenClaw 内部 SDK 接口、
不需要跑子进程**。介于 "core 写死的 native" 和 "完全解耦的 MCP" 之间,
专门用来做"深度集成 OpenClaw 内部能力,但又不属于 core" 的工具。
## 怎么写一个
入口 `src/plugin-sdk/tool-plugin.ts``defineToolPlugin`。最小例子
(`extensions/llm-task/index.ts`):
```ts
import { defineToolPlugin } from "openclaw/plugin-sdk/tool-plugin";
import { Type } from "typebox";
export default defineToolPlugin({
id: "llm-task",
name: "LLM Task",
description: "Generic JSON-only LLM tool for structured tasks.",
configSchema: Type.Object({
defaultProvider: Type.Optional(Type.String()),
defaultModel: Type.Optional(Type.String()),
// ...
}),
tools: (tool) => [
tool({
name: "llm-task",
description: "...",
parameters: Type.Object({ // TypeBox schema
prompt: Type.String(),
input: Type.Optional(Type.Unknown()),
// ...
}),
execute: async (params, config, context) => { // 直接执行
// 用 context.api 调用 host 服务(LLM 推理、存储等)
// 返回 text 或 JSON
},
// 或者:
// factory: ({ api, config, toolContext }) => createMyTool(api),
}),
],
});
```
两种声明方式(`tool-plugin.ts` `ToolPluginToolDefinition` 的两个分支):
- **`execute`**:声明完就是个完整的 tool,框架直接调
- **`factory`**:返回一个或多个 `AnyAgentTool`,适合需要根据
config / capability 动态构造工具的场景(`llm-task` 用的就是 factory)
`defineToolPlugin` 内部把每个 tool 注册到 plugin entry,在 plugin
activation 时跑 `register(api)`,把 tool 加进 agent 可用 tool 池
(`tool-plugin.ts:187-200`)。
## Manifest 让发现"不跑代码"
每个 plugin 同时有一份 `openclaw.plugin.json`
(`extensions/llm-task/openclaw.plugin.json`):
```json
{
"id": "llm-task",
"activation": { "onStartup": true },
"configSchema": { "..." },
"contracts": {
"tools": ["llm-task"]
},
"toolMetadata": {
"llm-task": { "optional": true }
}
}
```
`contracts.tools` 把 plugin 提供的工具名列出来。这样 OpenClaw 在
**还没加载 plugin 代码**时就知道"装了这个 plugin 会多哪些工具"——
`openclaw plugins list`、setup、文档生成都不用 import 真实运行时。
这条规则在 `src/plugins/CLAUDE.md` 里写明:
> Preserve manifest-first behavior: discovery, config validation, and setup
> should work from metadata before plugin runtime executes.
`defineToolPlugin` 输出的对象上挂了一个 `[toolPluginMetadataSymbol]`,
包含同样的 metadata,这样 manifest 落地后可以做 contract 校验,
保证 manifest 跟代码不漂移(`tool-plugin.ts:109-116`)。
## Plugin SDK 边界
来自根 `AGENTS.md` 的硬规则:
> 插件 prod 代码:no core `src/**`, `src/plugin-sdk-internal/**`, other
> plugin `src/**`, or relative outside package.
插件只能通过 `openclaw/plugin-sdk/*` 跟 core 交互。`defineToolPlugin`
`register(api)` 拿到的 `OpenClawPluginApi` 就是 plugin 看得见的所有
host 能力(LLM 推理、存储、其他 plugin 句柄等),core 内部的具体实现
对 plugin 透明。
这条边界让 plugin tool 既能用上 host 内部能力(不像 MCP server 那样
完全隔离),又不会被 core 实现变更直接打死(只要 SDK 不破坏)。
## 仓库里有哪些 plugin tool
`grep "contracts.*tools" extensions/*/openclaw.plugin.json` 找到的:
| 插件 | 提供的能力 |
| ----------------- | ----------------------------------- |
| `browser` | 浏览器自动化 |
| `canvas` | 画图 |
| `diffs` | 大文件 diff |
| `llm-task` | "调一次 LLM 跑结构化任务" |
| `memory-core` | 知识/记忆存取 |
| `memory-lancedb` | 向量记忆 |
| `memory-wiki` | wiki 风格记忆 |
| `tavily` | Web 搜索 |
| `firecrawl` | Web 抓取 |
| `feishu` | 飞书集成 |
| `qqbot` | QQ 集成 |
| `google-meet` | Google Meet 集成 |
| `file-transfer` | 文件传输 |
| `codex-supervisor` | Codex 任务编排 |
| `lobster` | (内部任务) |
`extensions/` 里**大多数其实不是 tool plugin**,而是另外两类——见下节。
## 它在更大的 plugin 生态里的位置
`extensions/` 里有三种主要 plugin,用不同的 `define*Plugin` 入口:
| 入口 | 用途 | 例子 |
| ------------------------------- | ---------------------- | ------------------------------ |
| `defineToolPlugin` | 给 agent 注册工具 | `llm-task`, `tavily`, `browser` |
| `defineChannelPluginEntry` | 接入消息渠道 | `telegram`, `discord`, `imessage` |
| `defineSingleProviderPluginEntry` | 接入 LLM provider | `anthropic`, `openai`, `google` |
| `defineSetupPluginEntry` | 安装/onboarding 流程 | 各 plugin 的 setup 部分 |
不同入口在 SDK 内部都收敛到 `definePluginEntry`(`src/plugin-sdk/plugin-entry.ts`)
拿到统一的 plugin lifecycle(activation、register、config)。
**channel / provider plugin 不直接给 agent 注册工具**——它们暴露的是
消息渠道或模型推理能力。tool plugin 是这个生态里**专门给 agent 加
function 的那一类**。
(理论上 channel plugin / provider plugin 也可以通过 `register(api)`
顺手注册 tool,但很少这么干;一旦这么做就既是 channel/provider plugin
又有 tool plugin 的属性。)
## 它怎么进到模型的 tool 列表
跟 MCP tool 走同一条路径(见
[mcp-client-internals.md](./mcp-client-internals.md) 的 "Attempt 引爆点"):
```
plugin 启动时(activation.onStartup 或按需)
→ tools 列表里每个 tool({...}) 被注册成 AnyAgentTool
→ 加进 agent 的可用 tool 池(在 host 进程内存里)
每次 attempt 开始
→ tools = [...native, ...plugin, ...bundleMcp, ...bundleLsp]
→ 走 provider adapter
→ 发给 LLM
```
模型看见的就是一个名字 `llm-task` 或者 `tavily_search`,没有任何
"这是 plugin"的标识。execute 时框架找到对应 tool,调它的 `execute`
函数,**这一切都在 OpenClaw 同一个 Node 进程里**,跟 MCP 走 stdio
子进程或 HTTP 完全不同。
## 跟 MCP tool 的关键对比
| 维度 | plugin tool | MCP tool |
| -------------- | ---------------------------------- | ---------------------------------- |
| 语言 | TypeScript(锁死 Node) | 任意(JSON-RPC) |
| 进程 | 同进程 | 子进程 / 远端 |
| 启动开销 | 几乎 0(import 即可) | spawn + 握手 + listTools |
| 故障隔离 | 一个 plugin 崩了可能影响整个进程 | 子进程崩了不影响别人,有失败退避 |
| 访问 host 服务 | 通过 `OpenClawPluginApi` | 完全隔离,什么都拿不到 |
| 跨应用复用 | OpenClaw 专属 | 任何 MCP host 都能用 |
| 分发 | npm package(`@openclaw/plugin-*`) | 任何二进制 / 服务 |
| 适合做什么 | 深度集成 OpenClaw 内部能力的工具 | 通用、跨应用、可独立运行的工具 |
例子:`llm-task` 必须是 plugin tool,因为它要复用 OpenClaw 的
provider/auth/catalog 体系跑一次内嵌 LLM 推理;
`@modelcontextprotocol/server-filesystem` 适合做 MCP server,因为它
跟 OpenClaw 没耦合,Claude Code 也能用。
## 在"两根轴"里的位置
回顾 [mcp-overview.md](./mcp-overview.md) 的所有权 × 时机两根轴,
**plugin tool 占了"用户/第三方拥有 + 编译时确定"那一格**:
```
开发者拥有 用户/第三方拥有
┌──────────────────┬──────────────────┐
编译时确定 │ native tool │ plugin tool ★ │
├──────────────────┼──────────────────┤
运行时发现 │ (动态 native) │ MCP tool │
└──────────────────┴──────────────────┘
```
这个反例的价值在于:它证明了 "动态/静态" 跟 "开发者/用户" 是
**两个独立的轴**——不能简单说 "用户拥有的就一定是运行时发现"。
plugin tool 是用户拥有的(插件作者写、用户装),但因为还是 npm
package 同语言同进程,完全可以编译时确定。
## 相关
- [mcp-overview.md](./mcp-overview.md) — MCP 概览、与 tool use 的关系
- [mcp-client-internals.md](./mcp-client-internals.md) — MCP client
实现细节(plugin tool 跟 MCP tool 在 attempt 里的拼装)
+382
View File
@@ -0,0 +1,382 @@
# RAG 与向量化
本文是 RAG(Retrieval-Augmented Generation)概念笔记,重点回答两个
常被混淆的问题:**向量化的结果是什么**、**拼进 prompt 的到底是
向量还是原文**。
不像本目录其他笔记基于 OpenClaw 源码,这一篇是通用概念整理。
## RAG 是什么
一句话:**让 LLM 在回答前先去外部知识库里捞一段相关资料,把资料
拼进 prompt,再让模型基于资料回答。**
它解决 LLM 的三个硬伤:
1. **知识截止** — 训练完就定死了,新事件不知道
2. **不知道私有数据** — 公司文档、产品手册、内部 wiki 它没见过
3. **会编(幻觉)** — 不知道的事会一本正经胡说
RAG 的思路是:别让模型 "凭记忆答",让它 "看着资料答"。
## 跟其他方案的对比
| 方案 | 优势 | 劣势 |
| ------------------------------- | ----------------------------------- | ------------------------------------ |
| **RAG** | 知识可实时更新,可解释,便宜 | 检索质量决定上限,prompt 会变长 |
| **Fine-tuning** | 模型"内化"知识,不用每次塞资料 | 训练贵,数据更新要重训,容易遗忘 |
| **超长 context(塞全部文档)** | 不用检索,看全部信息 | 贵,"大海捞针"问题 |
| **Tool use / MCP**(让模型自己调 search) | 灵活,按需查 | 多一轮往返,延迟高,不一定查到 |
实际系统经常组合:RAG 提供基础知识,fine-tuning 调风格,tool use
处理结构化查询。
## 最小工作流
```
用户问:"我们公司的报销额度是多少?"
[1] 检索(Retrieval)
把问题向量化,在公司文档向量库里找最相关的几段
→ 拿到: "差旅报销单笔上限 2000 元,需要主管审批..."
[2] 增强(Augmentation)
把检索结果拼进 prompt:
"根据以下公司文档回答问题:
差旅报销单笔上限 2000 元,需要主管审批...
问题: 我们公司的报销额度是多少?"
[3] 生成(Generation)
LLM 基于 prompt 里给的资料生成回答
→ "公司差旅报销单笔上限是 2000 元,需要主管审批。"
```
## 典型架构
**离线索引阶段**(只做一次,数据更新时跑):
```
文档 → 切块(chunking) → embedding → 向量数据库
```
**在线检索阶段**(每次用户问问题时):
```
用户问题 → embedding → 向量数据库找 top-k 相似块 → 拼 prompt → LLM
```
常见 top-k = 3 ~ 10。常用向量数据库:Pinecone、Weaviate、Qdrant、
LanceDB、pgvector(Postgres 扩展)。
## 向量化:这是什么、为什么需要
### 为什么需要向量化
用户问 **"怎么退货?"**,文档库里有:
```
A. "退款政策:商品 7 天内可申请退货退款..."
B. "如何使用购物车..."
C. "我们的隐私条款..."
```
朴素的关键词匹配有问题:
- 用户问 "返货" 或 "怎么把货退回去" 时关键词对不上
- 文档里写 "退款政策",没出现 "退货" 原词
- 用户问 "我不想要这个东西了" 时,字面完全不含 "退货"
**关键词匹配是字面的,问问题是语义的**。需要一种方法,让计算机
能算 "这两段文字意思有多接近",哪怕字面完全不同。
### 向量是什么
向量就是 **一组数字**。比如:
```
"退货" → [0.21, -0.13, 0.88, 0.45, ..., 0.07] ← 1536 个数
```
这 1536 个数字不是随便定的,它们是某种 "语义坐标"。
**语义相近的词,向量在数学上也接近**:
```
"退货" → [0.21, -0.13, 0.88, ...]
"退款" → [0.23, -0.10, 0.85, ...] ← 跟 "退货" 很接近
"返货" → [0.22, -0.12, 0.87, ...] ← 也接近
"购物车" → [0.91, 0.40, -0.20, ...] ← 离 "退货" 远
"隐私" → [-0.55, 0.70, -0.10, ...] ← 完全不同方向
```
把它们想象成 1536 维空间里的点——意思相近的词,在这个空间里挨得近。
### "近" 怎么算
**余弦相似度(cosine similarity)**:
```
两向量方向越一致 → 越相似(值接近 1)
两向量方向越垂直 → 越无关(值接近 0)
两向量方向越相反 → 越相反(值接近 -1)
```
RAG 实际就是 "把用户问题向量和每段文档向量两两算 cosine,排序,
取 top-k"。
### 谁来 "算出" 这些向量
预训练好的 **embedding 模型**。常见:
- OpenAI 的 `text-embedding-3-small` / `text-embedding-3-large`
- 开源 `bge-large` / `gte-large` / `m3e`
- 中文向量化常用 `bge-large-zh` / `m3e-base`
- 多语言 `bge-m3` / `multilingual-e5`
训练目标大致是 "意思相近的句子输出向量接近,意思不同的句子输出向量
远"。用的时候只需要:
```python
embedding = embed_model("退货")
# → [0.21, -0.13, 0.88, ..., 0.07] (1536 个数)
```
做 RAG 时把它当**黑盒**:输入文本,输出向量,语义近的向量接近。
## 向量化的结果是什么、拼进 prompt 的是什么
**这是 RAG 最容易混淆的点。**
答案:**向量化的结果是向量(一组数字),但拼到 prompt 里的是原文,
不是向量。向量只用来"找",找到之后用的是原文。**
### 数据流
```
[索引阶段]
原文 向量(用来查) 存储
┌──────────────────┐
│ 块 #001: │
│ "退款政策:商品 │ ─embedder→ [0.21,-0.13,..] ─┐
│ 7 天内可申请退货"│ │
└──────────────────┘ │
┌──────────────────┐ │ 存进数据库:
│ 块 #002: │ │ 每条记录有两份
│ "购物车使用指南" │ ─embedder→ [0.91, 0.40,..] ─┤ 数据并存:
└──────────────────┘ │ ① 向量(索引用)
┌──────────────────┐ │ ② 原文(召回用)
│ 块 #003: │ │
│ "隐私条款..." │ ─embedder→ [-0.55,0.70,..] ─┘
└──────────────────┘
向量数据库里实际存的:
┌─────────────────────────────────────────────────────────────┐
│ id │ vector │ text │
│ #001 │ [0.21,-0.13,0.88,..]│ "退款政策:商品 7 天内可..." │
│ #002 │ [0.91, 0.40,-0.20,..]│ "购物车使用指南..." │
│ #003 │ [-0.55,0.70,-0.10,..]│ "隐私条款..." │
└─────────────────────────────────────────────────────────────┘
↑ ↑
用来 "找" 用来 "还原"
```
```
[查询阶段]
用户问题
"怎么退货?"
┌─embedder→ [0.20,-0.14,0.86,...] ← 问题也变成向量
│ 跟数据库里所有向量算 cosine 相似度
│ Top-1:#001 相似度 0.89
│ ↓
│ 根据 id #001 取回原文:
│ "退款政策:商品 7 天内可申请退货退款..."
│ ↓
└→ 把【原文】拼进 prompt:
"根据以下文档回答:
退款政策:商品 7 天内可申请退货退款...
问题:怎么退货?"
发给 LLM
```
### 关键澄清
**1. 向量只用来检索,不参与 prompt**
LLM 完全看不到向量。它跟 embedding 模型是两个不同的模型:
- embedding 模型:**文本 → 向量**
- LLM(GPT / Claude):**文本 → 文本**
把向量塞进 LLM 的 prompt 没意义——LLM 不知道 `[0.21, -0.13, ...]`
是什么,它只懂自然语言 token。
**2. 向量数据库同时存向量和原文**
每条记录通常长这样:
```json
{
"id": "doc-001-chunk-3",
"vector": [0.21, -0.13, 0.88, ...], // 索引结构,算相似度用
"text": "退款政策:商品 7 天内可申请退货退款...", // 原文,召回后用
"metadata": {
"source": "policies/refund.md",
"chunk_index": 3,
"updated_at": "2026-05-12"
}
}
```
- `vector` 字段:用 HNSW 之类的索引结构,给 "找最近邻" 用
- `text` 字段:召回后真正塞进 prompt 的内容
- `metadata`:过滤条件(按日期、按部门筛选)
**3. 向量是有损的压缩**
`text-embedding-3-small` 输出 1536 维向量,1536 个浮点数 ≈ 6 KB,
跟几百字原文容量差不多——但**你没法从向量还原出原文**。
向量是 "这段文字的语义指纹",指纹用来匹配,看真实内容要回到原文。
数据库必须把原文也存下来。
### 类比
可以把这套机制类比图书馆:
- **向量** ≈ 图书索引卡的 "主题分类号",用来快速定位
- **原文** ≈ 书的内容,真正要读的东西
读者(LLM)读的是书,不是索引卡。索引卡只是帮你找到该读哪本书。
### 完整 prompt 长什么样
实际发给 LLM 的请求大概(简化):
```
System: You are a helpful assistant. Answer based on the provided context.
Context:
---
[Source: policies/refund.md]
退款政策:商品 7 天内可申请退货退款,需保持原包装完好,联系客服 400-xxx 办理。
---
[Source: faq/shipping.md]
订单状态查询:登录后在"我的订单"查看,异常订单 24 小时内会有客服回访。
---
Question: 怎么退货?
```
从头到尾全是自然语言文本。**向量已经在检索阶段完成了它的使命,
不会出现在最终 prompt 里。**
## 向量的几个有意思的性质
### 1. 不止短词,任意长度文本
embedding 模型能把一个 **短词** 变成向量,也能把一 **整段几百字**
变成同样维度的向量。所以 "问题向量" 和 "文档向量" 可以直接比较。
### 2. 跨语言也能用
好的多语言 embedding(`bge-m3``multilingual-e5`)让 "退货" 和
"return goods" 的向量接近。RAG 可以做跨语言检索:中文问题查英文文档。
### 3. 多模态
CLIP 这类模型把 **图片和文字** 映射到同一个向量空间。可以用文字
"穿红裙子的女孩" 检索图片库——文字向量和图片向量直接算 cosine。
### 4. 维度高就是为了 "细分"
为什么要 1536 维而不是 10 维?因为人类语言的语义维度太多——词性、
情感、领域、时态、正式程度...每一维捕捉一点点信息。低维度容易把
"无关的事压扁到同一处"。
## RAG 的几个常见坑
### 1. 检索失败比幻觉更糟
如果检索没拉到相关文档,模型只能瞎编。要么加 "如果资料里没有就说
不知道",要么在 retrieval 失败时直接拒绝回答。
### 2. 切块策略影响巨大
按固定长度切会切断句子,按段落切又长度不均。常见做法:
- **语义切块**:用嵌入相似度决定切点
- **重叠切块**:相邻块有 10% ~ 20% 重叠,避免边界丢上下文
- **保留层级**:把 "标题 + 段落" 一起塞进块里,保留文档结构
### 3. embedding ≠ 相关性
embedding 抓的是 "语义相似",不一定是 "对这个问题有答案"。
例子:用户问 "明天天气怎么样",文档里有:
- A. "天气预报通常由气象台发布..."
- B. "明天我们公司放假..."
embedding 可能把 A 排在前面(都讨论天气),但 A 没回答用户的问题;
B 字面不相关但其实是用户该看的(他可能想问 "明天用不用上班")。
工程上常见加强:
- **混合检索**:embedding(语义)+ BM25(关键词)双路召回合并
- **Rerank**:第一轮拉 50 个,用专门的 reranker 模型
(`cohere-rerank``bge-reranker`)精排到 top 5
- **HyDE**:让 LLM "假想" 一个答案,用假答案去检索——比直接用
问题更接近文档表述
### 4. 评估很难
模型说错了,是检索没拉到?还是拉到了模型没用?还是用了但理解错了?
分阶段评估:
- 检索准确率(召回的 top-k 里有没有正确答案)
- 端到端准确率(最终回答对不对)
- 引用一致性(回答有没有 hallucinate,引用的内容真的在检索结果里吗)
## RAG 跟 MCP 的关系
可以把 RAG 视作 **"一类常见的 MCP server"**——很多 MCP server 干的
事就是 "接到 query → 内部跑 RAG → 返回相关内容":
- `mcp-server-notion`:用户问问题,server 在 Notion 里搜相关页面返回
- `mcp-server-confluence`:同上,在内部 wiki 上跑 RAG
- 公司自建 MCP server:把产品文档喂进 LanceDB,模型问到产品时查它
但 RAG 也可以 **不通过 MCP**——直接在 agent 框架里写死
(LlamaIndex、LangChain 那些),或者在每次 LLM 请求前直接拼 prompt。
MCP 只是把 RAG 能力 "包装成可复用工具" 的一种方式。
详见 [mcp-overview.md](./mcp-overview.md)。
## 总结
```
LLM 不知道答案?
├─ 是公开知识但过时了 → RAG(查最新资料)或 web search tool
├─ 是你的私有数据 → RAG(查内部文档库)
├─ 是结构化数据(SQL/API) → tool use(直接查)
├─ 是需要算的(数学/代码) → tool use(让模型调 code interpreter)
└─ 是需要模型"内化"的风格/技能 → fine-tuning
```
RAG 是最常用的一格——便宜、可解释、知识能实时更新,适合 "知识问答"
和 "基于文档回答" 这类任务。
**心智模型一句话**:向量是 "导航地图"(找到正确目的地),原文是
"目的地内容"(交给 LLM 读)。embedding 模型把文本变成向量是为了
搜索;搜索完成后,LLM 看到的永远是原文。