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:
Vendored
+3
-1
@@ -27,5 +27,7 @@
|
|||||||
"file-recovery": true,
|
"file-recovery": true,
|
||||||
"publish": false,
|
"publish": false,
|
||||||
"sync": true,
|
"sync": true,
|
||||||
"webviewer": false
|
"webviewer": false,
|
||||||
|
"footnotes": false,
|
||||||
|
"bases": true
|
||||||
}
|
}
|
||||||
Vendored
+13
-5
@@ -13,12 +13,12 @@
|
|||||||
"state": {
|
"state": {
|
||||||
"type": "markdown",
|
"type": "markdown",
|
||||||
"state": {
|
"state": {
|
||||||
"file": "blog/function-call-101.md",
|
"file": "blog/mcp-101.md",
|
||||||
"mode": "source",
|
"mode": "source",
|
||||||
"source": false
|
"source": false
|
||||||
},
|
},
|
||||||
"icon": "lucide-file",
|
"icon": "lucide-file",
|
||||||
"title": "function-call-101"
|
"title": "mcp-101"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -142,13 +142,13 @@
|
|||||||
"state": {
|
"state": {
|
||||||
"type": "outline",
|
"type": "outline",
|
||||||
"state": {
|
"state": {
|
||||||
"file": "blog/function-call-101.md",
|
"file": "blog/mcp-101.md",
|
||||||
"followCursor": false,
|
"followCursor": false,
|
||||||
"showSearch": false,
|
"showSearch": false,
|
||||||
"searchQuery": ""
|
"searchQuery": ""
|
||||||
},
|
},
|
||||||
"icon": "lucide-list",
|
"icon": "lucide-list",
|
||||||
"title": "function-call-101 的大纲"
|
"title": "mcp-101 的大纲"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -160,6 +160,7 @@
|
|||||||
},
|
},
|
||||||
"left-ribbon": {
|
"left-ribbon": {
|
||||||
"hiddenItems": {
|
"hiddenItems": {
|
||||||
|
"bases:新建数据库": false,
|
||||||
"switcher:打开快速切换": false,
|
"switcher:打开快速切换": false,
|
||||||
"graph:查看关系图谱": false,
|
"graph:查看关系图谱": false,
|
||||||
"canvas:新建白板": false,
|
"canvas:新建白板": false,
|
||||||
@@ -170,9 +171,16 @@
|
|||||||
},
|
},
|
||||||
"active": "d0cf7351ba09fde6",
|
"active": "d0cf7351ba09fde6",
|
||||||
"lastOpenFiles": [
|
"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",
|
"blog/function-call-101.md",
|
||||||
"README.md",
|
"README.md",
|
||||||
|
"blog/README.md",
|
||||||
|
"llm-autoregressive-kvcache.md",
|
||||||
"function-call-flow.md",
|
"function-call-flow.md",
|
||||||
"blog",
|
"blog",
|
||||||
"skills-management-design.md",
|
"skills-management-design.md",
|
||||||
|
|||||||
@@ -37,4 +37,35 @@
|
|||||||
LLM 自回归生成与 KV Cache:token 逐步生成的机制、prefill vs decode 阶段、
|
LLM 自回归生成与 KV Cache:token 逐步生成的机制、prefill vs decode 阶段、
|
||||||
首 token 慢的原因,以及服务端 Prompt Cache 的命中条件与 OpenClaw 中保证顺序确定性的做法。
|
首 token 慢的原因,以及服务端 Prompt Cache 的命中条件与 OpenClaw 中保证顺序确定性的做法。
|
||||||
|
|
||||||
|
- [mcp-overview.md](./mcp-overview.md)
|
||||||
|
MCP(Model 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>
|
完整官方文档:<https://docs.openclaw.ai>
|
||||||
|
|||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
Vendored
+33
@@ -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
|
||||||
|
}
|
||||||
Vendored
+181
@@ -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": []
|
||||||
|
}
|
||||||
@@ -8,3 +8,9 @@
|
|||||||
Function Call 是什么、LLM 如何学会使用工具:从一个问题入手,
|
Function Call 是什么、LLM 如何学会使用工具:从一个问题入手,
|
||||||
解释模型不执行工具、只输出结构化调用请求的本质,
|
解释模型不执行工具、只输出结构化调用请求的本质,
|
||||||
并以 OpenClaw 源码为例说明工具声明、调用、执行的完整链路。
|
并以 OpenClaw 源码为例说明工具声明、调用、执行的完整链路。
|
||||||
|
|
||||||
|
- [mcp-101.md](./mcp-101.md)
|
||||||
|
MCP 是什么、为什么 Function Call 之后还需要它:用 LSP 解决编辑器 × 语言
|
||||||
|
M × N 问题的类比引入,说清 MCP 跟 Function Call 是不同层(模型↔应用
|
||||||
|
vs 应用↔工具),澄清三个常见误解,并用 OpenClaw 源码示意 MCP server
|
||||||
|
怎么被发现、何时连接、模型如何看到扁平 tool 列表。
|
||||||
|
|||||||
+370
@@ -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
@@ -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
|
||||||
@@ -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
@@ -0,0 +1,241 @@
|
|||||||
|
# MCP(Model 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
@@ -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
@@ -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 看到的永远是原文。
|
||||||
Reference in New Issue
Block a user