RAG 是什么?16 种 RAG 方案一次讲清!AI 应用开发必学 | 万字干货
最近这两年,只要你接触过 AI 编程,大概率听过一个词,RAG(Retrieval-Augmented Generation)。
但很多小伙伴对 RAG 都只是一知半解,导致面试的时候只能说出 “检索增强生成” 这六个字,面试官再多问一点,就只能 “阿巴阿巴”。
而这篇文章,是对 RAG 技术的一个全景科普,从最初的 Naive RAG、到现在最主流的 Agentic RAG,总共 16 种主流 RAG 方案,我会一次性给大家讲清楚。以后开发 AI 应用的过程中,无论遇到什么场景,都能选择最合适的 RAG 方案。
干货比较多,建议先点赞收藏,再慢慢往下看~
什么是 RAG?
AI 大模型有一些硬伤,比如:
- 知识有截止日期
- 会一本正经地胡说八道,也就是我们常说的幻觉
- 缺乏私有知识,了解不到内部的文档写了什么
比如问 DeepSeek:程序员鱼皮的最新项目是什么?
结果它给我扯了个两年半以前的项目出来,技术栈也完全不对!
解决这个问题就可以用 RAG。RAG 的核心思想是先搜再答,让大模型在回答之前先去搜一遍相关资料,再基于搜到的知识来组织答案。
就跟考试的时候偷偷翻书一样,遇到不会的先翻一翻书,再根据书里的知识答题。
还是问 AI 同样的问题,我们主动给 AI 一些参考资料,他的回答就会准确一些:
这个思路听起来简单,但在实际工程上 RAG 已经演化出了很多种不同的实现方法,从最初的「切块 → 搜索 → 生成」,到让 AI Agent 自主决策检索策略的 Agentic RAG,复杂度和能力天差地别。
有朋友可能会问:现在的大模型不是已经支持百万 token 的上下文窗口了,还需要 RAG 吗?
答案是:需要,而且用得比以前更多了!
因为把所有文档塞进上下文窗口,既贵又不靠谱。上下文越长 token 费用越高,而且大模型普遍存在 “Lost in the Middle” 问题,顾名思义,就是对超长上下文中间部分的注意力会明显下降。
这个也不难理解,就像听别人说话一样,我们对开头和结尾的印象会相对深刻一些,中间的总是容易忘记。
不过呢,RAG 和长上下文也不是互斥的关系,现在一般的最佳实践是先用 RAG 给 AI 提供相对精确的资料,再利用长上下文窗口进行有针对性的分析推理,两者互补。
好,背景交代完了。下面我们正式进入主题,挨个讲讲每种 RAG 方案。
我们先从最基础的 RAG 讲起,如果你之前完全没接触过 RAG,看完这一节就能理解它的核心原理。
主流 RAG 方案
标准 RAG 及变体
Naive RAG
Naive RAG 这个词听起来就很牛对不对?
但其实,Naive 是朴素的意思,Naive RAG 是最基本的 RAG 实现方案。
假设你有一份 200 页的公司员工手册,怎么让 AI 基于里面的内容回答员工的提问呢?
最简单粗暴的做法就是每次提问都把整本手册塞给 AI 大模型。
但是一本手册可能几十万字,全塞进去又贵又慢,而且上下文一长,大模型还容易犯前面提到的 “Lost in the Middle” 的毛病,导致回答质量下降。
所以更合理的思路是:员工问什么,我们就只把相关的几段内容塞给 AI。
那问题又来了,怎么定位到相关的那几段呢?
如果用关键词匹配,很容易出现问题和文档里的关键词不一致的问题,比如员工问 “老板不批假怎么办”,文档里写的是 “请假审批流程”,关键词对不上,就搜不到。
这就需要用到向量了。
所谓向量,就是把一段文字用一串数字表示出来,让计算机可以比较语义上的相似度。
举几个例子感受一下:
| 文本 | 向量(简化示意) |
|---|---|
| 我喜欢吃鱼 | [0.21, 0.85, 0.13, ...] |
| 我爱吃海鲜 | [0.23, 0.82, 0.15, ...] |
| 今天天气真好 | [0.88, 0.12, 0.41, ...] |
语义越接近的句子,它们的向量在数学空间里离得越近。
负责把文字转成向量的模型,叫 Embedding 模型;存储这些向量并支持快速相似度搜索的数据库,叫向量数据库,比如 Milvus、Chroma、Qdrant 等。
理解了向量,Naive RAG 的做法就很好理解了,主要分为两步。
第一步是离线索引:
- 把文档切成小块(chunk),每块几百字
- 用 Embedding 模型把每个小块转成向量
- 把向量和对应原文都存进向量数据库
第二步是在线查询问答:
- 把用户问题也用 Embedding 模型转成向量
- 去向量库里搜最相似的几个文档块(比如 Top 5)
- 把这几个块和用户问题拼成 Prompt,交给大模型生成回答
回到开头那份 200 页的员工手册。我们先把它切成几百个小块并向量化入库,当员工问 “年假有多少天?” 的时候,RAG 的执行流程是这样的:
- 系统把问题转成向量
- 在向量库里找到最相似的 5 个文档块(比如某块写着 “入职满一年享有 10 天年假,满三年 15 天…”)
- 把这 5 个块连同问题一起交给大模型
- 大模型回答:“入职满一年享有 10 天年假”
不过 Naive RAG 也有一些比较明显的问题:
- 切块方式粗暴,可能把一段完整的语义从中间截断
- 检索质量完全依赖 embedding 模型,搜不到就没辙
- 搜到了垃圾文档也不管,导致输出错误答案
这些局限,就是后面所有进阶方法要解决的问题。
下面我用伪代码来帮助大家理解,不熟悉编程的同学可以跳过,不影响后续的学习~
1)离线索引阶段:
# 1. 把文档切成小块,chunk_size=500 表示每块 500 字, # chunk_overlap=50 表示相邻块之间重叠 50 字,避免关键信息被切断 chunks = split_into_chunks(documents, chunk_size=500, chunk_overlap=50) # 2. 把每个小块转成向量,连同原文一起存入向量数据库 for each chunk in chunks: vector = embedding_model.encode(chunk) # 调用 Embedding 模型编码 vector_store.insert(vector, chunk) # 向量和原文一起入库2)在线查询阶段:
# 1. 用同一个 Embedding 模型把用户问题也转成向量 query_vector = embedding_model.encode(user_query) # 2. 在向量库里搜最相似的 Top 5 文档块 top_k_chunks = vector_store.search(query_vector, k=5) # 3. 把检索到的文档块拼进 Prompt,作为参考资料 prompt = "基于以下参考资料回答问题:\n" + join(top_k_chunks) + "\n问题:" + user_query # 4. 交给大模型生成最终回答 answer = LLM.generate(prompt)Multi-Query RAG
用户提问的方式千奇百怪。比如同样是想知道公司报销流程,有人会问怎么报销,有人会问费用审批流程是什么,还有人会问花了钱怎么找公司要回来。
但文档里可能只写了 “报销申请流程” 这个表述,如果用户的措辞和文档差距太大,向量检索就可能搜不到正确的内容。
Multi-Query 的思路就是:既然一种问法搜不全,那就让大模型把原始问题改成多种不同的表述,分别去搜,最后把结果合并去重。
这种方法的代价就是每次提问要多调用一次 LLM 做改写,再多跑 N 次向量检索,延迟和成本都会增加。
而且如果 LLM 改写出的问题方向跑偏,会把无关文档也带进来,影响答案质量。
所以它比较适合面向普通用户的客服、电商等场景,用户表述口语化、和文档术语差距大,多花这点成本还是有必要的。但是在术语规范的专业领域,Multi-Query 的收益就很有限了。
Multi-Query RAG 的代码实现如下:
# 1. 让 LLM 把原问题改写成多个表述 queries = LLM.generate("请将以下问题改写成 3 个不同的表述:" + user_query) // 例如 AI 返回: ["报销流程是什么", "费用审批怎么操作", "如何提交报销申请"] # 2. 每个表述分别走一次向量检索 all_results = [] for each query in queries: results = vector_store.search(embed(query), k=5) all_results.append(results) # 3. 合并去重,得到覆盖面更广的候选文档 merged_chunks = deduplicate(all_results) # 4. 把合并后的文档 + 原始问题交给大模型生成回答 answer = LLM.generate(merged_chunks + user_query)HyDE
AI 回复的效果不好,可能不是因为用户的问法不好,而是用户的问题和文档的语义空间不一致。
用户的提问往往很短,比如 “KV Cache 是什么?”,就这么几个字。
但文档里关于 KV Cache 的描述可能是一大段技术解释。一短一长,在 embedding 空间中可能离得很远,就检索不到了。
HyDE 的做法就是让大模型凭空写一个答案(不必完全准确),然后用这段假答案的向量去检索。因为假答案和真文档的文体更接近,两者在向量空间中离得也更近。
还是上面的例子,用户问:“KV Cache 是什么?”
LLM 先编一段假答案:“KV Cache 是一种在 Transformer 推理过程中缓存 Key 和 Value 矩阵的优化技术,可以避免重复计算…”
这段假答案虽然不一定完全准确,但它的向量和真实文档里关于 KV Cache 的那段描述非常接近,所以能精准命中正确的文档。
不过 HyDE 也有一个风险,如果 LLM 编的假答案方向完全跑偏了(比如把 KV Cache 理解成了 Redis 缓存),那检索结果就会更差。所以它比较适合 LLM 对问题领域有基本认知的场景,冷门领域或企业私有术语慎用。
HyDE 的代码示例如下:
# 1. 让 LLM 先凭空生成一段“假答案”(不必完全准确) hypothetical_answer = LLM.generate("请回答:" + user_query) # 2. 用假答案的向量去检索,因为它的文体更接近真实文档 hyp_vector = embedding_model.encode(hypothetical_answer) top_k_chunks = vector_store.search(hyp_vector, k=5) # 3. 把检索到的真实文档 + 原始问题交给大模型生成最终回答 answer = LLM.generate(top_k_chunks + user_query)上面这三种方法,解决的是「能不能搜到」的问题。但搜到之后,资料的质量好不好呢?这就是下面要处理的了。
提升检索质量
语义分块(Semantic Chunking)
Naive RAG 里最粗暴的一步就是切块,每 500 个字切一刀,管你是不是正好切在一句话中间。
比如某块文本是 “员工请假需要提前 3 天申请,超过 5 天需要”
刚好在这里被截断了,下一个块从 “部门经理审批” 开始。
这两个块单独看都不完整,检索和 AI 的理解都会打折扣。
虽然可以通过 chunk_overlap 设置相邻块之间重复内容的长度,但是效果相对有限。
语义分块的做法是先把文档按句子拆开,计算相邻句子的 embedding 相似度,当相似度突然下降时,说明话题变了,就在这里切一刀,尽量保证每个句子的语义都是完整的:
其余的步骤就跟 Naive RAG 一致了。
不过这种方式代价也不小,首先要为每一句话都算一次 embedding,成本和耗时比按字数切分要高。
而且相似度阈值非常难调,所以它比较适合结构松散、话题变化比较快的文档,像会议纪要、访谈记录这些。
对于本身就有清晰章节结构的技术手册、产品说明文档,直接按标题切效果也不差,还更便宜。
语义分块的伪代码示例如下:
# 1. 把文档拆成单句 sentences = split_into_sentences(document) chunks = [] current_chunk = [sentences[0]] # 2. 遍历相邻句子,计算 embedding 相似度 for i from 1 to len(sentences) - 1: similarity = cosine_similarity(embed(sentences[i-1]), embed(sentences[i])) # 相似度骤降,话题切换 if similarity < threshold: chunks.append(join(current_chunk)) current_chunk = [] current_chunk.append(sentences[i])层级索引(Parent-Child Retrieval)
切块这件事有一个天然矛盾。切得小吧,检索精度高但上下文不足;切得大吧,上下文丰富但噪声也大。
Parent-Child Retrieval 的策略则是两层都要。
先把文档切成大块,再把每个大块细分成小块。检索时用小块匹配,命中后返回它所属的大块。
