Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
12 KiB
RAG 是什么?让 LLM 看着你的资料回答问题
本文是 RAG(Retrieval-Augmented Generation,检索增强生成)的科普介绍, 不依赖具体源码。完整技术细节见 ../rag-overview.md。
如果你看过 Function Call 101 和 MCP 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 模型预训练时的目标就是 "让意思相近的句子输出 向量接近,意思不同的句子向量远"。你不需要懂它的训练原理,只需要把 它当成一个黑盒:输入文本,输出"语义指纹"。
有了这个,检索就变成一件优雅的事:
- 离线时:把文档库里每段文字都向量化,把(向量,原文)成对存进 向量数据库(Pinecone、Qdrant、LanceDB 之类)
- 用户问问题时:把问题也向量化
- 在数据库里找跟 "问题向量" 最接近的几个 "文档向量"
- 把对应的原文拼进 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.md、 mcp-101.md
- 系列下一篇:skills-101.md——RAG 解决了 "知识" 的问题,Skill 解决 "什么时候用哪个工具" 的问题,会借用 RAG 类似的 思路但走出一个有意思的变体