Files
openclaw-notes/rag-overview.md
T
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

13 KiB

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

训练目标大致是 "意思相近的句子输出向量接近,意思不同的句子输出向量 远"。用的时候只需要:

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. 向量数据库同时存向量和原文

每条记录通常长这样:

{
  "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-m3multilingual-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-rerankbge-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

总结

LLM 不知道答案?

  ├─ 是公开知识但过时了        → RAG(查最新资料)或 web search tool
  ├─ 是你的私有数据             → RAG(查内部文档库)
  ├─ 是结构化数据(SQL/API)    → tool use(直接查)
  ├─ 是需要算的(数学/代码)    → tool use(让模型调 code interpreter)
  └─ 是需要模型"内化"的风格/技能 → fine-tuning

RAG 是最常用的一格——便宜、可解释、知识能实时更新,适合 "知识问答" 和 "基于文档回答" 这类任务。

心智模型一句话:向量是 "导航地图"(找到正确目的地),原文是 "目的地内容"(交给 LLM 读)。embedding 模型把文本变成向量是为了 搜索;搜索完成后,LLM 看到的永远是原文。