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

383 lines
13 KiB
Markdown

# 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 看到的永远是原文。