RAG检索策略完全指南

RAG检索策略完全指南

检索是RAG系统的灵魂!检索不准,再好的LLM也白搭。让我给你一个完整的检索优化方案。


🎯 一、检索的本质问题

核心挑战

问题的本质:
用户问题:"为什么植物晚上不进行光合作用?"
    ↓
向量化:[0.123, -0.456, 0.789, ...]
    ↓
在向量数据库中找相似向量
    ↓
找到的可能是:
  ❌ "植物的根系吸收营养的过程..." (相似度0.72)
  ❌ "植物细胞的结构包括..." (相似度0.68)
  ✅ "光合作用需要光照,没有光就无法进行" (相似度0.65)
    
结果:最相关的内容反而分数最低!

为什么会这样?

  1. 词汇鸿沟:用户问法 ≠ 文档表述
  2. 语义漂移:向量相似 ≠ 语义相关
  3. 上下文丢失:单个chunk缺少完整信息
  4. 噪音干扰:无关内容污染检索结果

🔍 二、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测试新策略
  ✓ 监控检索指标

posted @ 2026-01-16 17:28  XiaoZhengTou  阅读(3)  评论(0)    收藏  举报