RAG检索策略完全指南
RAG检索策略完全指南
检索是RAG系统的灵魂!检索不准,再好的LLM也白搭。让我给你一个完整的检索优化方案。
🎯 一、检索的本质问题
核心挑战
问题的本质:
用户问题:"为什么植物晚上不进行光合作用?"
↓
向量化:[0.123, -0.456, 0.789, ...]
↓
在向量数据库中找相似向量
↓
找到的可能是:
❌ "植物的根系吸收营养的过程..." (相似度0.72)
❌ "植物细胞的结构包括..." (相似度0.68)
✅ "光合作用需要光照,没有光就无法进行" (相似度0.65)
结果:最相关的内容反而分数最低!
为什么会这样?
- 词汇鸿沟:用户问法 ≠ 文档表述
- 语义漂移:向量相似 ≠ 语义相关
- 上下文丢失:单个chunk缺少完整信息
- 噪音干扰:无关内容污染检索结果
🔍 二、7种检索策略对比
| 策略 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 1. 基础向量检索 | 余弦相似度 | 简单快速 | 词汇鸿沟严重 | 基线方案 |
| 2. 混合检索 | 向量+关键词 | 平衡语义和精确 | 需要调参 | 通用推荐⭐⭐⭐⭐⭐ |
| 3. 重排序 | 二次精排 | 准确度高 | 速度慢 | 高要求场景 |
| 4. 查询改写 | 问题优化 | 提升召回 | 可能过度优化 | 口语化问题 |
| 5. 假设性文档嵌入 | 生成假答案 | 效果好 | 需要LLM | 复杂问题 |
| 6. 元数据过滤 | 预过滤 | 精确高效 | 需要元数据 | 结构化内容 |
| 7. 多查询融合 | 多角度检索 | 召回全面 | 成本高 | 关键查询 |
🚀 三、实战方案(按优先级)
方案1:混合检索(必须做!)⭐⭐⭐⭐⭐
原理:向量检索(语义)+ BM25(关键词)
from langchain.retrievers import EnsembleRetriever
from langchain.retrievers import BM25Retriever
from langchain.vectorstores import Chroma
class HybridRetriever:
"""混合检索器 - 向量+关键词"""
def __init__(self, vectorstore, documents):
# 1. 向量检索器
self.vector_retriever = vectorstore.as_retriever(
search_type="similarity",
search_kwargs={"k": 5}
)
# 2. BM25关键词检索器
self.bm25_retriever = BM25Retriever.from_documents(documents)
self.bm25_retriever.k = 5
# 3. 融合检索器(RRF算法)
self.ensemble_retriever = EnsembleRetriever(
retrievers=[self.vector_retriever, self.bm25_retriever],
weights=[0.6, 0.4] # 向量60%,BM2540%
)
def retrieve(self, query: str, k: int = 3):
"""混合检索"""
results = self.ensemble_retriever.get_relevant_documents(query)
return results[:k]
# 使用示例
retriever = HybridRetriever(vectorstore, all_documents)
# 测试对比
query = "光合作用需要什么条件?"
# 纯向量检索
vector_only = vectorstore.similarity_search(query, k=3)
# 混合检索
hybrid_results = retriever.retrieve(query, k=3)
print("纯向量检索:")
for doc in vector_only:
print(f" - {doc.page_content[:50]}...")
print("\n混合检索(更准确):")
for doc in hybrid_results:
print(f" - {doc.page_content[:50]}...")
效果对比:
纯向量检索结果:
❌ "植物的根系结构包括主根和侧根..." (语义相似但不相关)
❌ "植物细胞的组成成分有..." (包含"植物"但不相关)
✅ "光合作用需要三个条件:光照、叶绿素、原料" (相关但排第3)
混合检索结果(向量+关键词):
✅ "光合作用需要三个条件:光照、叶绿素、原料" (排第1!)
✅ "光合作用的过程分为光反应和暗反应" (相关)
✅ "没有光照植物无法进行光合作用" (相关)
准确率提升:40% → 90% 🎉
方案2:查询改写(Query Rewriting)⭐⭐⭐⭐
问题:用户问题口语化、模糊、有指代
class QueryRewriter:
"""查询改写器"""
def __init__(self, llm):
self.llm = llm
def rewrite_query(self, query: str, conversation_history: list = None):
"""
改写查询,提升检索效果
"""
# 场景1:消除指代歧义
if self._has_pronoun(query) and conversation_history:
query = self._resolve_coreference(query, conversation_history)
# 场景2:扩展关键词
query = self._expand_keywords(query)
# 场景3:生成多个查询变体
variations = self._generate_variations(query)
return {
"original": query,
"rewritten": query,
"variations": variations
}
def _has_pronoun(self, query: str) -> bool:
"""检测指代词"""
pronouns = ["它", "他", "她", "这", "那", "这个", "那个", "这些", "那些"]
return any(p in query for p in pronouns)
def _resolve_coreference(self, query: str, history: list) -> str:
"""消除指代歧义"""
prompt = f"""
对话历史:
{self._format_history(history)}
当前问题:{query}
任务:将问题中的指代词替换为具体的实体,使问题可以独立理解。
示例:
历史:"什么是光合作用?"
当前:"它需要什么条件?"
改写:"光合作用需要什么条件?"
只返回改写后的问题,不要解释:
"""
rewritten = self.llm.generate(prompt)
return rewritten.strip()
def _expand_keywords(self, query: str) -> str:
"""关键词扩展"""
# 同义词扩展
synonyms = {
"是什么": ["定义", "概念", "含义"],
"为什么": ["原因", "原理", "机制"],
"怎么做": ["步骤", "过程", "方法"],
}
for key, values in synonyms.items():
if key in query:
# 不替换,而是补充
query = f"{query} {' '.join(values)}"
return query
def _generate_variations(self, query: str) -> list:
"""生成查询变体"""
prompt = f"""
原始问题:{query}
请生成3个不同角度的问法,帮助检索更全面的信息:
1. 更具体的问法
2. 更广泛的问法
3. 从不同角度的问法
格式:
1. [问法1]
2. [问法2]
3. [问法3]
"""
response = self.llm.generate(prompt)
variations = self._parse_variations(response)
return variations
# 使用示例
rewriter = QueryRewriter(llm)
# 场景1:指代消歧
history = [
{"question": "什么是光合作用?", "answer": "光合作用是..."}
]
query = "它需要什么条件?"
result = rewriter.rewrite_query(query, history)
print(f"原始:{result['original']}")
print(f"改写:{result['rewritten']}")
# 输出:光合作用需要什么条件?
# 场景2:生成变体
query = "光合作用需要什么?"
result = rewriter.rewrite_query(query)
print("查询变体:")
for var in result['variations']:
print(f" - {var}")
# 输出:
# - 光合作用的三个必要条件是什么?(具体)
# - 植物进行光合作用需要哪些因素?(广泛)
# - 没有什么条件植物就无法光合作用?(反向)
方案3:重排序(Re-ranking)⭐⭐⭐⭐⭐
原理:第一次粗排(快速),第二次精排(准确)
from sentence_transformers import CrossEncoder
class ReRanker:
"""重排序器 - 提升Top结果的准确性"""
def __init__(self):
# 使用交叉编码器(比向量相似度更准确)
self.cross_encoder = CrossEncoder(
'BAAI/bge-reranker-large', # 中文重排模型
max_length=512
)
def retrieve_and_rerank(self, query: str, vectorstore, k_initial: int = 20, k_final: int = 3):
"""
两阶段检索:
1. 召回20个候选(快速)
2. 精排选出3个(准确)
"""
# 阶段1:粗排 - 召回更多候选
candidates = vectorstore.similarity_search(query, k=k_initial)
# 阶段2:精排 - 使用CrossEncoder重新打分
pairs = [[query, doc.page_content] for doc in candidates]
scores = self.cross_encoder.predict(pairs)
# 按得分排序
ranked_results = sorted(
zip(candidates, scores),
key=lambda x: x[1],
reverse=True
)
# 返回Top K
top_docs = [doc for doc, score in ranked_results[:k_final]]
top_scores = [score for doc, score in ranked_results[:k_final]]
return top_docs, top_scores
# 使用示例
reranker = ReRanker()
query = "为什么植物晚上不进行光合作用?"
# 普通检索
normal_results = vectorstore.similarity_search(query, k=3)
# 带重排序的检索
reranked_results, scores = reranker.retrieve_and_rerank(
query, vectorstore, k_initial=20, k_final=3
)
print("普通检索:")
for doc in normal_results:
print(f" - {doc.page_content[:60]}...")
print("\n重排序后(更准确):")
for doc, score in zip(reranked_results, scores):
print(f" 分数:{score:.3f} - {doc.page_content[:60]}...")
效果对比:
普通检索(Top 3):
相似度0.72 - "植物的根系吸收营养..." ❌
相似度0.68 - "光合作用是植物利用光能..." ⚠️(相关但不是答案)
相似度0.65 - "光合作用需要光照..." ✅
重排序后(Top 3):
相关度0.95 - "光合作用需要光照,没有光就无法进行" ✅
相关度0.89 - "晚上没有光照,因此植物无法光合作用" ✅
相关度0.82 - "光是光合作用的必要条件" ✅
准确率:33% → 100% 🎉
方案4:元数据过滤(Metadata Filtering)⭐⭐⭐⭐
原理:根据元数据预过滤,缩小搜索范围
class MetadataFilterRetriever:
"""基于元数据的智能检索"""
def __init__(self, vectorstore):
self.vectorstore = vectorstore
def retrieve_with_filters(self, query: str, k: int = 3):
"""
根据查询意图自动添加过滤条件
"""
# 分析查询意图
filters = self._extract_filters(query)
# 带过滤的检索
if filters:
results = self.vectorstore.similarity_search(
query,
k=k,
filter=filters
)
else:
results = self.vectorstore.similarity_search(query, k=k)
return results
def _extract_filters(self, query: str) -> dict:
"""从查询中提取过滤条件"""
filters = {}
# 规则1:问题类型过滤
if any(kw in query for kw in ["是什么", "定义", "概念"]):
filters["content_type"] = "定义"
elif any(kw in query for kw in ["过程", "步骤", "怎么"]):
filters["content_type"] = "过程"
elif any(kw in query for kw in ["公式", "方程式", "计算"]):
filters["has_formula"] = True
# 规则2:科目过滤
subjects = {
"生物": ["光合作用", "细胞", "遗传", "进化"],
"物理": ["力", "运动", "能量", "电"],
"化学": ["反应", "元素", "化合物", "分子"],
}
for subject, keywords in subjects.items():
if any(kw in query for kw in keywords):
filters["subject"] = subject
break
# 规则3:难度过滤
if "基础" in query or "简单" in query:
filters["difficulty"] = "基础"
elif "高级" in query or "深入" in query:
filters["difficulty"] = "进阶"
return filters
# 使用示例
filter_retriever = MetadataFilterRetriever(vectorstore)
# 示例1:自动识别需要公式
query1 = "光合作用的化学方程式是什么?"
results1 = filter_retriever.retrieve_with_filters(query1)
# 自动添加 filter={"has_formula": True}
# 示例2:科目过滤
query2 = "什么是力?"
results2 = filter_retriever.retrieve_with_filters(query2)
# 自动添加 filter={"subject": "物理"}
# 示例3:内容类型过滤
query3 = "光合作用的过程是怎样的?"
results3 = filter_retriever.retrieve_with_filters(query3)
# 自动添加 filter={"content_type": "过程"}
方案5:假设性文档嵌入(HyDE)⭐⭐⭐⭐
原理:让LLM先生成假答案,用假答案去检索
class HyDERetriever:
"""假设性文档嵌入检索器"""
def __init__(self, llm, vectorstore):
self.llm = llm
self.vectorstore = vectorstore
def retrieve(self, query: str, k: int = 3):
"""
HyDE检索流程:
1. 让LLM生成假答案
2. 用假答案去检索
3. 返回真实文档
"""
# 步骤1:生成假设性答案
hypothetical_answer = self._generate_hypothetical_answer(query)
# 步骤2:用假答案检索(往往比用问题检索更准)
results = self.vectorstore.similarity_search(
hypothetical_answer,
k=k
)
return results
def _generate_hypothetical_answer(self, query: str) -> str:
"""生成假设性答案"""
prompt = f"""
请根据问题生成一个假设性的答案。
这个答案不需要完全准确,但要包含可能出现在真实答案中的关键词和表述方式。
问题:{query}
假设性答案(只返回答案,不要解释):
"""
response = self.llm.generate(prompt)
return response.strip()
# 使用示例
hyde = HyDERetriever(llm, vectorstore)
query = "为什么植物晚上不进行光合作用?"
# 普通检索
normal_results = vectorstore.similarity_search(query, k=3)
# HyDE检索
hyde_results = hyde.retrieve(query, k=3)
print("普通检索(用问题):")
for doc in normal_results:
print(f" - {doc.page_content[:60]}...")
print("\nHyDE检索(用假答案):")
print(f"生成的假答案:因为光合作用需要光照,晚上没有光...")
for doc in hyde_results:
print(f" - {doc.page_content[:60]}...")
为什么HyDE有效?
问题:"为什么植物晚上不进行光合作用?"
→ 向量:偏向"问题"的表述
→ 检索结果:可能是其他问题
假答案:"因为光合作用需要光照,晚上没有光,所以无法进行"
→ 向量:偏向"答案"的表述
→ 检索结果:真实的答案内容!
效果提升:20-30%
方案6:多查询融合(Multi-Query Fusion)⭐⭐⭐⭐
原理:从不同角度问问题,合并结果
class MultiQueryRetriever:
"""多查询融合检索器"""
def __init__(self, llm, vectorstore):
self.llm = llm
self.vectorstore = vectorstore
def retrieve(self, query: str, k: int = 3):
"""
多查询融合:
1. 生成多个查询变体
2. 分别检索
3. 融合去重
"""
# 步骤1:生成查询变体
queries = self._generate_multi_queries(query)
queries.append(query) # 加上原始问题
# 步骤2:每个查询分别检索
all_results = []
for q in queries:
results = self.vectorstore.similarity_search(q, k=k)
all_results.extend(results)
# 步骤3:去重并融合(RRF算法)
unique_results = self._reciprocal_rank_fusion(all_results)
return unique_results[:k]
def _generate_multi_queries(self, query: str, n: int = 3) -> list:
"""生成多个查询变体"""
prompt = f"""
原始问题:{query}
请从不同角度生成{n}个相关问题,帮助全面检索信息:
要求:
1. 每个问题角度不同
2. 都与原问题相关
3. 使用不同的关键词
格式(只返回问题,每行一个):
1. [问题1]
2. [问题2]
3. [问题3]
"""
response = self.llm.generate(prompt)
queries = self._parse_queries(response)
return queries[:n]
def _reciprocal_rank_fusion(self, documents: list, k: int = 60) -> list:
"""
倒数排序融合算法
来自多个检索器的结果融合
"""
# 计算每个文档的融合分数
doc_scores = {}
for rank, doc in enumerate(documents):
doc_id = doc.page_content # 使用内容作为唯一标识
# RRF公式:1 / (k + rank)
if doc_id not in doc_scores:
doc_scores[doc_id] = {
"doc": doc,
"score": 0
}
doc_scores[doc_id]["score"] += 1 / (k + rank + 1)
# 按分数排序
sorted_docs = sorted(
doc_scores.values(),
key=lambda x: x["score"],
reverse=True
)
return [item["doc"] for item in sorted_docs]
# 使用示例
multi_query = MultiQueryRetriever(llm, vectorstore)
query = "光合作用需要什么条件?"
results = multi_query.retrieve(query, k=3)
print("生成的查询变体:")
print(" 1. 原始:光合作用需要什么条件?")
print(" 2. 变体:光合作用的必要因素有哪些?")
print(" 3. 变体:没有什么植物就无法光合作用?")
print(" 4. 变体:影响光合作用的条件是什么?")
print("\n融合后的检索结果:")
for doc in results:
print(f" - {doc.page_content[:60]}...")
🎯 四、组合策略(推荐方案)
完整的检索Pipeline
class AdvancedRetriever:
"""高级检索器 - 组合多种策略"""
def __init__(self, llm, vectorstore, documents):
self.llm = llm
self.vectorstore = vectorstore
# 初始化各个组件
self.query_rewriter = QueryRewriter(llm)
self.hybrid_retriever = HybridRetriever(vectorstore, documents)
self.reranker = ReRanker()
self.metadata_filter = MetadataFilterRetriever(vectorstore)
def retrieve(self, query: str, conversation_history: list = None, k: int = 3):
"""
完整的检索Pipeline:
1. 查询改写(消歧、扩展)
2. 混合检索(向量+关键词)
3. 元数据过滤(可选)
4. 重排序(精排)
"""
# 步骤1:查询改写
rewrite_result = self.query_rewriter.rewrite_query(
query, conversation_history
)
rewritten_query = rewrite_result["rewritten"]
logger.info(f"原始查询: {query}")
logger.info(f"改写查询: {rewritten_query}")
# 步骤2:混合检索(召回20个候选)
candidates = self.hybrid_retriever.retrieve(rewritten_query, k=20)
# 步骤3:元数据过滤(可选)
filters = self.metadata_filter._extract_filters(query)
if filters:
candidates = [
doc for doc in candidates
if self._match_filters(doc.metadata, filters)
]
# 步骤4:重排序(精排到Top 3)
final_results, scores = self.reranker.retrieve_and_rerank(
rewritten_query,
candidates,
k_final=k
)
return {
"documents": final_results,
"scores": scores,
"original_query": query,
"rewritten_query": rewritten_query,
"candidates_count": len(candidates)
}
def _match_filters(self, metadata: dict, filters: dict) -> bool:
"""检查元数据是否匹配过滤条件"""
for key, value in filters.items():
if key not in metadata or metadata[key] != value:
return False
return True
# 使用示例
advanced_retriever = AdvancedRetriever(llm, vectorstore, all_documents)
# 场景:学生连续提问
history = [
{"question": "什么是光合作用?", "answer": "光合作用是..."}
]
query = "它需要什么条件?" # 有指代词
result = advanced_retriever.retrieve(query, history, k=3)
print(f"原始查询: {result['original_query']}")
print(f"改写查询: {result['rewritten_query']}")
print(f"召回候选: {result['candidates_count']}个")
print(f"\n最终结果(Top 3):")
for i, (doc, score) in enumerate(zip(result['documents'], result['scores']), 1):
print(f"\n{i}. 相关度: {score:.3f}")
print(f" 内容: {doc.page_content[:100]}...")
print(f" 来源: {doc.metadata.get('source')}, 页码: {doc.metadata.get('page')}")
📊 五、检索效果评估
评估指标
class RetrievalEvaluator:
"""检索效果评估器"""
def evaluate(self, test_set: list):
"""
评估检索质量
test_set格式:
[
{
"query": "什么是光合作用?",
"relevant_docs": ["doc_123", "doc_456"], # 标准答案
"retrieved_docs": ["doc_123", "doc_789", "doc_456"] # 检索结果
},
...
]
"""
metrics = {
"precision@k": 0, # 精确率
"recall@k": 0, # 召回率
"mrr": 0, # 平均倒数排名
"ndcg": 0, # 归一化折损累计增益
}
for item in test_set:
relevant = set(item["relevant_docs"])
retrieved = item["retrieved_docs"]
# 精确率@K:检索出的文档中有多少是相关的
relevant_retrieved = set(retrieved) & relevant
precision = len(relevant_retrieved) / len(retrieved) if retrieved else 0
# 召回率@K:相关文档中有多少被检索出来
recall = len(relevant_retrieved) / len(relevant) if relevant else 0
# MRR:第一个相关文档的排名倒数
for rank, doc_id in enumerate(retrieved, 1):
if doc_id in relevant:
mrr = 1 / rank
break
else:
mrr = 0
metrics["precision@k"] += precision
metrics["recall@k"] += recall
metrics["mrr"] += mrr
# 平均值
n = len(test_set)
for key in metrics:
metrics[key] /= n
return metrics
# 使用示例 - A/B测试不同策略
evaluator = RetrievalEvaluator()
# 准备测试集
test_set = load_test_set() # 100个标注好的问题
# 测试不同策略
strategies = {
"基础向量": basic_retriever,
"混合检索": hybrid_retriever,
"混合+重排": advanced_retriever,
}
print("检索策略对比:")
print("-" * 60)
for name, retriever in strategies.items():
# 执行检索
results = [retriever.retrieve(item["query"]) for item in test_set]
# 评估
metrics = evaluator.evaluate(results)
print(f"\n{name}:")
print(f" 精确率@3: {metrics['precision@k']:.2%}")
print(f" 召回率@3: {metrics['recall@k']:.2%}")
print(f" MRR: {metrics['mrr']:.3f}")
# 输出示例:
"""
基础向量:
精确率@3: 45.2%
召回率@3: 38.1%
MRR: 0.423
混合检索:
精确率@3: 72.5% ⬆️ +60%
召回率@3: 68.3% ⬆️ +79%
MRR: 0.687 ⬆️ +62%
混合+重排:
精确率@3: 89.6% ⬆️ +98%
召回率@3: 84.2% ⬆️ +121%
MRR: 0.856 ⬆️ +102%
"""
🎯 六、实战建议
优先级排序
第1优先:混合检索 ⭐⭐⭐⭐⭐
├─ 成本:低(无额外开销)
├─ 效果:+40-60%
└─ 实施:1小时
第2优先:重排序 ⭐⭐⭐⭐⭐
├─ 成本:中(需要额外模型)
├─ 效果:+20-40%
└─ 实施:2小时
第3优先:查询改写 ⭐⭐⭐⭐
├─ 成本:低(用现有LLM)
├─ 效果:+15-30%
└─ 实施:3小时
第4优先:元数据过滤 ⭐⭐⭐⭐
├─ 成本:中(需要标注元数据)
├─ 效果:+10-25%
└─ 实施:视数据量
第5优先:HyDE/多查询 ⭐⭐⭐
├─ 成本:高(多次LLM调用)
├─ 效果:+10-20%
└─ 实施:4小时
━━━━━━━━━━━━━━━━━━━━━━
组合使用:可达 +100-150% 提升!
实施检查清单
□ 阶段1:基础检索(1天)
✓ 实现基础向量检索
✓ 测试100个问题
✓ 记录基线指标
□ 阶段2:混合检索(1天)
✓ 添加BM25检索
✓ 实现RRF融合
✓ 对比效果提升
□ 阶段3:重排序(1天)
✓ 集成重排模型
✓ 调整候选数量
✓ 验证精度提升
□ 阶段4:查询优化(2天)
✓ 实现查询改写
✓ 处理指代消歧
✓ 添加关键词扩展
□ 阶段5:元数据增强(视情况)
✓ 标注内容类型
✓ 提取关键词
✓ 实现过滤逻辑
□ 阶段6:持续优化(长期)
✓ 收集失败案例
✓ A/B测试新策略
✓ 监控检索指标

浙公网安备 33010602011771号