RAG 的定义与核心组件(Retriever + Reranker + Generator)?
核心概念
检索增强生成(Retrieval-Augmented Generation, RAG)是一种将大型语言模型(LLM)的强大生成能力与外部知识库的实时、动态信息检索相结合的混合式 AI 架构。其核心思想是,在生成回答之前,首先从一个大规模的文档语料库(如维基百科、公司内部文档、数据库)中检索出与用户问题相关的最新、最准确的信息片段。然后,将这些检索到的信息作为上下文(Context)与原始问题一起提供给 LLM,引导模型生成基于事实、有据可查的回答,从而显著缓解 LLM 的“幻觉”问题、知识过时问题,并提供答案的可追溯性。
原理与推导
RAG 的工作流程可以被形式化地理解为一个三阶段的概率链式过程:检索(Retriever)、重排序(Reranker) 和 生成(Generator)。
从概率角度看,一个标准的生成模型试图最大化给定输入 后,输出序列 的概率 。而 RAG 引入了一个潜在变量(latent variable)——被检索到的文档集 ,其目标是建模 。整个过程可以看作是边缘化潜在文档 :
在实践中,我们无法对整个语料库求和,因此采用一种近似方法:首先用检索器找到一个最可能相关的文档子集 ,然后基于这个子集进行生成。
1. 检索器 (Retriever)
动机:从海量文档中快速、高效地召回一个与查询(Query)可能相关的候选文档集。此阶段优先考虑召回率(Recall)。
原理:最主流的方法是密集向量检索(Dense Passage Retrieval, DPR)。
-
离线索引(Offline Indexing):使用一个预训练的
Transformer模型(称为 Bi-Encoder)将知识库中所有的文档片段(Passages) 独立地编码成高维向量 。这些向量存储在一个专门的向量数据库(如 FAISS, Milvus)中,并建立索引以支持快速近邻搜索。其中 是文档编码器。
-
在线查询(Online Querying):当用户输入一个查询 时,使用另一个(通常结构相同或相似的)编码器 将其编码为查询向量 。
然后在向量数据库中,通过计算查询向量与所有文档向量的相似度(通常是余弦相似度或内积),找出 Top-k 个最相似的文档向量。
检索器的目标是返回 。
复杂度:
- 索引构建:,其中 是文档数量, 是向量维度。这是一次性离线成本。
- 查询(暴力搜索):,非常慢。
- 查询(使用近似最近邻 ANN 索引,如 HNSW): 或 ,非常快,是工程实践中的标准做法。
2. 重排序器 (Reranker)
动机:检索器为了速度牺牲了一部分精度,返回的 Top-k 文档中可能包含不那么相关的内容。重排序器使用一个更强大、更精细但计算成本更高的模型,对这 k 个候选文档进行重新打分和排序,以提高最终送入生成器上下文的精确率(Precision)。
原理:通常使用**跨编码器(Cross-Encoder)**模型。
- 与将查询和文档分开编码的 Bi-Encoder 不同,Cross-Encoder 将查询 和每个候选文档 拼接成一个单一的输入序列,例如
[CLS] q [SEP] d_i [SEP]。 - 这个拼接后的序列被完整地输入到一个
Transformer模型(如BERT)中。模型通过内部的自注意力机制,可以对查询和文档的词元(token)进行深度、细粒度的交互。 - 模型最终输出一个单一的分数 ,表示 与 的相关性。
- 根据分数 对 k 个文档进行降序排序,然后选择前 n 个()作为最终的上下文。
复杂度:
- ,其中 是候选文档数, 分别是查询和文档的长度。由于 通常不大(例如 20-100),这个计算开销是可接受的。
3. 生成器 (Generator)
动机:利用 LLM 强大的语言理解和生成能力,基于原始问题和经过筛选的高质量上下文,生成一个流畅、准确、符合逻辑的最终答案。
原理:这是一个标准的条件文本生成任务。
- 上下文构建(Prompting):将原始查询 和重排序后的 Top-n 个文档 组合成一个结构化的提示(Prompt)。格式至关重要,一个常见的模板如下:
text1请根据以下提供的上下文信息来回答问题。如果上下文中没有足够信息,请回答“根据现有信息无法回答”。2上下文:3---4[文档 d'_1 的内容]5---6[文档 d'_2 的内容]7...8---9问题:[原始查询 q]10回答:
- 自回归生成(Autoregressive Generation):LLM 以这个增强后的 Prompt 作为输入,逐个词元地生成答案序列 。在生成每个词元 时,模型会考虑整个 Prompt 和已经生成的词元序列 。 其中 是重排序后的文档集。
代码实现
下面是一个使用 sentence-transformers, faiss, 和 transformers 库实现的简化版 RAG 流程。
1import torch2import faiss3import numpy as np4from sentence_transformers import SentenceTransformer, CrossEncoder5from transformers import AutoTokenizer, AutoModelForSeq2SeqLM67# 确保有 GPU 可用,否则回退到 CPU8device = torch.device("cuda" if torch.cuda.is_available() else "cpu")9print(f"正在使用设备: {device}")1011# --- 0. 准备知识库 ---12knowledge_base = [13 "苹果公司是一家美国的跨国科技公司,总部位于加利福尼亚州的库比蒂诺。",14 "iPhone 是由苹果公司设计和销售的一系列智能手机。",15 "MacBook Pro 是苹果公司推出的一款高端笔记本电脑,于2006年首次发布。",16 "特斯拉是一家美国电动汽车和清洁能源公司,总部位于德克萨斯州的奥斯汀。",17 "马斯克是特斯拉的首席执行官,并以其在 SpaceX 和 Neuralink 的工作而闻名。",18 "2023年,苹果发布了其首款空间计算设备 Apple Vision Pro。",19]2021# --- 1. 检索器 (Retriever) ---22# 为什么使用 bi-encoder:为了高效地将查询和文档独立编码成向量,适用于大规模检索。23retriever_model = SentenceTransformer('moka-ai/m3e-base', device=device)2425# 离线编码知识库26print("正在编码知识库...")27db_embeddings = retriever_model.encode(knowledge_base, convert_to_numpy=True)2829# 为什么使用 FAISS:它是一个高效的向量相似性搜索库,可以处理海量向量数据。30# IndexFlatL2 使用 L2 距离(欧氏距离)进行精确搜索。31embedding_dim = db_embeddings.shape[1]32index = faiss.IndexFlatL2(embedding_dim)33index.add(db_embeddings)3435def retrieve(query: str, top_k: int):36 print(f"\n--- 检索阶段 (Top-{top_k}) ---")37 # 为什么要做 normalize:对于 L2 距离,归一化向量后的 L2 距离与余弦相似度等价,可以更好地度量方向。38 query_embedding = retriever_model.encode([query], convert_to_numpy=True)39 faiss.normalize_L2(query_embedding) # 在搜索前归一化查询向量4041 # 搜索最近的 k 个向量42 distances, indices = index.search(query_embedding, top_k)43 retrieved_docs = [knowledge_base[i] for i in indices[0]]4445 print("检索到的文档:")46 for doc in retrieved_docs:47 print(f"- {doc}")48 return retrieved_docs4950# --- 2. 重排序器 (Reranker) ---51# 为什么使用 cross-encoder:通过将查询和文档对一起输入模型,可以捕捉更深层次的交互,从而实现更精确的重排序。52reranker_model = CrossEncoder('BAAI/bge-reranker-base', max_length=512)5354def rerank(query: str, retrieved_docs: list, top_n: int):55 print(f"\n--- 重排序阶段 (Top-{top_n}) ---")56 # 为什么创建 pairs:Cross-Encoder 需要 (query, document) 对作为输入。57 pairs = [(query, doc) for doc in retrieved_docs]5859 # 预测分数60 scores = reranker_model.predict(pairs)6162 # 为什么 zip 和 sort:将分数与文档配对,然后按分数降序排序。63 doc_scores = list(zip(retrieved_docs, scores))64 doc_scores.sort(key=lambda x: x[1], reverse=True)6566 reranked_docs = [doc for doc, score in doc_scores[:top_n]]6768 print("重排序后的文档:")69 for doc, score in doc_scores[:top_n]:70 print(f"- (分数: {score:.2f}) {doc}")71 return reranked_docs7273# --- 3. 生成器 (Generator) ---74# 为什么使用 Seq2Seq 模型:像 T5 这样的模型非常适合问答和摘要任务。75generator_tokenizer = AutoTokenizer.from_pretrained("google/flan-t5-base")76generator_model = AutoModelForSeq2SeqLM.from_pretrained("google/flan-t5-base").to(device)7778def generate(query: str, reranked_docs: list):79 print("\n--- 生成阶段 ---")80 # 为什么构建这样的 prompt:这是指导 LLM 如何利用上下文的关键步骤,明确指示其任务和信息来源。81 context = "\n".join(reranked_docs)82 prompt = f"""83 根据以下信息回答问题。8485 信息:86 {context}8788 问题: {query}89 """9091 print(f"构建的 Prompt:\n{prompt}")9293 # 为什么用 tokenizer:将文本转换为模型可以理解的数字 ID。94 inputs = generator_tokenizer(prompt, return_tensors="pt", max_length=1024, truncation=True).to(device)9596 # 生成答案97 outputs = generator_model.generate(**inputs, max_length=200, num_beams=5, early_stopping=True)9899 # 为什么用 tokenizer.decode:将模型输出的数字 ID 转换回人类可读的文本。100 answer = generator_tokenizer.decode(outputs[0], skip_special_tokens=True)101102 print("\n生成的答案:")103 print(answer)104 return answer105106# --- 主流程 ---107if __name__ == "__main__":108 query = "苹果公司最新发布了什么计算设备?"109110 # 1. 检索111 retrieved_docs = retrieve(query, top_k=3)112113 # 2. 重排序114 reranked_docs = rerank(query, retrieved_docs, top_n=2)115116 # 3. 生成117 final_answer = generate(query, reranked_docs)
工程实践
-
使用场景:
- 企业级智能客服:基于最新的产品手册、FAQ 和历史工单回答客户问题。
- 内部知识库查询:允许员工查询公司内部的技术文档、规章制度、项目报告。
- 法律/金融/医疗分析助手:基于海量的专业文献、法规条款、财报数据提供分析和摘要。
- 代码生成与解释:结合代码库和 API 文档,生成符合规范的代码或解释现有代码。
-
超参数选择经验:
- 文档分块(
Chunking):这是 RAG 系统性能的基石。chunk_size通常在 256-512 个 token 之间。chunk_overlap(如 50-100 token)可以防止关键信息在块的边界被切断。 - Retriever
top_k:通常设置为 10 到 20。太小可能错过关键信息(低召回率),太大则会引入过多噪声,增加Reranker的负担。 Rerankertop_n:通常设置为 2 到 5。这个数量决定了送入生成器上下文的质量和长度。需要权衡信息密度与 LLM 上下文窗口长度限制及处理成本。
- 文档分块(
-
性能/显存/吞吐的权衡:
- Retriever:向量数据库的索引类型是关键。HNSW 等 ANN 索引在牺牲极小的精度下,可实现毫秒级查询。
Embedding模型的大小也影响显存和速度。 Reranker:是可选组件。不用它,延迟更低,但答案质量可能下降。使用它,会增加几十到几百毫秒的延迟。- Generator:是最大的性能瓶颈。模型越大,生成质量越好,但推理速度越慢,显存占用越高。使用量化(Quantization)、剪枝(Pruning)或更小的模型可以提升速度。流式生成(Streaming)可以显著改善用户感知的首字延迟。
- Retriever:向量数据库的索引类型是关键。HNSW 等 ANN 索引在牺牲极小的精度下,可实现毫秒级查询。
-
常见坑和调试技巧:
- “大海捞针”问题:如果知识库非常庞大且异构,简单的向量相似度可能不足以找到正确信息。需要结合关键词搜索(稀疏检索,如
BM25)和向量搜索(密集检索)的混合检索(Hybrid Search)。 - “Lost in the Middle”:LLM 对长上下文的注意力并非均匀分布,往往更关注开头和结尾的内容。调试时,可以尝试将被
Reranker评为最高分的文档放在 Prompt 的最前面或最后面。 - 评估困难:RAG 的端到端评估很难。需要建立组件化评估流水线:单独评估 Retriever (Recall@k, MRR)、
Reranker(Precision@n),以及 Generator (答案与事实的一致性、相关性)。Ragas 等框架提供了评估 RAG 质量的指标。
- “大海捞针”问题:如果知识库非常庞大且异构,简单的向量相似度可能不足以找到正确信息。需要结合关键词搜索(稀疏检索,如
常见误区与边界情况
-
误区1:RAG 就是给 LLM 喂数据
- 正解:RAG 是一个复杂的系统工程,而非简单的 Prompt 拼接。它包括数据预处理(分块)、向量化、索引、检索、重排序等多个环节,每个环节都对最终效果有巨大影响。它是一个“开卷考试”系统,而不是“考前辅导”的微调。
-
误区2:检索到的上下文越多越好
- 正解:过多的上下文会引入噪声,分散 LLM 的注意力,甚至可能因为包含矛盾信息而导致模型“困惑”。上下文的质量远比数量重要,这也是
Reranker存在的价值。
- 正解:过多的上下文会引入噪声,分散 LLM 的注意力,甚至可能因为包含矛盾信息而导致模型“困惑”。上下文的质量远比数量重要,这也是
-
边界情况1:检索不到相关信息
- 当用户的查询与知识库内容完全无关时,Retriever 仍会返回最“相似”的(但实际不相关的)文档。这会误导 Generator 产生错误的答案。
- 应对策略:1) 在
Reranker阶段设置一个分数阈值,低于该阈值的文档直接丢弃。2) 在 Generator 的 Prompt 中明确指示,如果上下文信息不足以回答问题,就直接声明“无法回答”。
-
边界情况2:检索到矛盾信息
- 知识库中可能存在关于同一主题的多个相互矛盾的文档(例如,不同时间点的报告)。
- 应对策略:这是一个前沿研究问题。目前的 LLM 可能会随机选择一个信息源,或者尝试进行综合但可能出错。可以在 Prompt 中引导模型指出信息来源和存在的矛盾,例如:“根据文档A...,而根据文档B...”。
-
常见面试追问:
- 问:RAG 和模型微调(Fine-tuning)有什么区别和联系?
- 答:它们解决的问题不同。RAG 是为了解决知识获取问题,通过外部知识库为 LLM 提供动态、实时的信息。微调是为了解决能力适应问题,通过在特定任务的数据上训练,教会模型新的技能、格式或风格(如学会以客服的口吻说话)。它们可以结合使用:先对一个基础模型进行微调,使其更擅长遵循指令和利用上下文,然后将这个微调过的模型作为 RAG 系统中的 Generator。
- 问:如何更新 RAG 系统中的知识?
- 答:这涉及到更新向量数据库。对于新增知识,只需将其编码后增量添加到索引中。对于修改或删除的知识,需要先从索引中删除旧的向量,再添加新的向量(如果适用)。这要求向量数据库支持高效的删除和更新操作。定期对整个知识库进行重新索引也是保持数据新鲜度的常用策略。
- 问:如果你的 RAG 系统回答很慢,你会从哪里开始优化?
- 答:首先用日志和计时器分析三个组件(Retriever,
Reranker, Generator)各自的耗时。通常 Generator 是瓶颈。优化方向包括:1) Generator:使用更小的模型、模型量化、或优化推理引擎(如vLLM)。2)Reranker:如果耗时显著,可以考虑使用更小的Reranker模型或减少送入Reranker的k值。3) Retriever:检查 ANN 索引的配置是否最优,网络延迟是否过高。
- 答:首先用日志和计时器分析三个组件(Retriever,
- 问:RAG 和模型微调(Fine-tuning)有什么区别和联系?