Files
openclaw-notes/blog/rag-101.md
T
2026-06-17 17:32:26 +08:00

12 KiB

RAG 是什么?让 LLM 看着你的资料回答问题

本文是 RAG(Retrieval-Augmented Generation,检索增强生成)的科普介绍, 不依赖具体源码。完整技术细节见 ../rag-overview.md

如果你看过 Function Call 101MCP 101,你已经知道 LLM 怎么 "用工具" 了。但还有一类 问题工具解决不了:模型不知道你公司的数据

你问 ChatGPT "我们公司的报销额度是多少",它怎么可能知道? 报销政策没在它的训练数据里。

RAG 是目前最常用的解法。这篇讲它是什么、怎么工作、为什么需要专门搞一个 概念叫 "向量化"。


从一个具体问题开始

假设你给公司做一个 AI 助手,员工问:

"我们公司的报销额度是多少?"

你把这句话丢给 LLM,得到:

"我无法获取你们公司的具体政策。建议查询员工手册或联系 HR..."

废话。员工知道员工手册里有,但 200 页谁愿意翻?他们就是想让 AI 直接 告诉他们。

问题在于:LLM 训练时根本没见过你公司的员工手册。它的"知识"全部 封存在训练权重里,新数据进不去。

你有几个选择:

选项 A:把整本手册塞进 system prompt

System: 你是公司 AI 助手。以下是员工手册:
[200 页 PDF 文字...]

User: 我们公司的报销额度是多少?

理论可行,但:

  • 200 页可能 50 万 token,每次对话都付这个钱,贵
  • 模型有"大海捞针"问题——内容太多,中间部分容易被忽略
  • 手册更新了得重新塞,而且系统提示太长会挤占其他重要指令的空间

选项 B:Fine-tuning(微调)

把员工手册做成训练数据,微调一个版本的模型,让它"记住"这些政策。

可行,但:

  • 训练贵,得 GPU 集群
  • 政策一改要重训
  • 微调可能让模型遗忘其他能力(灾难性遗忘)
  • 你需要一支 ML 团队

选项 C:让模型自己搜

加一个 search 工具,让模型自己决定要不要查、查什么、读哪条结果。

可行(其实就是 Function Call 的应用),但:

  • 多了几轮往返,慢
  • 模型可能错过该查的时机
  • 还是需要一个搜索引擎能搜到内部文档

选项 D:RAG

在用户问问题之前,先去公司文档库里找相关段落,把段落和问题一起 塞给模型,让模型基于段落回答。

User 输入: "我们公司的报销额度是多少?"

  ↓ 在用户看不到的后台:
  ↓ [1] 检索:在公司文档库找最相关的几段
  ↓ → 找到:"差旅报销单笔上限 2000 元,需主管审批..."

  ↓ [2] 增强:把检索结果拼进 prompt
  ↓ Prompt: "根据以下文档回答:
  ↓          差旅报销单笔上限 2000 元,需主管审批...
  ↓          问题:我们公司的报销额度是多少?"

  ↓ [3] 生成:LLM 基于资料回答

模型输出: "公司差旅报销单笔上限是 2000 元,需要主管审批。"

R + A + G。模型不"记住"任何东西,每次回答之前临时拿资料看一眼。


为什么这是个好主意

对比一下:

方案 知识更新 成本 可解释 实施难度
全塞 prompt 每次都要重塞 每次对话都贵 不行(模型从一堆里挑) 简单
Fine-tuning 改了要重训 训练超贵,推理便宜 不行(知识进了权重)
让模型自己搜 实时(搜的是新数据) 多轮往返 能("我搜了 X 得到 Y")
RAG 实时(查的是当前库) 检索 + 一次推理 能("根据 X 文档")

RAG 几乎在所有维度上都不错。它最大的优势是"知识跟模型解耦"—— 你想换底层模型?随便换,检索逻辑没动;你想更新政策?改一下文档, 重新索引,搞定。

不用训练,不用 ML 团队,不用调几小时超参。


三个步骤拆开看

[1] 检索:怎么找到 "相关" 的段落

最朴素的想法:关键词搜

用户问 "退货",我在文档库 grep "退货",拿到所有包含"退货"的段落。

问题是:

  • 用户问 "退货" 但文档里只写 "退款政策" → 漏了
  • 用户问 "我不想要这东西了" → 字面不含 "退货",漏了
  • 用户问 "返货" → 也漏了

人类问问题是语义的,搜索却只能匹配字面的

RAG 的核心创新就在这里:用向量化把 "字面" 变成 "语义"

向量化:把文字变成一组数字

text-embedding-3-small(OpenAI 的 embedding 模型)接收一段文字, 吐出 1536 个浮点数:

"退货"    → [0.21, -0.13, 0.88, 0.45, ..., 0.07]
"退款"    → [0.23, -0.10, 0.85, 0.42, ..., 0.05]   ← 数字很接近
"购物车" → [0.91, 0.40, -0.20, 0.10, ..., -0.30]  ← 数字差很远

这 1536 个数字不是随便的——它们是 "语义坐标"。意思相近的词, 数字也接近

为什么?embedding 模型预训练时的目标就是 "让意思相近的句子输出 向量接近,意思不同的句子向量远"。你不需要懂它的训练原理,只需要把 它当成一个黑盒:输入文本,输出"语义指纹"

有了这个,检索就变成一件优雅的事:

  1. 离线时:把文档库里每段文字都向量化,把(向量,原文)成对存进 向量数据库(Pinecone、Qdrant、LanceDB 之类)
  2. 用户问问题时:把问题也向量化
  3. 在数据库里找跟 "问题向量" 最接近的几个 "文档向量"
  4. 把对应的原文拼进 prompt(注意是原文,不是向量)

"接近"用 余弦相似度 算——把向量当 1536 维空间里的方向,方向越 一致,夹角越小,余弦值越接近 1。

一个常见误解:向量和 prompt

很多人初学时会问:"向量化的结果是什么?是不是把向量塞进 prompt?"

不是。 LLM 看不懂向量,它只懂自然语言。向量只在"检索"这一步用 来匹配,匹配完之后用的是原文

数据库里其实是两份数据并存:

┌─────────────────────────────────────────────────────────────┐
│ 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,..]│ "隐私条款..."                │
└─────────────────────────────────────────────────────────────┘
              ↑                        ↑
         用来 "找"                  用来 "还原"

类比:向量像图书索引卡上的"主题分类号",原文像书的内容。 读者(LLM)读的是书,索引卡只是帮你找到该读哪本书。

最终发给 LLM 的 prompt 长这样,从头到尾全是自然语言:

System: You are a helpful assistant. Answer based on the provided context.

Context:
---
[Source: policies/refund.md]
退款政策:商品 7 天内可申请退货退款,需保持原包装完好,联系客服 400-xxx 办理。
---

Question: 怎么退货?

向量没出现,因为它的使命在检索那一步就完成了。

[2] 增强:把检索结果塞进 prompt

这一步简单,就是把找到的几段原文按某种格式拼进 prompt。常见格式:

请基于以下资料回答用户问题。如果资料里没有相关信息,直接说"我不知道"。

[资料 1] {{ chunk_1 }}
[资料 2] {{ chunk_2 }}
[资料 3] {{ chunk_3 }}

用户问题:{{ user_query }}

"如果资料里没有相关信息,直接说我不知道" 这一句很重要—— 没它的话模型容易在检索失败时强行编一个答案出来。

[3] 生成:让 LLM 回答

普通 LLM 调用,但 prompt 里多了"参考资料"。模型在生成时会优先用 prompt 里给的内容,不太敢瞎编(因为提示明确告诉它"基于资料")。

这一步没什么特别的——RAG 的"魔法"全在前两步。


实际工程里的坑

讲到这里你可能觉得 RAG 很简单——"切块 + 向量化 + cosine + 拼 prompt" 四步搞定。它的概念确实简单,工程却充满坑

坑 1:切块策略

文档不能整篇喂进去——一篇 50 页的政策手册一个向量,语义信息全被 平均掉了。要切块,但怎么切?

  • 按固定长度切?会切断句子,信息丢
  • 按段落切?长度不均,有的几行有的几百行
  • 按章节切?太大,匹配不精确

实际常用做法:滑动窗口 + 重叠——每 500 字一块,相邻块重叠 100 字, 避免边界丢上下文。更高级的做语义切块,用 embedding 相似度判断切点。

坑 2:检索失败比幻觉更糟

如果检索没拉到相关文档,模型就只能瞎编了。它可能写出一个听起来 专业、引用煞有介事但完全错误的答案。

防御措施:

  • prompt 里明确写 "如果资料里没有就说不知道"
  • 设相似度阈值——top-1 的 cosine 低于 0.5 就直接拒绝回答
  • 召回后做 reranker 二次验证

坑 3:embedding ≠ 相关性

embedding 模型抓的是 "语义相似",不一定是 "对你这个问题有答案"。

例子:用户问 "明天天气怎么样?",文档里有:

  • A. "天气预报通常由气象台发布..."
  • B. "明天我们公司放假,大家不用上班..."

embedding 可能把 A 排在前面(都讨论天气),但 A 其实没回答用户的问题; B 字面不相关,但其实是用户该看的(他可能想问 "明天用不用上班")。

实战常见的加强:

  • 混合检索:embedding(语义)+ BM25(关键词)双路召回合并
  • Reranker:先拉 50 个,用专门的重排模型(cohere-rerank、bge-reranker) 精排到 top 5
  • HyDE:让 LLM 先 "想象" 一个理想答案,用想象答案去检索——比直接 用问题更接近文档表述

坑 4:评估超难

模型说错了,根因是什么?

  • 检索没拉到正确文档?
  • 拉到了但模型没用?
  • 用了但理解错了?

需要分阶段评估:

  • 检索准确率(top-k 里有没有正确答案)
  • 端到端准确率(最终回答对不对)
  • 引用一致性(回答有没有 hallucinate,真的在检索结果里吗)

很多生产 RAG 系统在这上面投入的精力不亚于 RAG 本身的实现。


RAG 跟 MCP 是什么关系

MCP 101 讲过 MCP 是 AI 应用怎么接入外部工具的协议。 那 RAG 跟 MCP 是什么关系?

很多 MCP server 内部就是 RAG。比如 mcp-server-notion 接到 query 后,在 Notion 里做向量检索,返回相关页面——这是个标准 RAG 系统,只是 包装成了 MCP 接口。

但 RAG 也可以不通过 MCP——直接在 agent 框架里写死,或者每次 LLM 请求前手工拼 prompt。MCP 是分发机制,RAG 是内部实现,两者不冲突。

更广义看,RAG 是 "模型不知道怎么办" 这个问题的多种解法之一:

模型不知道怎么办?
  ├─ 缺事实/知识(报销额度、产品手册) → RAG(查文档库,把原文拼进 prompt)
  ├─ 缺实时数据(今天天气、最新 PR)   → tool use(让模型自己调 API)
  └─ 缺通用能力(根本不会写代码)       → fine-tuning(改模型本身)

下一篇会讲第四类情况——模型有工具但不知道什么时候用哪个—— 这又是另一种 "知识" 的缺失,需要另一种解法。


总结

RAG 的核心思路一句话:不让模型"凭记忆"回答,让它"看着资料"回答

[索引阶段]  文档 → 切块 → 向量化 → 存数据库

[查询阶段]  问题 → 向量化 → 找近邻 → 拼原文 → LLM 生成

它在四个维度上比其他方案漂亮:

  • 知识可实时更新——改文档重新索引就行,不用重训模型
  • 可解释——回答可以标注来源
  • 便宜——只需要一次嵌入服务调用 + 一次 LLM 推理
  • 可控——你完全决定文档库的内容和检索策略

如果你做的 AI 应用要回答 "基于我们的数据" 的问题,RAG 是默认应该考虑 的方案。它不一定是最完美的,但它便宜、好懂、可演化。


想深入了解?

  • 完整技术细节:../rag-overview.md——包括向量数据库 选型、常见混合检索 / rerank / HyDE 策略、向量化背后的数学
  • 系列前文:function-call-101.mdmcp-101.md
  • 系列下一篇:skills-101.md——RAG 解决了 "知识" 的问题,Skill 解决 "什么时候用哪个工具" 的问题,会借用 RAG 类似的 思路但走出一个有意思的变体