Pre-retrieval / Retrieval / Post-retrieval / Generation 四阶段优化点?
核心概念
Retrieval-Augmented Generation (RAG) 是一种结合了信息检索(Retrieval)与大语言模型生成(Generation)能力的技术框架。其核心思想是在回答问题或生成文本前,先从一个大规模的知识库(如文档、网页、数据库)中检索出相关的上下文信息,然后将这些信息与原始问题一同输入给语言模型,指导其生成更准确、更具事实依据的回答。RAG 的优化可以分为四个关键阶段:Pre-retrieval(检索前处理)、Retrieval(检索)、Post-retrieval(检索后处理)和 Generation(生成),每个阶段都提供了独特的优化杠杆。
原理与推导
1. Pre-retrieval(检索前处理)
此阶段关注于优化输入给检索系统的数据本身,即知识库的构建和处理。"Garbage in, garbage out",高质量的知识库是 RAG 系统性能的基石。
-
核心技术:数据清洗与分块 (
Chunking)- 数据清洗: 移除无关信息(如 HTML 标签、广告、页眉页脚)、修正格式错误、统一编码等。
- 分块 (
Chunking): 将长文档切分成更小、更易于检索的语义单元。- 固定大小分块 (Fixed-size
Chunking): 按固定字符数或 Token 数切分,通常带有重叠(Overlap)以避免语义割裂。简单高效,但可能切断完整的语义单元。 - 递归/语义分块 (Recursive/Semantic
Chunking): 优先使用语义边界(如段落\n\n、句子.)进行切分。当切分后的块仍然过大时,再递归地使用下一级分隔符。这能更好地保持语义完整性。 - 命题分块 (Propositional
Chunking): 将文档分解为一系列原子性的事实陈述或命题,每个命题作为一个独立的检索单元。这能提供更精确、更集中的上下文。
- 固定大小分块 (Fixed-size
-
核心技术:元数据与索引增强
- 元数据 (Metadata): 为每个数据块附加结构化信息,如文档来源、创建日期、章节标题等。这些元数据可以在检索阶段用于过滤,提高检索效率和精度。
- 索引增强 (Index Enhancement): 不仅仅索引原文,还可以索引原文的摘要、关键词、假设性问题等。例如,可以为每个数据块生成一个它可能回答的问题,并将这个问题与数据块一同索引。这有助于弥合查询和文档之间的语义鸿沟。
2. Retrieval(检索)
此阶段的目标是从海量数据块中,根据用户查询(Query)快速、准确地找出最相关的 Top-K 个数据块。
- 核心技术:混合检索 (Hybrid Search)
-
稠密检索 (Dense Retrieval): 基于深度学习模型(如 Sentence-BERT)将查询和数据块映射到高维向量空间。通过计算向量间的相似度(通常是余弦相似度或点积)来衡量相关性。
- 数学原理: 给定查询向量 和文档块向量 ,其相关性分数 为:
- 优势: 能捕捉语义相似性,即使关键词不完全匹配。
- 劣势: 对关键词精确匹配不敏感,且依赖 embedding 模型的质量。
-
稀疏检索 (Sparse Retrieval): 基于关键词匹配的传统信息检索方法,如
BM25。- 数学原理:
BM25算法为每个查询词 计算一个分数,综合考虑了词频(TF)、逆文档频率(IDF)和文档长度。 其中, 是词 在文档 中的频率, 是逆文档频率, 是文档长度, 是平均文档长度, 和 是超参数。 - 优势: 对关键词匹配非常高效和准确。
- 劣势: 无法理解同义词或语义相近的表达。
- 数学原理:
-
混合检索: 结合稠密和稀疏检索的优点。常用方法是分别计算两种分数,然后通过加权求和或更复杂的融合算法(如 Reciprocal Rank Fusion, RRF)得到最终排名。
- RRF 算法: 不依赖于分数本身的大小,而是依赖于排名。对于每个文档 ,其 RRF 分数为: 其中 是文档 在第 个检索结果列表中的排名, 是一个常数(通常为 60)。
-
3. Post-retrieval(检索后处理)
在获得初步的 Top-K 检索结果后,此阶段旨在对其进行精炼,以提高送入生成模型的上下文质量。
-
核心技术:重排 (Re-ranking)
- 原理: 使用一个更强大但计算成本更高的模型(通常是 Cross-Encoder)对初步检索到的 Top-K 结果进行重新排序。与在检索阶段使用的 Bi-Encoder(分别为 query 和 document 编码)不同,Cross-Encoder 将
(query, document)对同时输入模型,允许更深层次的交互和更精确的相关性判断。 - 复杂度: Bi-Encoder 检索复杂度为 (其中 N 是文档数,Q 是查询数,D 是向量维度),而 Cross-Encoder 重排复杂度为 (其中 K 是重排数量,L 是序列长度),因此它只适用于小规模的 K。
- 原理: 使用一个更强大但计算成本更高的模型(通常是 Cross-Encoder)对初步检索到的 Top-K 结果进行重新排序。与在检索阶段使用的 Bi-Encoder(分别为 query 和 document 编码)不同,Cross-Encoder 将
-
核心技术:上下文压缩 (Context Compression)
- 原理: 识别并移除检索到的数据块中的冗余或无关信息,只保留与查询最直接相关的句子。这可以在不丢失关键信息的前提下,将更多的文档块内容塞进 LLM 有限的上下文窗口中,同时减少噪声干扰。
- 方法: 可以使用小型语言模型(如
LLMLingua)或基于规则的方法(如 extractive summarization)来实现。
4. Generation(生成)
此阶段是 RAG 的最后一环,LLM 利用处理后的上下文信息来生成最终的回答。
-
核心技术:提示工程 (Prompt Engineering)
- 原理: 设计结构化的提示词(Prompt),清晰地指导 LLM 如何利用提供的上下文。一个典型的 RAG Prompt 结构如下:
text1基于以下上下文信息,请简洁地回答用户的问题。如果上下文没有提供相关信息,请回答“根据我所掌握的信息,无法回答该问题”。23[上下文]4{retrieved_context}56[问题]7{user_query}89[回答]- 优化: 调整指令、上下文和问题的顺序、加入 few-shot 示例等,都可以显著影响生成质量。
-
核心技术:生成器微调 (Generator Fine-tuning)
- 原理: 在特定任务的数据集上对生成器 LLM 进行微调。数据集通常包含
(context, query, ideal_answer)三元组。微调可以使 LLM 更好地适应特定领域的语言风格、遵循指令,以及更有效地从上下文中合成答案。 - 优势: 相比纯提示工程,微调能带来更根本、更稳定的性能提升,尤其是在处理特定格式或需要深度推理的任务时。
- 原理: 在特定任务的数据集上对生成器 LLM 进行微调。数据集通常包含
代码实现
下面是一个简化的、端到端的 RAG 流程示例,使用 NumPy 和 Scikit-learn 模拟各个阶段的优化点。
1import numpy as np2from sklearn.feature_extraction.text import TfidfVectorizer3from sklearn.metrics.pairwise import cosine_similarity45# --- 0. 模拟知识库 ---6documents = [7 "人工智能(AI)是模拟人类智能的科学与工程。主要领域包括机器学习、自然语言处理和计算机视觉。",8 "机器学习是人工智能的一个分支,它使计算机能够从数据中学习,而无需进行显式编程。常见算法有线性回归和支持向量机。",9 "自然语言处理(NLP)旨在让计算机理解和生成人类语言。BERT和GPT是NLP领域的两个著名模型。",10 "BERT是一种基于Transformer的语言表示模型,由Google开发,擅长理解上下文。",11 "GPT,即生成式预训练Transformer,由OpenAI开发,在文本生成方面表现出色。",12 "RAG结合了检索和生成,通过外部知识库增强大型语言模型的能力,以减少幻觉并提高事实准确性。"13]1415# --- 1. Pre-retrieval 阶段 ---16def chunk_text(docs, chunk_size=50, overlap=10):17 """18 一个简单的固定大小分块函数19 为什么这样做: 将长文档切分为可管理的、语义集中的小块,是构建有效检索索引的基础。20 """21 chunks = []22 metadata = []23 for i, doc in enumerate(docs):24 for start in range(0, len(doc), chunk_size - overlap):25 end = start + chunk_size26 chunks.append(doc[start:end])27 metadata.append({"source_doc_id": i}) # 为每个块添加元数据28 return chunks, metadata2930chunks, metadata = chunk_text(documents)31print(f"--- 1. Pre-retrieval: 文档被切分为 {len(chunks)} 个块 ---")32# print(chunks[0], metadata[0])333435# --- 2. Retrieval 阶段 ---36class HybridRetriever:37 def __init__(self, chunks):38 self.chunks = chunks39 # 为什么这样做: 初始化稀疏和稠密检索器。这是混合检索的基础。40 # 稀疏检索器 (TF-IDF)41 self.tfidf_vectorizer = TfidfVectorizer()42 self.sparse_embeddings = self.tfidf_vectorizer.fit_transform(self.chunks)4344 # 稠密检索器 (模拟的 Embedding 模型)45 self.embedding_dim = 16 # 实际场景中维度通常是 768 或更高46 self.dense_embeddings = np.random.rand(len(chunks), self.embedding_dim)47 # 为什么这样做: 实际项目中,这里会用一个预训练的 embedding 模型 (如 M3E, BGE) 来生成向量。48 # 这里用随机向量来模拟这个过程。49 self.dense_embeddings /= np.linalg.norm(self.dense_embeddings, axis=1, keepdims=True)5051 def search(self, query, top_k=5, alpha=0.5):52 """53 执行混合检索54 为什么这样做: 结合稀疏检索的关键词匹配能力和稠密检索的语义理解能力,获得更鲁棒的检索结果。55 """56 # 稀疏检索57 query_sparse = self.tfidf_vectorizer.transform([query])58 sparse_scores = cosine_similarity(query_sparse, self.sparse_embeddings).flatten()5960 # 稠密检索61 query_dense = np.random.rand(1, self.embedding_dim) # 模拟查询向量62 query_dense /= np.linalg.norm(query_dense)63 dense_scores = cosine_similarity(query_dense, self.dense_embeddings).flatten()6465 # 混合分数 (简单加权)66 # 为什么这样做: alpha 是一个超参数,用于平衡两种检索方式的重要性。67 hybrid_scores = alpha * dense_scores + (1 - alpha) * sparse_scores6869 # 获取 top_k 结果70 top_k_indices = np.argsort(hybrid_scores)[-top_k:][::-1]71 return [(self.chunks[i], hybrid_scores[i]) for i in top_k_indices]7273retriever = HybridRetriever(chunks)74query = "什么是RAG?"75retrieved_results = retriever.search(query, top_k=3)76print(f"\n--- 2. Retrieval: 检索到 Top-{len(retrieved_results)} 个结果 ---")77for chunk, score in retrieved_results:78 print(f" - [分数: {score:.4f}] {chunk}")798081# --- 3. Post-retrieval 阶段 ---82def rerank_and_compress(query, results):83 """84 模拟重排和压缩过程85 """86 # 重排 (Re-ranking)87 # 为什么这样做: 使用更精细的模型(此处用规则模拟)对初步结果重排序,将最相关的放在最前面。88 # 实际场景会用 Cross-Encoder 模型。这里我们简单地给包含 "RAG" 的块更高权重。89 reranked_scores = []90 for chunk, score in results:91 new_score = score92 if "RAG" in chunk:93 new_score += 0.2 # 模拟 Cross-Encoder 发现更强的相关性94 reranked_scores.append(new_score)9596 sorted_indices = np.argsort(reranked_scores)[::-1]97 reranked_results = [results[i] for i in sorted_indices]9899 # 压缩 (Compression)100 # 为什么这样做: 移除上下文中的噪声,让LLM更专注于核心信息。101 # 这里简单地只保留重排后 Top-1 的结果作为最终上下文。102 final_context = reranked_results[0][0]103104 return final_context, reranked_results105106final_context, reranked_results = rerank_and_compress(query, retrieved_results)107print(f"\n--- 3. Post-retrieval: 重排后的结果 ---")108for chunk, _ in reranked_results:109 print(f" - {chunk}")110print(f"压缩后的最终上下文: '{final_context}'")111112113# --- 4. Generation 阶段 ---114def generate_answer(query, context):115 """116 模拟 LLM 生成答案117 为什么这样做: 这是 RAG 的最后一步,通过精心设计的 Prompt,指导 LLM 基于提供的上下文生成答案。118 """119 prompt = f"""120 基于以下上下文信息,请简洁地回答用户的问题。121122 [上下文]123 {context}124125 [问题]126 {query}127128 [回答]129 """130 # 在实际应用中,这里会调用一个 LLM API (如 OpenAI, Anthropic)131 # 这里我们用一个简单的规则来模拟生成过程132 if "RAG" in context:133 simulated_answer = "RAG是一种结合了检索和生成的技术,它通过从外部知识库获取信息来增强大型语言模型的能力,目的是减少幻觉并提升事实准确性。"134 else:135 simulated_answer = "根据我所掌握的信息,无法准确回答该问题。"136137 print("\n--- 4. Generation: 生成最终答案 ---")138 print(f"生成的 Prompt:\n{prompt}")139 print(f"模拟的 LLM 回答:\n{simulated_answer}")140 return simulated_answer141142generate_answer(query, final_context)
工程实践
- Pre-retrieval:
- 场景: 处理复杂、非结构化的文档(如 PDF 财报、HTML 网页)时至关重要。
- 经验法则: Chunk size 一般设在 256-512 tokens 之间,overlap 设为 chunk size 的 10-20%。对于代码或结构化文本,使用递归/语义分块效果更好。
- 权衡: Chunk size 太小,上下文不完整;太大,噪声过多,检索不精确。需要根据文档特性和查询类型进行实验。
- Retrieval:
- 场景: 几乎所有 RAG 系统都需要。混合检索对于需要同时理解语义和匹配特定术语(如产品名、代码函数名)的场景特别有效。
- 经验法则: 混合检索的权重 通常从 0.5 开始,然后根据评测集效果进行调整。对于法律、医疗等专业领域,微调 embedding 模型能显著提升检索效果。
- 性能: 向量数据库(如 FAISS, Milvus, Pinecone)使用 HNSW 等近似最近邻算法,可以在牺牲极少量召回率的情况下,实现海量数据(亿级)的毫秒级查询。
- Post-retrieval:
- 场景: 在对答案质量要求极高,且能容忍一定延迟(~100-500ms)的场景中使用,如面向客户的问答机器人。
- 经验法则: 初步检索 Top 20-50 个文档,然后用 Cross-Encoder 重排 Top 5-10 个。这样可以在性能和质量间取得良好平衡。
- 权衡: Re-ranking 显著增加延迟和计算成本,但通常能带来 5-15% 的准确率提升。上下文压缩同样增加延迟,但能有效利用 LLM 的上下文窗口,特别是在处理多个长文档时。
- Generation:
- 场景: 所有 RAG 系统都需要。当通用 LLM 无法很好地遵循指令或合成上下文时,需要进行微调。
- 经验法则: 先从优化 Prompt 开始,这是成本最低、见效最快的方法。只有当 Prompt Engineering 达到瓶颈时,才考虑微调。微调需要高质量的
(context, query, answer)数据集,至少需要几百到几千条样本。 - 调试技巧: 如果 LLM 忽略上下文,尝试在 Prompt 中加强指令,如“你必须只使用提供的上下文来回答”。如果 LLM 只是复述上下文,尝试要求它“以你自己的话总结并回答”。
常见误区与边界情况
-
误区1:
Embedding模型是万能的。- 辨析: 通用 embedding 模型(如 M3E-base)在通用领域表现良好,但在专业领域(如金融、法律)可能无法很好地理解术语。此时,需要领域微调(Domain-specific Fine-tuning)。此外,它们对关键词的敏感度不如稀疏检索。
- 追问: "如果你的 RAG 在一个新领域的表现不好,你会从哪里开始排查?"
- 回答要点: 首先检查 Pre-retrieval 的分块策略是否合理。然后,评估 Retrieval 阶段,使用评测集(如
mAP,NDCG@k)分别测试稠密、稀疏和混合检索的效果,判断是否是 embedding 模型或关键词匹配出了问题。最后才考虑生成阶段的 Prompt 或模型问题。
-
误区2:检索到的文档越多越好。
- 辨析: LLM 的上下文窗口是有限的,并且存在“迷失在中间”(Lost in the Middle)的问题,即模型更关注上下文的开头和结尾部分。过多的文档会引入噪声,稀释关键信息,反而降低生成质量。
- 追问: "如何确定最佳的 Top-K 值?"
- 回答要点: 这是一个需要实验确定的超参数。可以构建一个评测集,测试不同的 K 值(如 3, 5, 10)对最终答案质量的影响。同时,结合 Post-retrieval 的重排和压缩技术,可以在送入更多信息的同时,将最相关的部分放在最突出的位置(开头或结尾),缓解“迷失在中间”的问题。
-
误区3:直接用 Cross-Encoder 做检索。
- 辨析: Cross-Encoder 的计算复杂度使其无法用于大规模文档的初步检索。它需要对每个
(query, document)对都进行一次完整的前向传播。在一个有百万文档的库中,这是不可行的。它的正确用法是在一个小候选集上进行重排。
- 辨析: Cross-Encoder 的计算复杂度使其无法用于大规模文档的初步检索。它需要对每个
-
边界情况:无答案问题 (No-Answer Questions)
- 问题: 当知识库中不包含问题的答案时,RAG 系统很容易“幻化”出一个看似合理的错误答案。
- 处理:
- 检索阶段: 如果检索到的所有文档分数都非常低,可以提前终止,判断为无法回答。
- 生成阶段: 在 Prompt 中明确指示,如果上下文中没有答案,就直接回答“无法回答”。
- 模型微调: 在微调数据中加入大量无答案的样本,训练模型识别这种情况并输出特定回复。
- §1.2Naive RAG / Advanced RAG / Modular RAG / Agentic RAG 的演进路径?→
- §1.2RAG Survey(Tongji 2024、HKU 2024)的分类体系?→
- §1.2From RAG to Context Engineering(2025 末)的范式转变?→
- §1.2Reasoning-Intensive Retrieval(BRIGHT 基准)的挑战?→
- §1.1RAG 的定义与核心组件(Retriever + Reranker + Generator)?→
- §1.1RAG vs 长上下文 vs 微调 vs Prompt Caching 的决策矩阵?→