§1.2.8

RAG Survey(Tongji 2024、HKU 2024)的分类体系?

好的,我们来深入剖析 RAG 领域的最新研究进展,特别是两篇关键综述论文(通常指同济大学发布的《A Survey on Retrieval-Augmented Text Generation》和香港大学等机构发布的《Retrieval-Augmented Generation for Large Language Models: A Survey》)中提炼出的分类体系。

核心概念

检索增强生成(Retrieval-Augmented Generation, RAG)是一种将大型语言模型(LLM)与外部知识库相结合的范式。其核心思想是,在生成回答之前,首先从一个大规模的语料库(如维基百科、公司内部文档)中检索出与用户问题相关的文档片段,然后将这些片段作为上下文(Context)一并提供给 LLM,引导其生成更准确、更具事实性、更符合特定知识领域的回答。这有效缓解了 LLM 固有的知识陈旧、事实幻觉等问题,如同让模型进行“开卷考试”。

原理与推导

RAG 的核心可以被形式化为一个两阶段的概率过程。给定一个输入查询 xx,我们想生成一个输出序列 yy。传统的 Seq2Seq 模型直接建模 P(yx)P(y|x),而 RAG 则引入了一个隐变量 zz,代表检索到的文档。

生成过程的概率可以表示为:

P(yx)=zDP(yx,z)P(zx)P(y|x) = \sum_{z \in \mathcal{D}} P(y|x, z) \cdot P(z|x)

这个公式可以分为两个核心部分:

  1. Retriever (检索器): P(zx)P(z|x)。这一部分负责根据输入 xx 从知识库 D\mathcal{D} 中找到最相关的文档 zz。在实践中,这通常不是一个严格的概率分布,而是通过一个评分函数 s(x,z)s(x, z) 来实现的,例如向量相似度。检索器会返回得分最高的 Top-K 个文档。
    • 几何解释: 检索过程可以看作是在高维向量空间中寻找与查询向量 qxq_x 最邻近的文档向量 vzv_z。常用的相似度度量是余弦相似度:
    sim(qx,vz)=qxvzqxvz\text{sim}(q_x, v_z) = \frac{q_x \cdot v_z}{\|q_x\| \|v_z\|}
  2. Generator (生成器): P(yx,z)P(y|x, z)。这是一个标准的条件生成模型(即 LLM),它接收原始查询 xx 和检索到的文档 zz 作为联合输入,然后生成最终的答案 yy

根据 Tongji 2024 和 HKU 2024 等综述,RAG 的发展可以划分为三个主要范式:

1. Naïve RAG (朴素 RAG)

这是最基础和经典的 RAG 流程,遵循“检索-阅读”的线性管道。

  • 原理:
    1. Indexing: 离线过程。将文档库切分成块(Chunks),使用预训练的编码器(如 Sentence-BERT)将每个块编码为向量,并存入向量数据库(如 FAISS, Milvus)。
    2. Retrieval: 在线过程。将用户查询 xx 用同一个编码器编码成向量,然后在向量数据库中执行相似性搜索(如 MIPS,Maximum Inner Product Search),获取 Top-K 个最相似的文档块。
    3. Generation: 将原始查询 xx 和检索到的 K 个文档块拼接成一个长长的提示(Prompt),输入给 LLM 生成答案。
  • 算法复杂度:
    • Indexing: 设文档库有 NN 个块,编码器输出维度为 dd。建立索引的时间复杂度取决于所用的向量数据库,例如 FAISS 的 IndexFlatL2O(Nd)O(N \cdot d),但更高效的索引结构(如 HNSW)可以做到近似对数时间。
    • Retrieval: 对于暴力搜索是 O(Nd)O(N \cdot d),对于 HNSW 等近似最近邻(ANN)算法,查询时间复杂度约为 O(logN)O(\log N)O(poly(logN))O(\text{poly}(\log N))
    • Generation: 时间复杂度与输入序列长度和生成长度成正比,通常是 Transformer 架构的 O(L2)O(L^2),其中 LL 是输入 Prompt 的长度。

2. Advanced RAG (高级 RAG)

朴素 RAG 在索引、检索和生成等环节存在诸多挑战。高级 RAG 旨在通过优化特定模块来提升性能。

  • 原理: 围绕核心 RAG 流程的**前(Pre-)、中(Retrieval)、后(Post-)**三个阶段进行增强。
    • Pre-Retrieval (检索前优化): 优化索引质量。
      • Chunking 优化: 从固定大小切分,发展到基于句子边界(Sentence-aware)、递归切分(Recursive Chunking)或使用模型(如 Unstructured.io)进行内容感知的切分。
      • Data Cleaning & Metadata: 清理无关信息,并为每个 Chunk 添加标题、章节、日期等元数据,用于后续的过滤或混合检索。
    • Retrieval (检索中优化): 提升检索精度。
      • Query Transformation: 用户的原始查询可能不适合直接用于向量检索。
        • HyDE (Hypothetical Document Embeddings): 让 LLM 先为查询生成一个“假设性”的答案文档,再用这个文档的向量去检索,因为假设的文档在形式上更接近于知识库中的文档。
        • Multi-Query: 让 LLM 将一个复杂查询分解成多个子查询,分别检索后合并结果。
      • Fine-tuning Embedder: 使用领域内的数据对检索用的编码器模型进行微调,使其更能理解特定领域的语义相似性。通常使用对比学习损失(Contrastive Loss),如 Triplet Loss: L(q,d+,d)=max(0,sim(q,d)sim(q,d+)+\margin)\mathcal{L}(q, d^+, d^-) = \max(0, \text{sim}(q, d^-) - \text{sim}(q, d^+) + \margin) 其中 qq 是查询,d+d^+ 是正样本(相关文档),dd^- 是负样本(不相关文档),\margin\margin 是间隔。
    • Post-Retrieval (检索后优化): 处理检索到的信息。
      • Reranking: 初步检索(e.g., Top-20)的结果可能包含噪声。使用一个更强大但更慢的模型(如 Cross-Encoder)对这些初步结果进行重新排序,选出最终的 Top-K (e.g., Top-3)。
      • Context Compression: 检索到的上下文可能很长,包含冗余或无关信息,且可能超出 LLM 的上下文窗口。使用小模型或规则来压缩上下文,提取最关键的信息,以应对“迷失在中间(Lost in the Middle)”的问题。

3. Modular RAG (模块化 RAG)

这是 RAG 的最新范式,将 RAG 视为一个灵活、可插拔的框架,而非固定的线性管道。它引入了更复杂的控制流和功能模块。

  • 原理:
    • 新模块:
      • Search Module: 不再局限于向量检索,可以融合关键词搜索(BM25)、知识图谱查询、SQL 查询等多种检索方式。
      • Memory Module: 将近期对话的检索结果或生成的答案缓存起来,用于后续的交互,实现上下文感知。
      • Fusion Module: 智能地融合来自不同检索源的信息,或对检索到的信息进行加权、排序。
    • 新模式:
      • Iterative / Recursive RAG: 一次检索不满足时,系统可以基于初步结果生成新的查询,进行多轮检索,直到收集到足够的信息。这类似于人类的研究过程。
      • LLM-as-X: LLM 的角色不再仅仅是最后的生成器。
        • LLM-as-Planner: 对于复杂问题,LLM 首先制定一个执行计划,决定何时检索、检索什么、如何组合信息。
        • LLM-as-Rewriter: LLM 负责重写用户的原始查询,使其更适合检索。
        • LLM-as-Judge: LLM 评估检索到的文档的相关性,充当 Reranker 或决定是否需要再次检索。

代码实现

下面是一个使用 transformers, sentence-transformers, 和 faiss-cpu 实现的 Naïve RAG 示例。

python
1import torch
2import faiss
3import numpy as np
4from sentence_transformers import SentenceTransformer
5from transformers import pipeline, AutoTokenizer, AutoModelForCausalLM
6
7# 1. 准备环境和模型
8# 确保已安装: pip install torch sentence-transformers faiss-cpu transformers
9device = "cuda" if torch.cuda.is_available() else "cpu"
10
11# 用于编码文档和查询的句向量模型
12print("加载句向量模型...")
13encoder = SentenceTransformer('moka-ai/m3e-base', device=device)
14
15# 用于最终生成答案的语言模型
16print("加载语言模型...")
17# 注意:这里使用一个较小的模型作为示例。实际应用中通常使用更大的模型。
18# 如果显存不足,可以换成更小的模型如 'gpt2'
19model_name = "Qwen/Qwen1.5-0.5B-Chat"
20tokenizer = AutoTokenizer.from_pretrained(model_name)
21model = AutoModelForCausalLM.from_pretrained(model_name).to(device)
22generator = pipeline('text-generation', model=model, tokenizer=tokenizer, device=device)
23
24# 2. 索引 (Indexing)
25print("\n--- 索引阶段 ---")
26# 我们的知识库
27documents = [
28 "人工智能(AI)是研究、开发用于模拟、延伸和扩展人的智能的理论、方法、技术及应用系统的一门新的技术科学。",
29 "大型语言模型(LLM)是人工智能领域的一个重要分支,它能够理解和生成人类语言。",
30 "检索增强生成(RAG)通过结合外部知识库来减少大型语言模型的幻觉问题。",
31 "FAISS 是 Facebook AI 开发的一个用于高效相似性搜索和密集向量聚类的库。"
32]
33
34# 为什么这样做:将文本编码为向量是RAG的核心,它将语义信息压缩到数学表示中。
35print("正在将文档编码为向量...")
36doc_embeddings = encoder.encode(documents)
37doc_embeddings = np.array(doc_embeddings).astype('float32')
38
39# 为什么这样做:向量数据库(这里用FAISS模拟)能够极快地找到与查询向量最相似的文档向量。
40# d 是向量维度
41d = doc_embeddings.shape[1]
42# IndexFlatL2 使用L2距离进行暴力搜索,对于小数据集来说足够了。
43index = faiss.IndexFlatL2(d)
44index.add(doc_embeddings)
45print(f"索引建立完毕,包含 {index.ntotal} 个文档,向量维度为 {d}。")
46
47# 3. 检索 (Retrieval)
48print("\n--- 检索阶段 ---")
49query = "什么是RAG?"
50k = 2 # 我们希望检索最相关的2个文档
51
52# 为什么这样做:查询也必须用同一个模型编码,以确保查询向量和文档向量在同一个语义空间中。
53print(f"查询: {query}")
54query_embedding = encoder.encode([query])
55query_embedding = np.array(query_embedding).astype('float32')
56
57# 为什么这样做:index.search 会返回距离和索引。D是距离,I是文档在原始列表中的索引。
58distances, indices = index.search(query_embedding, k)
59
60print(f"检索到的 Top-{k} 文档索引: {indices[0]}")
61retrieved_docs = [documents[i] for i in indices[0]]
62
63# 4. 生成 (Generation)
64print("\n--- 生成阶段 ---")
65# 为什么这样做:这是RAG的关键步骤,将检索到的知识(上下文)和原始问题一起打包成一个清晰的指令,
66# 引导LLM基于给定的信息来回答问题,而不是依赖其内部可能过时或错误的知识。
67context = "\n".join(retrieved_docs)
68prompt = f"""
69你是一个AI助手。请根据下面提供的上下文信息来回答问题。如果上下文中没有相关信息,请说你不知道。
70
71上下文:
72{context}
73
74问题: {query}
75
76回答:
77"""
78
79print("构建的Prompt:")
80print(prompt)
81
82# 为什么这样做:调用LLM进行文本生成,max_length和temperature是常用参数,
83# 控制生成长度和创造性。
84messages = [
85 {"role": "system", "content": "你是一个AI助手。"},
86 {"role": "user", "content": prompt}
87]
88text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
89model_inputs = tokenizer([text], return_tensors="pt").to(device)
90
91generated_ids = model.generate(
92 model_inputs.input_ids,
93 max_new_tokens=128
94)
95
96generated_ids = [
97 output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)
98]
99
100response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
101
102print("\nLLM 生成的回答:")
103print(response)

工程实践

  • 使用场景:
    • 企业级知识库问答: 客服机器人、内部文档查询系统、投研报告分析。
    • 内容创作辅助: 辅助撰写需要事实依据的文章、报告,自动引用来源。
    • 个性化教育: 根据学生的提问,从教材库中检索相关知识点并生成解释。
  • 超参数选择:
    • chunk_sizechunk_overlap: 这是最关键的超参数。chunk_size 太小会丢失上下文,太大则可能引入噪声。通常在 256-1024 token 之间。chunk_overlap (e.g., 10-20% of chunk_size) 用于保证句子不被切断。
    • top_k: 检索文档的数量。通常在 3-5 之间。太小可能信息不足,太大则增加噪声和 LLM 处理成本,并可能触发“迷失在中间”问题。
    • Embedding Model: 选择在 MTEB (Massive Text Embedding Benchmark) 等榜单上表现好的模型,并考虑其是否针对你的应用领域(如代码、金融)。
  • 性能 / 显存 / 吞吐 的权衡:
    • Latency: Naïve RAG 延迟最低。增加 Reranker、Query Transformation 等高级 RAG 模块会显著增加端到端延迟。
    • Cost: Modular RAG 中多次调用 LLM(如用于规划、重写)会大幅增加 API 调用成本。
    • Offline vs. Online: 索引构建是昂贵的离线任务。实时更新索引(例如,对于新闻数据)是一个工程挑战,通常采用增量索引或混合索引策略。
  • 常见坑和调试技巧:
    • 检索质量差:
      • 调试: 单独评估检索模块的召回率和准确率。检查 Chunking 策略是否合理。可视化查询和文档向量的分布(用 t-SNE 或 UMAP)。
      • 解决: 尝试不同的 Chunking 策略,或微调 Embedding 模型。
    • LLM 忽略上下文:
      • 调试: 检查最终构建的 Prompt 格式是否清晰。是否将最重要的信息放在了 Prompt 的开头或结尾(缓解“迷失在中间”)。
      • 解决: 优化 Prompt 模板,使用更强的指令,或使用专为遵循指令而微调的模型。
    • 评估困难: RAG 系统的评估是复杂的,需要综合评估检索和生成两部分。使用 RAGAS、ARES 等框架,从 Faithfulness(忠实度)、Answer Relevance(答案相关性)、Context Relevance(上下文相关性)等多个维度进行评估。

常见误区与边界情况

  • 误区一:RAG 就是“给 Prompt 加点东西”
    • 这是对 RAG 的极大简化。一个生产级的 RAG 系统是一个复杂的工程,涉及数据预处理、索引、多模态检索、Reranking、Prompt 压缩、结果生成和端到端评估等一系列技术栈。
  • 误区二:有了 RAG 就不需要微调(Fine-tuning)了
    • RAG 和微调是互补的。RAG 负责提供“外部知识”,解决事实性、时效性问题;微调负责教授模型特定的“技能”或“风格”,如模仿特定口吻、遵循复杂指令、掌握领域术语。最佳实践常常是“RAG + Fine-tuning”。
  • 误区三:检索到的文档越多(top_k 越大)越好
    • 错误。过多的文档会引入噪声,干扰 LLM 的判断,并可能超出其上下文窗口限制。实验表明,top_k 超过一定数量后,性能甚至会下降。
  • 边界情况与失败模式:
    • 知识库中无答案: 这是 RAG 的关键测试点。系统不应幻化答案,而应明确表示“根据我有的资料,无法回答这个问题”。这需要通过精心设计的 Prompt 来引导。
    • 查询与文档语义鸿沟: 用户的提问方式和文档的表述方式可能差异巨大。高级 RAG 中的 Query Transformation 就是为了解决这个问题。
    • 信息冲突: 检索到的多个文档包含相互矛盾的信息。LLM 如何处理这种情况是一个开放性问题。目前的做法可能包括:信任排名最高的文档、让 LLM 自行判断、或向用户呈现所有信息。
  • 常见面试追问:
    • "如果你的 RAG 系统上线后,用户反馈答案不准确,你会从哪些方面去排查和优化?"
      • 回答要点: 分模块排查。首先,检查检索模块,看是否召回了正确的文档(离线评估)。其次,检查生成模块,看 Prompt 是否合理,LLM 是否正确利用了上下文。最后,考虑端到端评估,引入 RAGAS 等工具进行量化分析,并根据分析结果决定是优化 Chunking、微调 Embedding 模型,还是引入 Reranker
    • "如何评估一个 RAG 系统的好坏?"
      • 回答要点: 提到 RAGAS 等框架,并解释其核心评估维度:Faithfulness(生成内容是否基于上下文)、Answer Relevance(答案是否切题)、Context Precision/Recall(上下文是否精准且全面)。同时,也要结合业务指标(如用户满意度、采纳率)进行综合评估。
    • "RAG 如何处理实时性要求高的数据,比如新闻流?"
      • 回答要点: 讨论索引更新策略。可以有定期(如每小时)的全量或增量更新。对于极高实时性,可以设计一个双路系统:一路查询静态的、大规模的向量索引,另一路查询一个小的、包含最新数据的实时索引(如内存中的小 FAISS 索引或直接用 BM25 搜索),最后融合两路结果。
相关题目