Files
clz 3c0cd86352 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 索引更新
2026-06-16 00:59:32 +08:00

225 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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