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 在相关性与多样性间取平衡:
其中 是已选集合, 控制相关性 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 | 零样本检索、概念抽象类问题 |
| Reranking | Top-K 精确性不够 |
| Context Compression | 上下文超长、Token 预算受限 |
| MMR | 结果冗余、需要多样性 |
| 强约束 Prompt | 任何严肃的 RAG 系统 |
| 模块化架构 | 多查询类型、复杂工作流 |