§1.1.3

RAG vs 长上下文 vs 微调 vs Prompt Caching 的决策矩阵?

核心概念

这是一个关于在大型语言模型(LLM)应用中,集成外部知识、调整模型行为和优化性能的四种关键技术的比较。

  • RAG (Retrieval-Augmented Generation, 检索增强生成):一种将信息检索系统与 LLM 生成器相结合的架构。它首先从外部知识库(如向量数据库)中检索相关文档片段,然后将这些片段作为上下文信息注入到 LLM 的提示中,指导模型生成更准确、更有依据的回答。
  • 长上下文 (Long Context):指 LLM 能够处理和理解的输入文本(Prompt)的最大长度。拥有长上下文能力的模型可以直接在 Prompt 中接收大量信息(如整本书、多个文档),并基于这些信息进行推理和生成,而无需外部检索系统。
  • 微调 (Fine-tuning):指在一个已经预训练好的 LLM 基础上,使用特定领域或任务的标注数据集进行二次训练,以调整模型内部的权重参数。其目标是让模型学习新的知识、适应特定的格式、风格或掌握某种特定技能。
  • Prompt Caching (提示缓存):一种针对 LLM 推理的性能优化技术。它通过缓存和重用 Prompt 中公共前缀部分的 Key-Value (KV) 状态,来避免重复计算,从而显著降低生成多个具有相同前缀的序列时的延迟。它不改变模型的知识或行为,纯粹是速度优化。

原理与推导

RAG (检索增强生成)

RAG 的核心思想是解耦知识存储和语言生成。其工作流程分为两步:

  1. 检索 (Retrieve):给定一个用户查询 qq,系统首先使用一个检索器 RR 从一个庞大的文档语料库 DD 中找到最相关的 kk 个文档片段 {d1,d2,...,dk}\{d_1, d_2, ..., d_k\}

    • 现代 RAG 通常使用稠密检索。文档和查询被编码为高维向量,通常使用双编码器(bi-encoder)模型如 Sentence-BERT。
    • 查询向量:vq=Encoder(q)v_q = \text{Encoder}(q)
    • 文档向量:vd=Encoder(d)v_d = \text{Encoder}(d)
    • 相似度计算(常用余弦相似度):S(q,d)=vqvdvqvdS(q, d) = \frac{v_q \cdot v_d}{\|v_q\| \|v_d\|}
    • 检索过程就是在一个向量数据库中执行最大内积搜索 (MIPS)。
  2. 生成 (Generate):将原始查询 qq 和检索到的文档片段 {di}\{d_i\} 组合成一个新的 Prompt,然后送入 LLM GG 生成最终答案 aa

    • 最终的生成概率可以概念化地表示为: P(aq)i=1kP(retrieve diq)PLLM(aq,di)P(a|q) \approx \sum_{i=1}^{k} P(\text{retrieve } d_i | q) \cdot P_{\text{LLM}}(a | q, d_i)
    • 实际上,通常将 top-k 文档拼接起来:a=G(prompt=concat(q,d1,d2,...,dk))a = G(\text{prompt} = \text{concat}(q, d_1, d_2, ..., d_k))

复杂度

  • 检索:对于一个有 NN 个文档的向量数据库,使用像 HNSW(Hierarchical Navigable Small World)这样的近似最近邻搜索算法,其查询复杂度大约是 O(logN)O(\log N)
  • 生成:与标准 LLM 推理相同,受限于生成长度。

长上下文 (Long Context)

长上下文的核心技术挑战在于 Transformer自注意力机制 (Self-Attention)

  • 原理:标准自注意力机制的计算公式为: Attention(Q,K,V)=softmax(QKTdk)V\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V 其中 Q,K,VQ, K, V 是输入序列经过线性变换后的查询、键、值矩阵,维度为 (L,dk)(L, d_k)LL 是序列长度,dkd_k 是特征维度。
  • 复杂度:计算 QKTQK^T 会产生一个 L×LL \times L 的注意力分数矩阵。因此,自注意力机制的时间和空间复杂度都是 O(L2)O(L^2)。当序列长度 LL 翻倍时,计算量和显存占用会增加到四倍,这使得扩展到极长序列(如百万级)变得非常困难。
  • 优化:为了实现长上下文,研究者们提出了多种优化方案,如 FlashAttention(通过 I/O 感知算法优化 GPU 显存读写,避免物化 L×LL \times L 矩阵)、稀疏注意力(如 Longformer)、线性化注意力等,旨在将复杂度降低到 O(LlogL)O(L \log L)O(L)O(L)

微调 (Fine-tuning)

微调的原理是基于迁移学习,利用预训练模型学到的通用特征,在特定任务上进行优化。

  • 全量微调 (Full Fine-tuning):更新模型的所有参数。

    • 目标是最小化在任务数据集 Dtask={(xi,yi)}D_{task} = \{(x_i, y_i)\} 上的损失函数 LLθ=argminθ(x,y)DtaskL(Gθ(x),y)\theta^* = \arg\min_{\theta} \sum_{(x, y) \in D_{task}} L(G_{\theta}(x), y) 其中 θ\theta 是模型的全部参数。这需要大量的计算资源和显存。
  • 参数高效微调 (PEFT, Parameter-Efficient Fine-tuning):只更新模型的一小部分参数或附加参数。

    • LoRA (Low-Rank Adaptation) 是最流行的 PEFT 方法之一。它假设模型权重的更新是低秩的。对于一个预训练权重矩阵 W0Rd×kW_0 \in \mathbb{R}^{d \times k}LoRA 的更新表示为: W0W0+ΔW=W0+BAW_0 \to W_0 + \Delta W = W_0 + BA 其中 BRd×rB \in \mathbb{R}^{d \times r}ARr×kA \in \mathbb{R}^{r \times k},而秩 rmin(d,k)r \ll \min(d, k)。训练时只更新 AABB,参数量从 d×kd \times k 减少到 r×(d+k)r \times (d+k)
    • 推理时,可以直接将 BABA 合并回 W0W_0,不增加任何推理延迟。

Prompt Caching (提示缓存)

该技术利用了 Transformer 的自回归生成特性。

  • 原理:在生成第 ii 个 token 时,Transformer 需要用到前面所有 i1i-1 个 token 的 Key 和 Value 向量(即 KV Cache)。如果多个请求有相同的前缀(例如,一个系统模板或 RAG 检索到的相同文档),这部分前缀的 KV Cache 计算是完全重复的。
  • 实现
    1. 当处理一个 Prompt P=Pprefix+PsuffixP = P_{\text{prefix}} + P_{\text{suffix}} 时,首先计算并存储 PprefixP_{\text{prefix}} 对应的所有 Transformer 层的 KV Cache
    2. 当另一个请求 P=Pprefix+PsuffixP' = P_{\text{prefix}} + P'_{\text{suffix}} 到来时,系统可以直接从缓存中加载 PprefixP_{\text{prefix}}KV Cache,然后从 PsuffixP'_{\text{suffix}} 的第一个 token 开始计算,极大地节省了处理前缀部分的计算时间。
  • 收益:假设前缀长度为 NN 后缀长度为 MM,无缓存时处理前缀的计算量约为 O(N2)O(N^2)。使用缓存后,这部分开销变为一次性的,后续请求只需 O(M2+2NM)O(M^2 + 2NM) 的计算量(后缀内部的自注意力和后缀对前缀的注意力)。对于 NMN \gg M 的场景(如大型 RAG 上下文),加速效果非常显著。

代码实现

下面是一个极简的 RAG 实现,用 NumPy 模拟向量检索过程,以展示其核心逻辑。

python
1import numpy as np
2
3# --- 1. 准备与索引 (Setup & Indexing) ---
4# 假设我们有一个知识库,包含三份关于不同水果的文档
5knowledge_base = {
6 "doc1": "苹果是一种又脆又甜的水果,富含维生素C。",
7 "doc2": "香蕉是黄色的,口感软糯,富含钾元素。",
8 "doc3": "草莓是红色的,表面有许多小籽,味道酸甜。"
9}
10
11# 模拟一个简单的词嵌入模型(实际项目中会用 Sentence-BERT 等)
12# 这里为了可复现性,我们用一个固定的随机投影矩阵
13embedding_dim = 10
14vocab = list(set("".join(knowledge_base.values()) + "苹果是什么?"))
15word_to_idx = {word: i for i, word in enumerate(vocab)}
16projection_matrix = np.random.rand(len(vocab), embedding_dim)
17
18def get_embedding(text: str) -> np.ndarray:
19 """
20 一个简化的文本向量化函数
21 为什么这样做:将文本转换为固定维度的向量,是进行相似度计算的前提。
22 这里使用简单的词向量平均,实际应使用更复杂的模型。
23 """
24 vector = np.zeros(embedding_dim)
25 words = [char for char in text if char in word_to_idx]
26 if not words:
27 return vector
28 for word in words:
29 vector += projection_matrix[word_to_idx[word]]
30 return vector / len(words)
31
32# 创建向量数据库
33vector_database = {
34 doc_id: get_embedding(text)
35 for doc_id, text in knowledge_base.items()
36}
37
38# --- 2. 检索 (Retrieval) ---
39def retrieve(query_embedding: np.ndarray, top_k: int = 1) -> list[str]:
40 """
41 从向量数据库中检索最相似的文档
42 为什么这样做:通过计算余弦相似度,找到与用户问题在语义上最相关的知识片段。
43 这是 RAG 的核心步骤,为 LLM 提供相关上下文。
44 """
45 similarities = {}
46 for doc_id, doc_embedding in vector_database.items():
47 # 计算余弦相似度
48 similarity = np.dot(query_embedding, doc_embedding) / (np.linalg.norm(query_embedding) * np.linalg.norm(doc_embedding))
49 similarities[doc_id] = similarity
50
51 # 按相似度降序排序,并返回 top_k 个文档的 ID
52 sorted_docs = sorted(similarities.items(), key=lambda item: item[1], reverse=True)
53 return [doc_id for doc_id, sim in sorted_docs[:top_k]]
54
55# --- 3. 生成 (Generation) ---
56def generate_answer(query: str, retrieved_docs: list[str]):
57 """
58 将检索到的文档和原始问题组合成一个 Prompt,并模拟 LLM 生成答案
59 为什么这样做:这是 RAG 的“增强”环节。通过提供上下文,引导 LLM 生成基于事实的、
60 更准确的回答,而不是依赖其内部可能过时或不准确的知识。
61 """
62 context = "\n".join([knowledge_base[doc_id] for doc_id in retrieved_docs])
63
64 # 构建最终的 Prompt
65 prompt = f"""
66 根据以下信息回答问题。
67
68 信息:
69 {context}
70
71 问题: {query}
72 回答:
73 """
74
75 print("--- 构建的最终 Prompt ---")
76 print(prompt)
77
78 # 模拟 LLM 的回答
79 # 在真实场景中,这里会调用一个 LLM API (e.g., OpenAI, Anthropic)
80 print("--- 模拟的 LLM 回答 ---")
81 if "苹果" in query:
82 print("根据提供的信息,苹果是一种富含维生素C的又脆又甜的水果。")
83 else:
84 print("对不起,我无法根据提供的信息回答你的问题。")
85
86# --- 主流程 ---
87if __name__ == "__main__":
88 user_query = "苹果是什么?"
89
90 # 1. 将用户查询向量化
91 query_embedding = get_embedding(user_query)
92
93 # 2. 检索相关文档
94 retrieved_doc_ids = retrieve(query_embedding, top_k=1)
95 print(f"查询: '{user_query}'")
96 print(f"检索到的最相关文档ID: {retrieved_doc_ids}\n")
97
98 # 3. 基于检索结果生成答案
99 generate_answer(user_query, retrieved_doc_ids)

工程实践

这四种技术不是互斥的,而是 LLM 应用工具箱中的不同工具。选择哪种或哪几种,取决于具体场景、预算和目标。

决策矩阵 / 经验法则

| 维度 | RAG | 长上下文 | 微调 (Fine-tuning) | Prompt Caching | | :--- | :--- | :--- | :--- | :--- | | 主要目标 | 注入外部、动态的事实性知识 | 处理单次请求中的海量信息 | 改变模型的行为、风格或技能 | 降低推理延迟,提升吞吐 | | 知识时效性 | (可实时更新向量库) | (每次请求时提供) | (需重新训练,成本高) | 不适用 (不改变知识) | | 幻觉抑制 | (答案有据可查,可溯源) | (易出现"大海捞针"问题) | (可能学会编造特定领域的"事实") | 不适用 | | 实现复杂度 | (需要搭建检索系统) | (只需调用支持长上下文的模型) | (需要数据、算力、专业知识) | (需推理引擎支持,如 vLLM) | | 开发成本 | (向量数据库、ETL pipeline) | (API 调用成本可能高) | (数据标注、GPU 训练) | (通常是集成现有框架) | | 推理成本 | (检索+生成,Prompt 变长) | 极高 (注意力计算是二次方复杂度) | (与基础模型相同,若用 PEFT) | 显著降低 (对有公共前缀的请求) | | 领域特异性 | (通过领域知识库实现) | (依赖 Prompt 工程) | 极强 (模型权重本身被改变) | 不适用 | | 适用场景 | 问答、客服、研究助理、知识库查询 | 代码分析、法律合同审查、财报分析 | 角色扮演、特定格式输出、特定任务优化 | 多租户系统、RAG 应用、Chain-of-Thought |

决策流程图(思维导图)

  1. 你的主要问题是性能/延迟吗?

    • ,且你的请求有大量重复的前缀(如系统指令、RAG上下文) -> 首先考虑 Prompt Caching
  2. 你的主要目标是让模型掌握新的事实性知识吗?(例如产品文档、最新新闻)

    • -> 首选 RAG。它更新快、成本低、可溯源。
    • 如果 RAG 检索到的内容很长,需要模型综合理解 -> RAG + 长上下文模型
  3. 你的主要目标是改变模型的根本行为、语气、风格或学会一种新技能吗?(例如,扮演特定角色、遵循复杂的指令格式、进行特定的代码转换)

    • -> 首选微调 (Fine-tuning),特别是 PEFT/LoRA
    • 如果微调后仍需引用外部动态知识 -> 微调 + RAG 组合。
  4. 你的主要任务是处理单一份巨大、连贯的文档吗?(例如,分析一本小说、审查一份超长的法律合同)

    • -> 首选长上下文模型。此时,将整个文档放入 Prompt 是最直接有效的方式。

常见误区与边界情况

  1. 误区:微调可以高效地向模型注入新知识。

    • 真相:微调更擅长教模型“如何表现”(风格、格式),而不是“知道什么”(事实)。用微调注入大量事实知识效率低下,且容易导致“灾难性遗忘”(忘记预训练时学到的知识)。RAG 是更适合知识注入的工具。
    • 追问:那什么时候微调可以用于知识?答:当知识是隐式的、与行为强相关时。比如,微调一个代码生成模型,让它学会使用某个新的、未在预训练数据中出现的 API。这既是知识(API 存在),也是技能(如何使用)。
  2. 误区:长上下文模型能完美理解并利用 Prompt 中的所有信息。

    • 真相:许多长上下文模型存在“中间迷失”(Lost in the Middle)问题,即模型更关注 Prompt 开头和结尾的信息,而忽略中间部分。因此,即使上下文窗口很大,关键信息的位置仍然重要。
    • 边界:模型的“有效上下文”可能小于其“名义上下文”。需要通过实验(如 Needle-in-a-Haystack 测试)来评估特定模型在特定长度下的真实表现。
  3. 误区:RAG 能完全消除幻觉。

    • 真相:RAG 只能减轻幻觉,不能根除。其效果严重依赖于检索质量。所谓“垃圾进,垃圾出”(Garbage In, Garbage Out)。如果检索器找不到相关信息或找到了错误信息,LLM 仍然可能产生幻觉或错误回答。
    • 追问:如何提升 RAG 的鲁棒性?答:优化检索器(更好的嵌入模型、混合检索)、实现重排(Re-ranking)步骤、让模型在无法回答时明确表示“根据所提供信息无法回答”。
  4. 误区:Prompt Caching 对所有应用都有用。

    • 真相:它只对具有大量重复前缀的请求有效。对于每个请求都完全独立的场景(如简单的单轮闲聊),它几乎没有收益,反而可能带来额外的缓存管理开销。
    • 边界:缓存的粒度是关键。是缓存整个系统指令,还是 RAG 的部分上下文?缓存大小和驱逐策略也需要考虑,否则可能消耗大量 GPU 显存。
  5. 组合使用的智慧

    • 最强大的系统往往是这些技术的组合。一个典型的先进架构可能是:LoRA 微调一个基础模型,使其适应特定领域的术语和任务格式;然后在推理时使用 RAG 为其提供最新的事实信息;整个服务部署在支持Prompt Caching的推理引擎上,以服务高并发请求;并选择一个长上下文版本的基础模型,以处理 RAG 检索回来的大量文档。
相关题目