系列文章:本篇是第 2 篇 / 共 4 篇
[引言]
上一篇讲到 BM25 FAQ 短路层,60% 的简单问题在那里就解决了。但剩下 40% 的问题进入 RAG 管道之后,还有一个很尴尬的现实:
同一种检索方式,对不同问题的效果差距非常大。
"什么是决策?" —— 直接检索就能命中。 "请分析管理学中各流派决策理论的异同" —— 太抽象了,直接检索召回率很低。 "比较一下泰勒和法约尔的管理思想" —— 涉及多个子话题,一次检索覆盖不全。 "管咋学啊背不下来" —— 口语化,教材里根本没有这种表述。
如果只用一种检索策略,总有一类问题的效果会很差。所以我设计了 4 种策略,让 LLM 自动判断问题类型、选择最优路径。
4 种策略分别解决什么问题

先上全景表,后面一个一个拆:
| 策略 | 适用场景 | 工作原理 |
|---|---|---|
| 直接检索 | 概念明确、表述清晰 | 标准混合检索 + Reranker 精排 |
| HyDE | 抽象、理论性、比较类 | LLM 先生成"假设性答案",用答案去检索 |
| 子查询拆分 | 复杂多部分问题 | LLM 拆分子问题 → 分别检索 → 合并去重 |
| 回溯简化 | 口语化、模糊表述 | LLM 转化为精确学术问题再检索 |
策略选择器:规则优先,LLM 兜底
# rag_qa/core/strategy_selector.py
class StrategySelector:
def select(self, query: str) -> str:
# 第1步:规则优先(O(1) 关键词匹配,毫秒级)
if any(kw in query for kw in ["比较", "对比", "区别", "异同"]):
return "子查询检索"
# 第2步:LLM 分类(temperature=0.1)
result = self.llm.invoke(strategy_prompt(query))
strategy_map = {
"A": "直接检索",
"B": "假设问题检索",
"C": "子查询检索",
"D": "回溯问题检索"
}
return strategy_map.get(result, "直接检索") # 失败默认直接检索
这里的设计思路和意图识别一样:规则优先 + LLM 兜底。包含"比较/对比/区别"等关键词的直接走子查询,省一次 LLM 调用。LLM 分类失败时默认回退到直接检索 —— 因为直接检索是最安全的策略,至少不会出错。
给 LLM 的 Prompt 也很简洁:
你是一个检索策略分类器。根据用户问题选择最合适的检索策略:
A. 直接检索 - 问题表述清晰,关键词明确
B. 假设问题检索 - 问题抽象、理论性强
C. 子查询检索 - 问题包含多个子话题
D. 回溯问题检索 - 问题口语化、模糊
用户问题:{query}
只回答 A/B/C/D
HyDE:与其用问题检索,不如先生成一个"假答案"
HyDE (Hypothetical Document Embeddings) 是这 4 种策略里最巧妙的一个。
def hyde_search(query: str):
# 1. LLM 生成假设性答案
hypothetical_answer = llm.generate(hyde_prompt(query))
# prompt: "假设你是一位管理学教授,请针对以下问题给出一个简要回答:{query}"
# 2. 用假设答案的 embedding 去检索
results = vector_store.hybrid_search(hypothysical_answer)
# 3. Reranker 精排(注意:rerank 时仍用原始 query)
return reranker.rerank(query, results)
为什么 HyDE 有效? 举个真实例子。
用户问:"各流派决策理论的异同"
直接用这个问题去检索,关键词是"流派"、"决策理论"、"异同" —— 但教材中的表述是"古典决策理论"、"行为决策理论"、"理性决策模型"。关键词对不上,召回率低。
HyDE 生成的假设答案会包含:"古典决策理论假设完全理性...行为决策理论引入有限理性概念...两者在假设前提、分析方法上存在差异..." —— 这些教材关键词被假设答案"补全"了,用假设答案去检索,召回率大幅提升。
但有个坑:Reranker 精排时必须用原始 query,不能用假设答案。因为假设答案是 LLM "编"的,可能包含错误信息,用它精排会把不相关内容排到前面。
子查询拆分:大问题拆成小问题,各个击破
def subquery_search(query: str): # 1. LLM 拆分子问题 sub_queries = llm.generate(subquery_prompt(query)) # prompt: "将以下问题分解为简单的子问题,每行一个" # 例如: "比较泰勒和法约尔" → # "泰勒科学管理理论的核心要点是什么" # "法约尔一般管理理论的核心要点是什么" # "泰勒和法约尔管理思想的主要区别是什么" # 2. 并行检索每个子问题 all_results = [] for sq in sub_queries: results = vector_store.hybrid_search(sq) all_results.extend(results) # 3. 按 parent_id 合并去重 merged = deduplicate_by_parent_id(all_results) # 4. Reranker 精排(用原始 query) return reranker.rerank(query, merged)
为什么要按 parent_id 去重? 多个子问题可能检索到同一个父块下的不同子块。如果不去重,同一个父块的内容会被重复送给 LLM,浪费 token 且可能让 LLM 过度关注这部分内容。
子查询拆分的 Prompt:
你是一个问题分解器。将以下复杂问题分解为2-4个简单的子问题。
要求:
1. 每个子问题应该是一个独立的、可检索的知识点
2. 每行一个子问题
3. 不要包含原问题中未提及的假设
用户问题:{query}
回溯简化:把口语翻译成教材语言
这是最"接地气"的一个策略。
def backtracking_search(query: str): # 1. LLM 将口语转化为学术问题 academic_query = llm.generate(backtracking_prompt(query)) # prompt: "将以下口语化问题转化为精确的学术问题" # 例如: "管咋学啊背不下来" → "管理学课程的学习方法和记忆技巧有哪些" # 例如: "决策那个玩意儿到底啥意思" → "管理学中决策的定义和内涵是什么" # 2. 用转化后的问题去检索 results = vector_store.hybrid_search(academic_query) # 3. Reranker 精排(用原始 query) return reranker.rerank(query, results)
回溯简化的 Prompt:
你是一个学术问题改写器。将以下口语化的问题转化为精确的学术问题。
要求:
1. 使用管理学专业术语
2. 保持原意不变
3. 只输出改写后的问题,不要解释
用户问题:{query}
4 种策略实际表现对比
| 维度 | 直接检索 | HyDE | 子查询 | 回溯 |
|---|---|---|---|---|
| LLM 调用次数 | 0 | 1 | 1+ | 1 |
| 检索次数 | 1 | 1 | N | 1 |
| 延迟 | 最低 | 中等 | 较高 | 中等 |
| 适用场景 | 关键词明确 | 抽象理论 | 多话题复合 | 口语化 |
| 核心优势 | 快 | 补全关键词 | 覆盖面广 | 术语对齐 |
实际跑下来的分布:直接检索覆盖约 50%,HyDE 约 20%,子查询约 15%,回溯约 15%。策略选择器本身的 LLM 调用延迟约 200ms,相比后续检索+生成的 2-5 秒基本可以忽略。
小结
本篇拆解了 4 种自适应检索策略。核心要点:
-
策略选择:规则优先 + LLM 兜底,失败默认直接检索
-
HyDE:用假设答案补全关键词,提升抽象问题的召回率
-
子查询:拆分后并行检索,按 parent_id 去重
-
回溯:口语→学术术语对齐
下一篇进入检索层内部 —— BGE-M3 双向量混合检索 + WeightedRanker 融合 + BGE-Reranker Cross-Encoder 精排 + 父子块上下文恢复,这是整个系统检索质量的核心。
项目地址:GitHub - jlu55404-art/RAG_Agent_project: RAG问答系统 · GitHub
有帮助欢迎 Star 支持!问题评论区交流~

390


被折叠的 条评论
为什么被折叠?



