§1.2.9

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): 将文档分解为一系列原子性的事实陈述或命题,每个命题作为一个独立的检索单元。这能提供更精确、更集中的上下文。
  • 核心技术:元数据与索引增强

    • 元数据 (Metadata): 为每个数据块附加结构化信息,如文档来源、创建日期、章节标题等。这些元数据可以在检索阶段用于过滤,提高检索效率和精度。
    • 索引增强 (Index Enhancement): 不仅仅索引原文,还可以索引原文的摘要、关键词、假设性问题等。例如,可以为每个数据块生成一个它可能回答的问题,并将这个问题与数据块一同索引。这有助于弥合查询和文档之间的语义鸿沟。

2. Retrieval(检索)

此阶段的目标是从海量数据块中,根据用户查询(Query)快速、准确地找出最相关的 Top-K 个数据块。

  • 核心技术:混合检索 (Hybrid Search)
    • 稠密检索 (Dense Retrieval): 基于深度学习模型(如 Sentence-BERT)将查询和数据块映射到高维向量空间。通过计算向量间的相似度(通常是余弦相似度或点积)来衡量相关性。

      • 数学原理: 给定查询向量 vqv_q 和文档块向量 vdv_d,其相关性分数 SdenseS_{dense} 为: Sdense(q,d)=cos(θ)=vqvdvqvdS_{dense}(q, d) = \cos(\theta) = \frac{v_q \cdot v_d}{\|v_q\| \|v_d\|}
      • 优势: 能捕捉语义相似性,即使关键词不完全匹配。
      • 劣势: 对关键词精确匹配不敏感,且依赖 embedding 模型的质量。
    • 稀疏检索 (Sparse Retrieval): 基于关键词匹配的传统信息检索方法,如 BM25

      • 数学原理: BM25 算法为每个查询词 qiq_i 计算一个分数,综合考虑了词频(TF)、逆文档频率(IDF)和文档长度。 Ssparse(q,d)=i=1nIDF(qi)f(qi,d)(k1+1)f(qi,d)+k1(1b+bdavgdl)S_{sparse}(q, d) = \sum_{i=1}^{n} \text{IDF}(q_i) \cdot \frac{f(q_i, d) \cdot (k_1 + 1)}{f(q_i, d) + k_1 \cdot (1 - b + b \cdot \frac{|d|}{\text{avgdl}})} 其中,f(qi,d)f(q_i, d) 是词 qiq_i 在文档 dd 中的频率,IDF(qi)\text{IDF}(q_i) 是逆文档频率,d|d| 是文档长度,avgdl\text{avgdl} 是平均文档长度,k1k_1bb 是超参数。
      • 优势: 对关键词匹配非常高效和准确。
      • 劣势: 无法理解同义词或语义相近的表达。
    • 混合检索: 结合稠密和稀疏检索的优点。常用方法是分别计算两种分数,然后通过加权求和或更复杂的融合算法(如 Reciprocal Rank Fusion, RRF)得到最终排名。

      • RRF 算法: 不依赖于分数本身的大小,而是依赖于排名。对于每个文档 dd,其 RRF 分数为: RRFScore(d)=i{dense,sparse}1k+ranki(d)\text{RRFScore}(d) = \sum_{i \in \{\text{dense}, \text{sparse}\}} \frac{1}{k + \text{rank}_i(d)} 其中 ranki(d)\text{rank}_i(d) 是文档 dd 在第 ii 个检索结果列表中的排名,kk 是一个常数(通常为 60)。

3. Post-retrieval(检索后处理)

在获得初步的 Top-K 检索结果后,此阶段旨在对其进行精炼,以提高送入生成模型的上下文质量。

  • 核心技术:重排 (Re-ranking)

    • 原理: 使用一个更强大但计算成本更高的模型(通常是 Cross-Encoder)对初步检索到的 Top-K 结果进行重新排序。与在检索阶段使用的 Bi-Encoder(分别为 query 和 document 编码)不同,Cross-Encoder 将 (query, document) 对同时输入模型,允许更深层次的交互和更精确的相关性判断。
    • 复杂度: Bi-Encoder 检索复杂度为 O(ND+QD)O(N \cdot D + Q \cdot D)(其中 N 是文档数,Q 是查询数,D 是向量维度),而 Cross-Encoder 重排复杂度为 O(KLqLd)O(K \cdot L_q \cdot L_d)(其中 K 是重排数量,L 是序列长度),因此它只适用于小规模的 K。
  • 核心技术:上下文压缩 (Context Compression)

    • 原理: 识别并移除检索到的数据块中的冗余或无关信息,只保留与查询最直接相关的句子。这可以在不丢失关键信息的前提下,将更多的文档块内容塞进 LLM 有限的上下文窗口中,同时减少噪声干扰。
    • 方法: 可以使用小型语言模型(如 LLMLingua)或基于规则的方法(如 extractive summarization)来实现。

4. Generation(生成)

此阶段是 RAG 的最后一环,LLM 利用处理后的上下文信息来生成最终的回答。

  • 核心技术:提示工程 (Prompt Engineering)

    • 原理: 设计结构化的提示词(Prompt),清晰地指导 LLM 如何利用提供的上下文。一个典型的 RAG Prompt 结构如下:
    text
    1基于以下上下文信息,请简洁地回答用户的问题。如果上下文没有提供相关信息,请回答“根据我所掌握的信息,无法回答该问题”。
    2
    3[上下文]
    4{retrieved_context}
    5
    6[问题]
    7{user_query}
    8
    9[回答]
    • 优化: 调整指令、上下文和问题的顺序、加入 few-shot 示例等,都可以显著影响生成质量。
  • 核心技术:生成器微调 (Generator Fine-tuning)

    • 原理: 在特定任务的数据集上对生成器 LLM 进行微调。数据集通常包含 (context, query, ideal_answer) 三元组。微调可以使 LLM 更好地适应特定领域的语言风格、遵循指令,以及更有效地从上下文中合成答案。
    • 优势: 相比纯提示工程,微调能带来更根本、更稳定的性能提升,尤其是在处理特定格式或需要深度推理的任务时。

代码实现

下面是一个简化的、端到端的 RAG 流程示例,使用 NumPy 和 Scikit-learn 模拟各个阶段的优化点。

python
1import numpy as np
2from sklearn.feature_extraction.text import TfidfVectorizer
3from sklearn.metrics.pairwise import cosine_similarity
4
5# --- 0. 模拟知识库 ---
6documents = [
7 "人工智能(AI)是模拟人类智能的科学与工程。主要领域包括机器学习、自然语言处理和计算机视觉。",
8 "机器学习是人工智能的一个分支,它使计算机能够从数据中学习,而无需进行显式编程。常见算法有线性回归和支持向量机。",
9 "自然语言处理(NLP)旨在让计算机理解和生成人类语言。BERT和GPT是NLP领域的两个著名模型。",
10 "BERT是一种基于Transformer的语言表示模型,由Google开发,擅长理解上下文。",
11 "GPT,即生成式预训练Transformer,由OpenAI开发,在文本生成方面表现出色。",
12 "RAG结合了检索和生成,通过外部知识库增强大型语言模型的能力,以减少幻觉并提高事实准确性。"
13]
14
15# --- 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_size
26 chunks.append(doc[start:end])
27 metadata.append({"source_doc_id": i}) # 为每个块添加元数据
28 return chunks, metadata
29
30chunks, metadata = chunk_text(documents)
31print(f"--- 1. Pre-retrieval: 文档被切分为 {len(chunks)} 个块 ---")
32# print(chunks[0], metadata[0])
33
34
35# --- 2. Retrieval 阶段 ---
36class HybridRetriever:
37 def __init__(self, chunks):
38 self.chunks = chunks
39 # 为什么这样做: 初始化稀疏和稠密检索器。这是混合检索的基础。
40 # 稀疏检索器 (TF-IDF)
41 self.tfidf_vectorizer = TfidfVectorizer()
42 self.sparse_embeddings = self.tfidf_vectorizer.fit_transform(self.chunks)
43
44 # 稠密检索器 (模拟的 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)
50
51 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()
59
60 # 稠密检索
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()
64
65 # 混合分数 (简单加权)
66 # 为什么这样做: alpha 是一个超参数,用于平衡两种检索方式的重要性。
67 hybrid_scores = alpha * dense_scores + (1 - alpha) * sparse_scores
68
69 # 获取 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]
72
73retriever = 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}")
79
80
81# --- 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 = score
92 if "RAG" in chunk:
93 new_score += 0.2 # 模拟 Cross-Encoder 发现更强的相关性
94 reranked_scores.append(new_score)
95
96 sorted_indices = np.argsort(reranked_scores)[::-1]
97 reranked_results = [results[i] for i in sorted_indices]
98
99 # 压缩 (Compression)
100 # 为什么这样做: 移除上下文中的噪声,让LLM更专注于核心信息。
101 # 这里简单地只保留重排后 Top-1 的结果作为最终上下文。
102 final_context = reranked_results[0][0]
103
104 return final_context, reranked_results
105
106final_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}'")
111
112
113# --- 4. Generation 阶段 ---
114def generate_answer(query, context):
115 """
116 模拟 LLM 生成答案
117 为什么这样做: 这是 RAG 的最后一步,通过精心设计的 Prompt,指导 LLM 基于提供的上下文生成答案。
118 """
119 prompt = f"""
120 基于以下上下文信息,请简洁地回答用户的问题。
121
122 [上下文]
123 {context}
124
125 [问题]
126 {query}
127
128 [回答]
129 """
130 # 在实际应用中,这里会调用一个 LLM API (如 OpenAI, Anthropic)
131 # 这里我们用一个简单的规则来模拟生成过程
132 if "RAG" in context:
133 simulated_answer = "RAG是一种结合了检索和生成的技术,它通过从外部知识库获取信息来增强大型语言模型的能力,目的是减少幻觉并提升事实准确性。"
134 else:
135 simulated_answer = "根据我所掌握的信息,无法准确回答该问题。"
136
137 print("\n--- 4. Generation: 生成最终答案 ---")
138 print(f"生成的 Prompt:\n{prompt}")
139 print(f"模拟的 LLM 回答:\n{simulated_answer}")
140 return simulated_answer
141
142generate_answer(query, final_context)

工程实践

  • Pre-retrieval:
    • 场景: 处理复杂、非结构化的文档(如 PDF 财报、HTML 网页)时至关重要。
    • 经验法则: Chunk size 一般设在 256-512 tokens 之间,overlap 设为 chunk size 的 10-20%。对于代码或结构化文本,使用递归/语义分块效果更好。
    • 权衡: Chunk size 太小,上下文不完整;太大,噪声过多,检索不精确。需要根据文档特性和查询类型进行实验。
  • Retrieval:
    • 场景: 几乎所有 RAG 系统都需要。混合检索对于需要同时理解语义和匹配特定术语(如产品名、代码函数名)的场景特别有效。
    • 经验法则: 混合检索的权重 α\alpha 通常从 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) 对都进行一次完整的前向传播。在一个有百万文档的库中,这是不可行的。它的正确用法是在一个小候选集上进行重排。
  • 边界情况:无答案问题 (No-Answer Questions)

    • 问题: 当知识库中不包含问题的答案时,RAG 系统很容易“幻化”出一个看似合理的错误答案。
    • 处理:
      1. 检索阶段: 如果检索到的所有文档分数都非常低,可以提前终止,判断为无法回答。
      2. 生成阶段: 在 Prompt 中明确指示,如果上下文中没有答案,就直接回答“无法回答”。
      3. 模型微调: 在微调数据中加入大量无答案的样本,训练模型识别这种情况并输出特定回复。
相关题目