Files
2026-06-17 17:32:26 +08:00

353 lines
12 KiB
Markdown

# RAG 是什么?让 LLM 看着你的资料回答问题
> 本文是 RAG(Retrieval-Augmented Generation,检索增强生成)的科普介绍,
> 不依赖具体源码。完整技术细节见
> [../rag-overview.md](../rag-overview.md)。
如果你看过 [Function Call 101](./function-call-101.md) 和
[MCP 101](./mcp-101.md),你已经知道 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](./function-call-101.md) 的应用),但:
- 多了几轮往返,慢
- 模型可能错过该查的时机
- 还是需要一个搜索引擎能搜到内部文档
### 选项 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-101.md) 讲过 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](../rag-overview.md)——包括向量数据库
选型、常见混合检索 / rerank / HyDE 策略、向量化背后的数学
- 系列前文:[function-call-101.md](./function-call-101.md)、
[mcp-101.md](./mcp-101.md)
- 系列下一篇:[skills-101.md](./skills-101.md)——RAG 解决了 "知识"
的问题,Skill 解决 "什么时候用哪个工具" 的问题,会借用 RAG 类似的
思路但走出一个有意思的变体