RAG 检索增强生成

RAG(Retrieval-Augmented Generation,检索增强生成)是在生成之前先检索:拿用户的问题去知识库里捞出相关片段,把这些片段和问题一起拼成新的输入,再交给模型生成回答。

它结合了两件事:信息检索(Retrieval)负责从外部知识库找材料,文本生成(Generation)负责把材料组织成回答。

注意这里没有发生的事:没有训练,没有改模型的任何一个参数。 模型拿到的还是那一段文字,只是这段文字里多了它本来不知道的东西。

这是理解 RAG 的关键,也是它所有优点和缺点的来源。最后 §6 会回到这一点上:RAG 本质上是一次 prompt 重构。

1. 提示工程、RAG、微调:先分清该用哪个

模型的能力来自海量知识的预训练——喂进去海量文本,出来一个具备各种能力的 AI。

但你用起来还是会拿到错误回复。这时候该上什么?先别急着选方案,先分清是哪种错。

症状 缺什么 对策
没问清楚 指令不明确,模型不知道你要什么 提示工程
缺乏背景知识 模型不知道你的事(你的库、你的文档、昨天的新闻) RAG
能力不足 模型本身做不到这件事 微调

三条不是同一层级的三个档位,是三个不同的问题:

  • 提示工程改的是输入的表达方式。 模型本来就会,你只是换了个说法让它做对。
  • RAG 改的是输入的来源。 模型不知道的事,检索出来塞给它。
  • 微调改的是模型本身。 不是它不知道什么,是它不知道该怎么做——格式、风格、领域里的细微判断。

一个够用的判据

这个问题的答案,写在某份文档里吗?

  • 在 → 用 RAG。答案存在,只是模型没见过,检索到就行,不需要训练。
  • 不在,你要的是它形成某种习惯(固定输出格式、特定口吻、领域里的隐含规则)→ 用微调。
  • 两个都不是,只是你没说清 → 用提示工程。

判据的价值在于它把「RAG 还是微调」这个常见纠结变成了一个可查证的问题:去翻文档,看答案在不在里面。

代价差一个数量级

改什么 迭代速度 成本
提示工程 输入文本 秒级 近乎为零
RAG 输入文本 + 一套检索系统 小时~天级(重建索引) 一次工程建设 + 持续维护
微调 模型的权重 天~周级 训练算力 + 标注数据 + 一套部署

所以顺序是:能靠提示工程解决就别上 RAG,能靠 RAG 解决就别微调。 这不是因为它们谁更高级,是因为代价差一个数量级。

而且三者是叠加关系,不是三选一。真实系统里通常三层都在:提示词写清楚任务、RAG 供上材料、微调过的模型保证输出格式稳定。

2. 什么是 RAG

最简的那张流程图:

1
2
3
4
5
6
7
Question
↓
Retriever ←→ Context
↓
Large Language Model
↓
Response

这个图的形状很简单,但值得多看一眼:问题的流向没变,只是中途插了一步取材料。

Retriever 和 Context 之间是双向箭头——它要去知识库里查。查完的结果,和原来的问题一起进模型。

拆开就是三步

一个常被引用的划分方式:

步骤 要解决的问题
Indexing(索引) 如何更好地把知识存起来
Retrieval(检索) 如何在大量知识中找到一小部分有用的,给到模型参考
Generation(生成) 如何结合用户的提问和检索到的知识,让模型生成有用的答案

三步看起来都很直白。但真做起来,每一步都有它自己的两难:分块要在语义完整和检索效率之间取舍,检索要在召回广度和精排准度之间取舍,生成要在「只依据材料回答」和「材料不够时怎么办」之间取舍。后面三节分别拆。

一个准确的比喻

LLM 是推理引擎,是大脑;RAG 是外挂,是硬盘、U 盘、记事本。

这个比喻好在它说清了分工:大脑负责想,外挂负责记。 外挂换一个,不影响大脑;大脑换一个,外挂还能接着用。

而它顺带解释了 RAG 为什么诱人:你没动大脑,只是给它配了个能随时翻的记事本。 想让模型知道新东西,不用重新训练——往记事本上写一页就行。

3. 三个优势,一个来源

RAG 常被列出的三个优势:

一、解决知识时效性问题。 模型的训练数据是静态的,停在了某个时间点;RAG 检索的是外部知识库,知识库随时更新,模型就随时知道。

二、减少模型幻觉。 通过引入外部知识,模型可以照着材料说,而不是从记忆里回忆,生成虚假内容的概率下降。

三、提升专业领域回答质量。 接上垂直领域的知识库——法律、医疗、你公司内部的接口文档——回答就有了专业深度,而这些东西本来不在训练数据里。

但这三点其实是同一个机制

三个优势来自同一处:生成的时候,模型手里多了一份带出处的事实。

  • 时效性,是因为知识库能随时更新,而权重不能
  • 幻觉减少,是因为模型在照着看,不是在回忆
  • 专业性,是因为知识库可以是任何领域的,不需要它出现在训练数据里

理解了这一点,就能理解它的边界——同一个机制,也是它所有缺点的来源。

一个诚实的补充

RAG 减少幻觉,但不消除幻觉。

有两种情况会让幻觉换个形式回来:

  • 检索错了。 捞回来的材料本身就是错的、或者答非所问。模型照着错材料说,产出依然错误——只是从「凭记忆编」变成了「拿着错材料编」。
  • 模型无视材料。 提示词没写清「只依据资料回答」,模型可能检索结果摆在眼前,还是按自己的记忆答。

所以上 RAG 之后要做的事变了:幻觉的锅有一部分转移到了检索质量上。 这就是 §5 要解决的问题。

4. Indexing:怎么把知识存进去

收集,然后切块

第一步是把散落的知识收拢:文档、网页、数据库、工单记录,多源数据整理成一个外部知识库。

然后把文档切成适当大小的片段(chunks)。这一步叫分块,它看起来最没技术含量,实际最容易出问题。

分块的两难:

1
2
3
4
5
6
7
块太小                                    块太大
↓ ↓
语义不完整 噪声太多
检索回来的一段话, 一个块里只有一句
本身读不懂 有用,其余全是干扰
↓ ↓
↕ 要在两者之间平衡
  • 块切太小:检索回来的片段本身读不懂。代词没指代对象(「它的配置在……」,它是谁?),前提条件在上一块里,模型拿到半截话没法用。
  • 块切太大:一个块里杂着好几个话题,真正有用的只有一句。噪声进上下文既干扰判断,又白占 token。

常见的折中是按语义边界切——段落、小节、标题层级,而不是按固定字符数硬切。再带上一部分重叠,避免把一句话从中间切断。

还有一个容易被忽略的硬约束:块的大小还受 embedding 模型的输入长度限制。不少中文 embedding 模型最大只吃 512 token(§7),超过就直接截断——切块切到 1000 字,后半段压根没进向量。

所以分块不是纯语义问题,也是模型约束问题。选 embedding 模型和定分块策略,是同一个决定的两半。

向量化:把语义变成坐标

用嵌入模型(embedding model)把每个文本块转成一个向量,存进向量数据库:

1
2
3
4
5
6
7
文档
↓
切 Chunk
↓
Embedding(语义理解)
↓
向量索引
1
2
3
4
原始数据 chunk(几个句子)
=> embedding(语义理解)
=> 向量数据库
=> 语义相似度的检索

这一步是 RAG 里最关键的转换:embedding 之后,「语义相似」变成了「向量距离近」。

检索于是变成一个几何问题——把问题也转成向量,去找离它最近的那些块。「找资料」这件事被翻译成了「找邻居」。

为什么不用关键词搜

因为同义不同词。

「怎么重置密码」和「账号登录不上怎么办」,字面几乎不重合,关键词检索(BM25)会漏掉后者。但语义上它们是同一件事,向量空间里离得很近。

但关键词检索也有它的长处:碰到专有名词、错误码、函数名这种必须精确匹配的东西,向量检索反而会「飘」——它觉得 ERR_4012 和 ERR_4013 语义很像。

所以生产系统里常见的是混合检索(hybrid retrieval):向量一路、关键词一路,两路各自召回,再合并排序。一路管「意思像」,一路管「字对得上」。

BGE-M3 就是把这个思路做进了一个模型——同时输出 dense、sparse、多向量三种表示(§7)。

5. Retrieval:怎么在大量知识里找到那一小部分

先转成向量,再比距离

查询处理:把用户输入的问题用同一个嵌入模型转成向量,然后在向量数据库里做相似度检索,取出最相关的若干片段。

「同一个模型」是关键。 问题向量和文档向量必须在同一个坐标系里,距离才有意义。换模型等于换坐标系——这一点后面 §7 还会再提一次,它有个很实际的后果。

但「最近」不等于「最相关」

只靠向量检索有一层天花板,原因在它的设计取舍:

向量检索为了快,把 query 和 doc 分开编码。 问题和文档各自独立地转成向量,中间从来没有「见过面」。代价是它只能比整体语义像不像,比不了细节是不是真的对得上。

举个例子:问「Go 里怎么等一组 goroutine 跑完」,检索回来一批讲 WaitGroup 的块。它们语义上都高度相关——但其中一个的代码是错的(wg.Add 写在了 go 后面)。光看向量距离,它和正确的那个块几乎一样近。

语义相似性判断不出对错。 所以需要第二阶段。

两阶段:召回要广度,精排要准度

这就是重排序(Rerank):

阶段 做什么 用什么 特点
召回 从全库里取出 Top-K 候选(比如 50 条) 向量检索(bi-encoder) 快,但糙
精排 给这 50 条重新打分,取 Top-N(比如 5 条) 重排序模型(cross-encoder) 慢,但准

差别在编码方式:reranker 把 query 和候选文档拼在一起送进模型,让它们真正「见面」再打分。所以它能判断细节对不对得上,而 bi-encoder 不能。

代价是它没法预先算好——每个候选都要现算一次,几十条就是几十次推理。所以它承担不了全库检索,只适合给召回结果做精排。

这个分工跟很多系统的形状一样:第一段管别漏(广度),第二段管别错(准度)。

Top-K 是第一个该调的旋钮

笔记里写的是 Top5。这是个合理的默认值,但它是个要调的参数,不是常数:

  • 取太少 —— 关键材料没被选进来,模型手里没有答案,只能编(回到 §3 的幻觉问题)
  • 取太多 —— 噪声一起送进去,还白占上下文预算,挤掉真正有用的那几条

实际调的时候,召回和精排的 K 要分开调:召回层可以慷慨一点(50 甚至 100,反正便宜),精排层再收紧(3~5 条)。因为精排的目的本来就是「从一堆像的里面挑出对的」。

再进一步:把检索做成工具

上面讲的是一条固定流水线——每次都检索。

真放进 Agent 里,更常见的做法是把检索本身做成一个工具:

1
2
3
@register_tool("search_knowledge_base")
def search_knowledge_base(query: str, top_k: int = 5) -> str:
"""在项目知识库里检索相关片段"""

这样模型自己决定:要不要检索、用什么 query 检索、检索几次。

这个自由度是有用的。「你好」不该触发一次全库检索;「这个报错怎么修」必须触发。而同一个问题,模型自己改写出来的 query 往往比用户原话更适合检索——用户说的是现象,检索要的是术语。

注意这里发生的事:RAG 从一条流水线,变成了模型手里的一个工具。

也就是说检索本身也是一次 Function Calling(Function Calling介绍 §4),所以它也守同一条原则——一个工具 = 一次外部动作。查知识库是一次外部动作,模型自己答不出来,符合这个标准。

而这其实是「重构 prompt」更彻底的版本:连要不要检索、检索什么,都交给模型决定了。

6. Generation:怎么组装出那个新 Prompt

最后一步是把材料拼成输入。所谓「上下文组装」,字面意思就是拼字符串:

1
2
3
4
5
6
7
8
你是客服助手,只根据下面的资料回答,资料里没有的就说不知道。

## 参考资料
[1] 重置密码:登录页点「忘记密码」,输入注册邮箱,去邮件里点链接……
[2] 密码规则:至少 8 位,需同时包含字母和数字……

## 用户问题
我密码忘了怎么办?

模型读的是这一段,然后生成回答。

那句话

到这里可以回到开头那句话了:

RAG 本质上,是重构了新的 prompt。

写成流转关系就是:

1
2
3
4
5
6
7
用户原问题
+
检索出来的知识
↓
构造成一个新的 Prompt
↓
交给 LLM

user prompt → rag based user prompt。

这句话值得记住,因为它一次解释了三件事。

一、它解释了 RAG 为什么不需要训练。 换知识库不用重新训练模型,因为改的只是拼进 prompt 的那段文本。模型没变,输入变了。

二、它解释了 RAG 的能力边界。 模型在 prompt 里已有的能力就是天花板——如果它读不懂那份材料,或者不会按材料回答,RAG 救不了。那是能力不足,是 §1 里微调要解决的问题。RAG 只能给它材料,给不了它本事。

三、它解释了 RAG 的主要失败模式。 两种失败都发生在「拼」这一步:

失败 根因 在哪一步
材料就是错的 检索错了 / 分块切坏了 §4、§5
材料是对的,模型不照做 指令没写清「只依据资料回答」 §6

第二种经常被忽略。检索做得再准,提示词里缺了那句约束,模型照样可以无视材料自己编。

所以一句话总结:RAG 没有让模型变聪明,它让模型在做这道题的时候,把参考书翻开了。 而参考书翻得对不对、你是不是叮嘱了它只准照书答——这两件事都得你自己管。

7. Embedding 模型怎么选

模型 最大输入 语言 特点
BGE-M3(智源) 8192 100+ dense + sparse + 多向量三合一,一个模型给全套混合检索
text-embedding-3-large(OpenAI) 8191 多语言 3072 维,长文本语义捕捉强,英文表现优秀
jina-embeddings-v2-small-en(Jina AI) 8192 英文 仅约 3300 万参数,轻量、适合实时推理
xiaobu-embedding-v2 512 中文 1792 维,中文语义优化,MTEB 中文榜上表现好
stella-mrl-large-zh-v3.5-1792d 512 中文 1792 维,MRL 训练,能按需截断向量维度

选型看四件事,按顺序:

一、先看语言。 中文为主 → BGE-M3 或中文专用模型;英文为主 → text-embedding-3-large 或 Jina。跨语言长文档 → BGE-M3,它就是为这个场景设计的。

二、再看长度上限。 这一条最容易被忽略,但它直接约束 §4 的分块:

1
2
BGE-M3 / Jina / text-embedding-3-large  →  能吃到 8000 token 上下
xiaobu / stella 系列 → 上限 512 token

512 意味着你的块不能切太大,否则超出的部分静默丢失——不报错,只是没进向量,检索永远找不回来。所以「选模型」和「定分块策略」是同一个决定。

三、再看部署方式。 Jina 的 small 版只有 3300 万参数,适合本地实时推理;text-embedding-3-large 是 API,省事但数据要出网。

四、最后看要不要混合检索。 打算向量 + 关键词一起上的话,BGE-M3 一个模型就全给了,不用再单独接一套 BM25。

这份清单里有两处要修正

一、Chinese-Alpaca-2 不是嵌入模型。

它是生成式的中文指令模型(Llama-2 的中文词表扩充 + 增量预训练 + 指令微调那一支),在 RAG 里它站在生成那一侧,不是检索那一侧。

把它列进 embedding 候选是分类放错了位置。真要用它,它接的是 §6 的 LLM 那个位置。

二、M3E-Turbo 这个名字查不到。

M3E(Moka Massive Mixed Embedding)是一个真实存在的系列——m3e-base(约 1.1 亿参数)和 m3e-large(约 3.4 亿参数),中文效果不错,跟 sentence-transformers 完全兼容,换模型名就能接进 chroma 这类向量库。

但没有叫 Turbo 的版本。用之前建议直接去模型仓库核对一下名字。

最后一条:换模型必须重建索引

embedding 模型换了,索引必须全部重建。

因为不同模型把同一段文本映射到不同的向量空间——上一节说的「坐标系」。拿旧索引配新模型,等于拿北京的坐标去查上海的地图,距离全是随机数。

好消息是它不会静默出错:检索出来的东西会明显不相干,你很快就会发现问题。但它是个上线前就该排进计划的工程量,不是出了事才想起来的。

8. 小结

一、提示工程、RAG、微调,按「缺什么」分工。 没问清楚 → 提示工程;缺乏背景知识 → RAG;能力不足 → 微调。一个够用的判据是:答案写在某份文档里吗? 在 → RAG。三者代价差一个数量级,所以按提示工程 → RAG → 微调的顺序往上加,而且它们是叠加不是三选一。

二、RAG = 检索 + 生成,全程不训练。 LLM 是大脑,RAG 是记着事的记事本。想让模型知道新东西,往记事本上写一页就行——这正是它诱人的地方。

三、三个优势来自同一个机制:生成时手里多了一份带出处的事实。 时效性、减少幻觉、专业深度,都是这一件事的三个侧面。

四、Indexing 的两难是分块。 块小则语义不完整,块大则噪声多,常见折中是按语义边界切、带一点重叠。还要记住一个硬约束:块不能超过 embedding 模型的输入上限(不少中文模型只有 512 token),超了会静默丢失。

五、Retrieval 要两阶段:先召回,再精排。 向量检索把「语义相似」变成了「距离近」,但它分开编码 query 和 doc,判断不出细节对不对。所以粗召回 Top-K 之后,要用 reranker 把 query 和候选拼在一起重新打分,再取 Top-N。召回要广度,精排要准度。 放进 Agent 里,检索还可以做成一个工具,让模型自己决定要不要查、查什么。

六、RAG 本质上是一次 prompt 重构。 用户问题 + 检索结果 → 拼成新 prompt → 交给 LLM。这一句解释了三件事:为什么不用训练(模型没变,输入变了)、能力边界在哪(模型读不懂材料,RAG 救不了,那是微调的活)、主要失败模式在哪(材料错了,或材料对了但指令没约束住它)。

七、选 embedding 模型先看语言和长度上限。 换模型 = 换坐标系,必须重建索引。

参考

作者

wangyawei

发布于

2026-09-15

更新于

2026-09-15

许可协议