§1.1.1

RAG 的定义与核心组件(Retriever + Reranker + Generator)?

核心概念

检索增强生成(Retrieval-Augmented Generation, RAG)是一种将大型语言模型(LLM)的强大生成能力与外部知识库的实时、动态信息检索相结合的混合式 AI 架构。其核心思想是,在生成回答之前,首先从一个大规模的文档语料库(如维基百科、公司内部文档、数据库)中检索出与用户问题相关的最新、最准确的信息片段。然后,将这些检索到的信息作为上下文(Context)与原始问题一起提供给 LLM,引导模型生成基于事实、有据可查的回答,从而显著缓解 LLM 的“幻觉”问题、知识过时问题,并提供答案的可追溯性。

原理与推导

RAG 的工作流程可以被形式化地理解为一个三阶段的概率链式过程:检索(Retriever)重排序(Reranker生成(Generator)

从概率角度看,一个标准的生成模型试图最大化给定输入 XX 后,输出序列 YY 的概率 P(YX)P(Y|X)。而 RAG 引入了一个潜在变量(latent variable)——被检索到的文档集 ZZ,其目标是建模 P(YX,Z)P(Y|X, Z)。整个过程可以看作是边缘化潜在文档 ZZ

P(YX)=ZCorpusP(YX,Z)P(ZX)P(Y|X) = \sum_{Z \in \text{Corpus}} P(Y|X, Z) \cdot P(Z|X)

在实践中,我们无法对整个语料库求和,因此采用一种近似方法:首先用检索器找到一个最可能相关的文档子集 {Z1,...,Zk}\{Z_1, ..., Z_k\},然后基于这个子集进行生成。


1. 检索器 (Retriever)

动机:从海量文档中快速、高效地召回一个与查询(Query)可能相关的候选文档集。此阶段优先考虑召回率(Recall)

原理:最主流的方法是密集向量检索(Dense Passage Retrieval, DPR

  1. 离线索引(Offline Indexing):使用一个预训练的 Transformer 模型(称为 Bi-Encoder)将知识库中所有的文档片段(Passages)did_i 独立地编码成高维向量 vdi\mathbf{v}_{d_i}。这些向量存储在一个专门的向量数据库(如 FAISS, Milvus)中,并建立索引以支持快速近邻搜索。

    vdi=ED(di)\mathbf{v}_{d_i} = E_D(d_i)

    其中 EDE_D 是文档编码器。

  2. 在线查询(Online Querying):当用户输入一个查询 qq 时,使用另一个(通常结构相同或相似的)编码器 EQE_Q 将其编码为查询向量 vq\mathbf{v}_q

    vq=EQ(q)\mathbf{v}_q = E_Q(q)

    然后在向量数据库中,通过计算查询向量与所有文档向量的相似度(通常是余弦相似度或内积),找出 Top-k 个最相似的文档向量。

    Similarity(q,di)=vqvdivqvdi\text{Similarity}(q, d_i) = \frac{\mathbf{v}_q \cdot \mathbf{v}_{d_i}}{\|\mathbf{v}_q\| \|\mathbf{v}_{d_i}\|}

    检索器的目标是返回 argmaxdiCorpuskSimilarity(q,di)\arg\max_{d_i \in \text{Corpus}}^k \text{Similarity}(q, d_i)

复杂度

  • 索引构建O(Ndemb)O(N \cdot d_{emb}),其中 NN 是文档数量,dembd_{emb} 是向量维度。这是一次性离线成本。
  • 查询(暴力搜索)O(Ndemb)O(N \cdot d_{emb}),非常慢。
  • 查询(使用近似最近邻 ANN 索引,如 HNSW)O(logN)O(\log N)O(poly(logN))O(\text{poly}(\log N)),非常快,是工程实践中的标准做法。

2. 重排序器 (Reranker)

动机:检索器为了速度牺牲了一部分精度,返回的 Top-k 文档中可能包含不那么相关的内容。重排序器使用一个更强大、更精细但计算成本更高的模型,对这 k 个候选文档进行重新打分和排序,以提高最终送入生成器上下文的精确率(Precision)

原理:通常使用**跨编码器(Cross-Encoder)**模型。

  • 与将查询和文档分开编码的 Bi-Encoder 不同,Cross-Encoder 将查询 qq 和每个候选文档 did_i 拼接成一个单一的输入序列,例如 [CLS] q [SEP] d_i [SEP]
  • 这个拼接后的序列被完整地输入到一个 Transformer 模型(如 BERT)中。模型通过内部的自注意力机制,可以对查询和文档的词元(token)进行深度、细粒度的交互。
  • 模型最终输出一个单一的分数 sis_i,表示 did_iqq 的相关性。 si=Mrerank(q,di)s_i = M_{\text{rerank}}(q, d_i)
  • 根据分数 sis_i 对 k 个文档进行降序排序,然后选择前 n 个(nkn \le k)作为最终的上下文。

复杂度

  • O(kLqLd)O(k \cdot L_q \cdot L_d),其中 kk 是候选文档数,Lq,LdL_q, L_d 分别是查询和文档的长度。由于 kk 通常不大(例如 20-100),这个计算开销是可接受的。

3. 生成器 (Generator)

动机:利用 LLM 强大的语言理解和生成能力,基于原始问题和经过筛选的高质量上下文,生成一个流畅、准确、符合逻辑的最终答案。

原理:这是一个标准的条件文本生成任务。

  1. 上下文构建(Prompting):将原始查询 qq 和重排序后的 Top-n 个文档 {d1,...,dn}\{d'_1, ..., d'_n\} 组合成一个结构化的提示(Prompt)。格式至关重要,一个常见的模板如下:
    text
    1请根据以下提供的上下文信息来回答问题。如果上下文中没有足够信息,请回答“根据现有信息无法回答”。
    2上下文:
    3---
    4[文档 d'_1 的内容]
    5---
    6[文档 d'_2 的内容]
    7...
    8---
    9问题:[原始查询 q]
    10回答:
  2. 自回归生成(Autoregressive Generation):LLM 以这个增强后的 Prompt 作为输入,逐个词元地生成答案序列 Y=(y1,...,ym)Y = (y_1, ..., y_m)。在生成每个词元 yty_t 时,模型会考虑整个 Prompt 和已经生成的词元序列 y<ty_{<t} P(YX,Z)=t=1mP(ytX,Z,y<t)P(Y|X, Z') = \prod_{t=1}^{m} P(y_t | X, Z', y_{<t}) 其中 Z={d1,...,dn}Z' = \{d'_1, ..., d'_n\} 是重排序后的文档集。

代码实现

下面是一个使用 sentence-transformers, faiss, 和 transformers 库实现的简化版 RAG 流程。

python
1import torch
2import faiss
3import numpy as np
4from sentence_transformers import SentenceTransformer, CrossEncoder
5from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
6
7# 确保有 GPU 可用,否则回退到 CPU
8device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
9print(f"正在使用设备: {device}")
10
11# --- 0. 准备知识库 ---
12knowledge_base = [
13 "苹果公司是一家美国的跨国科技公司,总部位于加利福尼亚州的库比蒂诺。",
14 "iPhone 是由苹果公司设计和销售的一系列智能手机。",
15 "MacBook Pro 是苹果公司推出的一款高端笔记本电脑,于2006年首次发布。",
16 "特斯拉是一家美国电动汽车和清洁能源公司,总部位于德克萨斯州的奥斯汀。",
17 "马斯克是特斯拉的首席执行官,并以其在 SpaceX 和 Neuralink 的工作而闻名。",
18 "2023年,苹果发布了其首款空间计算设备 Apple Vision Pro。",
19]
20
21# --- 1. 检索器 (Retriever) ---
22# 为什么使用 bi-encoder:为了高效地将查询和文档独立编码成向量,适用于大规模检索。
23retriever_model = SentenceTransformer('moka-ai/m3e-base', device=device)
24
25# 离线编码知识库
26print("正在编码知识库...")
27db_embeddings = retriever_model.encode(knowledge_base, convert_to_numpy=True)
28
29# 为什么使用 FAISS:它是一个高效的向量相似性搜索库,可以处理海量向量数据。
30# IndexFlatL2 使用 L2 距离(欧氏距离)进行精确搜索。
31embedding_dim = db_embeddings.shape[1]
32index = faiss.IndexFlatL2(embedding_dim)
33index.add(db_embeddings)
34
35def 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) # 在搜索前归一化查询向量
40
41 # 搜索最近的 k 个向量
42 distances, indices = index.search(query_embedding, top_k)
43 retrieved_docs = [knowledge_base[i] for i in indices[0]]
44
45 print("检索到的文档:")
46 for doc in retrieved_docs:
47 print(f"- {doc}")
48 return retrieved_docs
49
50# --- 2. 重排序器 (Reranker) ---
51# 为什么使用 cross-encoder:通过将查询和文档对一起输入模型,可以捕捉更深层次的交互,从而实现更精确的重排序。
52reranker_model = CrossEncoder('BAAI/bge-reranker-base', max_length=512)
53
54def 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]
58
59 # 预测分数
60 scores = reranker_model.predict(pairs)
61
62 # 为什么 zip 和 sort:将分数与文档配对,然后按分数降序排序。
63 doc_scores = list(zip(retrieved_docs, scores))
64 doc_scores.sort(key=lambda x: x[1], reverse=True)
65
66 reranked_docs = [doc for doc, score in doc_scores[:top_n]]
67
68 print("重排序后的文档:")
69 for doc, score in doc_scores[:top_n]:
70 print(f"- (分数: {score:.2f}) {doc}")
71 return reranked_docs
72
73# --- 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)
77
78def generate(query: str, reranked_docs: list):
79 print("\n--- 生成阶段 ---")
80 # 为什么构建这样的 prompt:这是指导 LLM 如何利用上下文的关键步骤,明确指示其任务和信息来源。
81 context = "\n".join(reranked_docs)
82 prompt = f"""
83 根据以下信息回答问题。
84
85 信息:
86 {context}
87
88 问题: {query}
89 """
90
91 print(f"构建的 Prompt:\n{prompt}")
92
93 # 为什么用 tokenizer:将文本转换为模型可以理解的数字 ID。
94 inputs = generator_tokenizer(prompt, return_tensors="pt", max_length=1024, truncation=True).to(device)
95
96 # 生成答案
97 outputs = generator_model.generate(**inputs, max_length=200, num_beams=5, early_stopping=True)
98
99 # 为什么用 tokenizer.decode:将模型输出的数字 ID 转换回人类可读的文本。
100 answer = generator_tokenizer.decode(outputs[0], skip_special_tokens=True)
101
102 print("\n生成的答案:")
103 print(answer)
104 return answer
105
106# --- 主流程 ---
107if __name__ == "__main__":
108 query = "苹果公司最新发布了什么计算设备?"
109
110 # 1. 检索
111 retrieved_docs = retrieve(query, top_k=3)
112
113 # 2. 重排序
114 reranked_docs = rerank(query, retrieved_docs, top_n=2)
115
116 # 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 的负担。
    • Reranker top_n:通常设置为 2 到 5。这个数量决定了送入生成器上下文的质量和长度。需要权衡信息密度与 LLM 上下文窗口长度限制及处理成本。
  • 性能/显存/吞吐的权衡

    • Retriever:向量数据库的索引类型是关键。HNSW 等 ANN 索引在牺牲极小的精度下,可实现毫秒级查询。Embedding 模型的大小也影响显存和速度。
    • Reranker:是可选组件。不用它,延迟更低,但答案质量可能下降。使用它,会增加几十到几百毫秒的延迟。
    • Generator:是最大的性能瓶颈。模型越大,生成质量越好,但推理速度越慢,显存占用越高。使用量化(Quantization)、剪枝(Pruning)或更小的模型可以提升速度。流式生成(Streaming)可以显著改善用户感知的首字延迟。
  • 常见坑和调试技巧

    • “大海捞针”问题:如果知识库非常庞大且异构,简单的向量相似度可能不足以找到正确信息。需要结合关键词搜索(稀疏检索,如 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 存在的价值。
  • 边界情况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 模型或减少送入 Rerankerk 值。3) Retriever:检查 ANN 索引的配置是否最优,网络延迟是否过高。
相关题目