人工智能实践(语言智能)
第4讲:RAG

4.3 生成策略

从朴素 RAG 到模块化 RAG——Query 改写、上下文压缩、幻觉抑制的工程取舍

RAG 架构的四个代际

Gao et al. (2024) 的综述把 RAG 架构划为四个代际——这是理解"为什么生成策略越来越复杂"的主线。

  • 朴素 RAG (Naive RAG):线性三步——索引、检索、生成。简单直接。
  • 高级 RAG (Advanced RAG):在朴素流程两端插入优化(查询改写、重排序、上下文压缩)。
  • 模块化 RAG (Modular RAG):把流水线拆解为搜索、路由、记忆、预测、融合等模块,支持非线性流程。
  • 智能体 RAG (Agentic RAG):LLM 智能体编排"是否检索、选哪个工具、何时停止"的决策。

本节聚焦前两代的工程取舍——这是你在绝大多数项目中实际会动手调的层面。


朴素 RAG 的四大失败模式

即便三要素齐备,朴素 RAG 仍会在以下场景失效:

失败模式表现根因
检索精度低召回文档与问题貌合神离语义相似 ≠ 问题相关
迷失在中间忽略长上下文中部信息Liu et al., 2024
多跳推理失败单轮检索凑不齐证据需跨文档综合
检索噪声幻觉不相关文档诱导错答缺少证据门控

"Lost in the Middle"(Liu et al., 2024)是 RAG 生成侧最重要的一个发现:当上下文超过 10-20 个片段时,模型对中间位置的信息关注度显著下降。这直接催生了重排序的必要性——把最相关的证据放在首尾位置。


预检索优化(Pre-Retrieval)

发起检索之前对 query 做加工:

1. Query 改写

用 LLM 把口语化、省略的问题改写为检索友好的查询:

def rewrite_query(raw_query: str) -> str:
    prompt = f"""请将以下用户问题改写为更适合信息检索的查询。
- 保持原始语义,补充隐含的关键概念
- 消除代词指代(把"它"替换为具体对象)
- 不加解释,只输出改写后的查询

原始问题:{raw_query}
改写查询:"""
    return llm.generate(prompt).strip()

2. Query 分解

把复杂问题拆解为多个子问题,并行检索并合并证据:

原问题:"比较 BERT 和 GPT 的预训练目标和模型规模"
  ↓ 分解
子问1:"BERT 的预训练目标是什么?"
子问2:"GPT 的预训练目标是什么?"
子问3:"BERT 的参数规模是多少?"
子问4:"GPT 的参数规模是多少?"
  ↓ 并行检索 + 合并上下文 → 生成

3. Step-back Prompting

先让 LLM 从原问题中抽象出上位概念,用上位概念检索,再回到原问题。对"需要理论背景"的问题效果显著(MMLU 物理 +7%)。


后检索优化(Post-Retrieval)

1. 重排序(Reranking)

4.1 Reranker 小节。核心:用交叉编码器对 Top-20 粗召回做精排,取 Top-3/5 送给 LLM。

2. 上下文压缩(Context Compression)

把检索到的片段中与问题无关的部分过滤掉,减少 token 消耗并缓解"Lost in the Middle":

  • 基于 LLM 的抽取:让小模型(如 Qwen-1.8B)从每个块中抽出与 query 相关的句子
  • 基于分数的过滤:丢弃重排序分数低于阈值的候选
  • LongLLMLingua / LLMLingua-2:token 级别的压缩,能把 20x 长的上下文压到原长

3. 去重与多样性(MMR)

Maximal Marginal Relevance 在相关性与多样性间取平衡:

MMR(d)=λsim(d,q)(1λ)maxdSsim(d,d)\text{MMR}(d) = \lambda \cdot \text{sim}(d, q) - (1-\lambda) \cdot \max_{d' \in S} \text{sim}(d, d')

其中 SS 是已选集合,λ[0,1]\lambda \in [0, 1] 控制相关性 vs 多样性的权重。


提示构造:抑制幻觉的关键

Prompt 模板的细节直接决定忠实度。对比两个版本:

请回答以下问题:{question}

参考资料:
{context}

问题:模型会混合参考资料自身参数知识,容易幻觉。

你是严谨的问答助手。请严格基于以下【参考资料】回答【问题】。

要求:
1. 只使用参考资料中明确提到的信息,不要推测或补充
2. 在每个结论后用【资料 N】标注依据
3. 如果参考资料中没有答案,请直接回答"根据现有资料无法回答"
4. 不要重复问题本身

【参考资料】
{numbered_context}

【问题】
{question}

【回答】

要点:①明确角色 ②显式约束 ③强制引用 ④允许拒答 ⑤编号上下文。

"允许拒答"是一个反直觉但极其重要的设计:只有当模型可以说"不知道"时,它才能避免编造。"No answer is better than a wrong answer"——这条原则值得写进每一个 RAG 的系统提示。


模块化 RAG:把流水线拆成可插拔模块

当你的 RAG 需要处理多类型查询(FAQ、数据库查询、多跳推理)时,线性流水线会变成一团乱麻。模块化 RAG 的做法是把每个环节都定义为可插拔模块:

模块职责
Search适配多后端:向量库、知识图谱、SQL、Web API
Router决定查询走哪个 Search
Memory用对话历史改写或增强 query
Predict直接生成"伪上下文",少检索
Fusion合并多源检索结果(RRF、加权融合)
# 伪代码:模块化 RAG 的一个调用
def modular_rag(query, history):
    query = MemoryModule(query, history)           # 重写 query
    route = RouterModule(query)                    # 路由到 kg / vector / sql
    evidence = SearchModule[route](query, k=10)
    evidence = FusionModule(evidence)              # 如果多路检索
    context = RerankModule(query, evidence, k=3)
    return GenerationModule(query, context)

这是 LangChain、LlamaIndex、DSPy 这类框架的核心抽象——每一个组件都可以被替换或 A/B 测试。


本节小结

策略何时用
Query 改写用户表达口语化、省略严重
Query 分解多跳、对比类问题
HyDE / Step-back零样本检索、概念抽象类问题
RerankingTop-K 精确性不够
Context Compression上下文超长、Token 预算受限
MMR结果冗余、需要多样性
强约束 Prompt任何严肃的 RAG 系统
模块化架构多查询类型、复杂工作流