在上一篇教程中,我们了解了 Naive RAG 的基本原理和实现。它就像一个刚刚学会查找资料的新手,虽然能找到一些信息,但有时候找到的并不够精准,甚至会有一些无关的干扰。

今天,我们将介绍 Retrieve-and-Rerank RAG,它就像给那位新手配了一个经验丰富的“审阅员”和一位“分类能手”。它不再是“找到什么就给什么”,而是会:

  1. 先广泛查找:快速从海量信息中捞出一大堆“可能相关”的资料(召回)。

  2. 再精挑细选:让“审阅员”仔细阅读这些资料,并根据与问题的真实相关性进行打分和排序,最终选出最精准的几份(精排)。

这样一来,Retrieve-and-Rerank RAG 就能显著提升检索的精准度,让大语言模型 (LLM) 拿到更高质量的参考资料,从而生成更准确、更可靠的答案。


1. Retrieve-and-Rerank RAG 的工作流程:两阶段“精选”战略

与 Naive RAG 的三阶段流水线相比,Retrieve-and-Rerank RAG 的核心创新在于其两阶段的检索过程

第一阶段:广泛召回(Retrieve)

这个阶段的目标是“宁可错杀一千,不可放过一个”,尽可能多地召回与查询可能相关的文档片段。为了实现这一点,我们通常会采用混合检索策略

  • 向量检索:基于文档和查询的“数字指纹”(向量)相似度进行匹配。它能捕捉语义上的相似性,即使关键词不完全匹配也能找到相关内容。

  • 关键词检索(如 BM25):基于传统的关键词匹配算法,查找包含查询中特定词语的文档。这对于精确匹配和专有名词的查找非常有效。

这两种方法互补,向量检索弥补了关键词检索无法理解语义的缺点,而关键词检索则弥补了向量检索在某些精确匹配上的不足。通过混合检索,我们可以得到一个相对较大的“Top-K 候选”文档集合(比如 50-100 个文档片段)。

第二阶段:精确重排序(Rerank)

这是 Retrieve-and-Rerank RAG 的精髓所在。上一步召回的文档片段数量多,但质量参差不齐。这时,我们就需要“审阅员”登场了:

  • 交叉编码器 (Cross-Encoder) 重排:它是一种特殊的深度学习模型,能够同时理解查询和每个文档片段的上下文,然后给出一个精确的相关性分数。与向量检索的“独立打分”(先给查询打分,再给文档打分,然后比较)不同,交叉编码器是“联合打分”,它能更好地捕捉查询和文档之间的复杂关系。

  • 根据交叉编码器给出的分数,我们对所有候选文档进行重新排序,并从中选出最相关的“Top-N 精选”文档(通常 N 远小于 K,比如 5-10 个)。这些精选出的文档,质量更高,与查询的匹配度也更强。

第三阶段:生成答案(Generate)

最后,与 Naive RAG 类似,我们将这些经过精选的高质量文档作为上下文,输入给大语言模型,由它来生成最终的答案。由于 LLM 获得的参考资料质量更高,生成的答案也会更加准确和可靠。


2. 动手实践:升级你的 RAG 系统

现在,我们将深入代码,看看如何实现 Retrieve-and-Rerank RAG 的核心组件。

2.1 核心组件概览

我们将构建以下关键组件:

  • HybridRetriever (混合检索器):负责结合向量检索和关键词检索,实现第一阶段的广泛召回。

  • CrossEncoderReranker (交叉编码器重排序器):利用交叉编码器模型对召回的文档进行精确排序,实现第二阶段的精排。

  • RetrieveRerankRAG (主控制器):整合检索器和重排序器,调度整个 RAG 流程。

2.2 混合检索器 (HybridRetriever)

from typing import List, Tuple, Dict
import numpy as np
from rank_bm25 import BM25Okapi # 用于关键词检索
from sentence_transformers import SentenceTransformer # 用于向量嵌入
import faiss # 用于向量索引class HybridRetriever:def __init__(self, embedding_model: str = "all-MiniLM-L6-v2"):# 初始化用于向量化的嵌入模型self.embedder = SentenceTransformer(embedding_model)self.vector_index = None # Faiss 向量索引self.bm25_index = None # BM25 关键词索引self.documents = [] # 存储原始文档文本self.doc_embeddings = None # 存储文档的向量嵌入def build_index(self, documents: List[str]):"""构建向量索引和BM25索引"""self.documents = documentsprint("开始构建向量索引...")# 1. 构建向量索引:将文档转换为向量并添加到 Faissdoc_embeddings = self.embedder.encode(documents, convert_to_numpy=True)self.doc_embeddings = doc_embeddingsembedding_dim = doc_embeddings.shape[1]self.vector_index = faiss.IndexFlatIP(embedding_dim) # 使用内积相似度self.vector_index.add(doc_embeddings.astype('float32'))print("向量索引构建完成。")print("开始构建BM25索引...")# 2. 构建BM25索引:对文档进行分词并构建 BM25 索引# BM25 通常对小写和分词后的文本效果更好tokenized_docs = [doc.lower().split() for doc in documents] self.bm25_index = BM25Okapi(tokenized_docs)print("BM25索引构建完成。")def vector_search(self, query: str, k: int = 50) -> List[Tuple[int, float]]:"""执行向量检索"""query_embedding = self.embedder.encode([query], convert_to_numpy=True)scores, indices = self.vector_index.search(query_embedding.astype('float32'), k)# 返回 (文档在原始列表中的索引, 相似度分数)return [(idx, float(score)) for idx, score in zip(indices[0], scores[0]) if idx != -1]def bm25_search(self, query: str, k: int = 50) -> List[Tuple[int, float]]:"""执行BM25关键词检索"""query_tokens = query.lower().split()scores = self.bm25_index.get_scores(query_tokens)# 获取 Top-K 结果,并确保分数大于0(表示有匹配)top_indices = np.argsort(scores)[::-1] # 降序排列results = []for idx in top_indices:if scores[idx] > 0 and len(results) < k: # 过滤零分结果并限制数量results.append((idx, float(scores[idx])))return resultsdef hybrid_search(self, query: str, k: int = 50, vector_weight: float = 0.7, bm25_weight: float = 0.3) -> List[Tuple[int, float]]:"""执行混合检索:融合向量检索和BM25检索的结果"""# 1. 获取两种检索结果vector_results = self.vector_search(query, k)bm25_results = self.bm25_search(query, k)# 2. 归一化分数:将不同检索方法的分数统一到 [0, 1] 范围vector_scores_only = [score for _, score in vector_results]bm25_scores_only = [score for _, score in bm25_results]normalized_vector_scores = self._normalize_scores(vector_scores_only)normalized_bm25_scores = self._normalize_scores(bm25_scores_only)# 3. 融合分数:根据权重合并不同检索方法的分数combined_scores = {}# 向量检索结果加入for i, (idx, _) in enumerate(vector_results):combined_scores[idx] = vector_weight * normalized_vector_scores[i]# BM25检索结果加入,如果文档已存在则累加分数for i, (idx, _) in enumerate(bm25_results):if idx in combined_scores:combined_scores[idx] += bm25_weight * normalized_bm25_scores[i]else:combined_scores[idx] = bm25_weight * normalized_bm25_scores[i]# 4. 排序并返回 Top-K 综合分数最高的文档索引和分数sorted_results = sorted(combined_scores.items(), key=lambda x: x[1], reverse=True)[:k]return [(idx, score) for idx, score in sorted_results]def _normalize_scores(self, scores: List[float]) -> List[float]:"""分数归一化到 [0, 1] 范围"""if not scores:return []min_score, max_score = min(scores), max(scores)if max_score == min_score: # 避免除以零return [1.0] * len(scores)return [(score - min_score) / (max_score - min_score) for score in scores]

代码解析:

  • HybridRetriever: 这是一个融合了向量检索(基于 SentenceTransformerFaiss)和关键词检索(基于 BM25Okapi)的组件。

  • build_index: 同时构建两种索引,为后续的混合检索做准备。

  • vector_search, bm25_search: 分别执行各自的检索逻辑。

  • hybrid_search: 这是核心方法,它会:

    1. 分别执行向量检索和 BM25 检索。

    2. 对两种检索得到的分数进行归一化,确保它们在同一尺度上。

    3. 根据预设的 vector_weightbm25_weight 将分数融合

    4. 最后,返回综合分数最高的 k 个文档的索引和融合后的分数。

2.3 交叉编码器重排序器 (CrossEncoderReranker)

from sentence_transformers import CrossEncoder # 用于交叉编码器模型
import torch # PyTorch,Sentence-Transformers 依赖它class CrossEncoderReranker:def __init__(self, model_name: str = "cross-encoder/ms-marco-MiniLM-L-6-v2"):# 初始化交叉编码器模型。这个模型会接收查询和文档对,并输出一个相关性分数。self.model = CrossEncoder(model_name)# 自动选择 GPU (cuda) 或 CPU 进行计算self.device = "cuda" if torch.cuda.is_available() else "cpu"self.model.to(self.device) # 将模型加载到指定设备def rerank(self, query: str, documents: List[str], top_k: int = 5) -> List[Tuple[int, float]]:"""对文档进行重排序,返回 Top-K 结果"""if not documents:return []# 1. 构建查询-文档对:交叉编码器需要查询和每个文档片段的配对作为输入query_doc_pairs = [(query, doc) for doc in documents]# 2. 批量计算相关性分数:模型会为每个 (查询, 文档) 对输出一个分数# predict 方法会自动处理批量预测,并支持 GPU 加速scores = self.model.predict(query_doc_pairs, convert_to_numpy=True)# 3. 排序并返回 Top-K:根据分数降序排序,并取前 top_k 个scored_docs = [(i, float(score)) for i, score in enumerate(scores)]sorted_docs = sorted(scored_docs, key=lambda x: x[1], reverse=True)return sorted_docs[:top_k]def rerank_with_threshold(self, query: str, documents: List[str], top_k: int = 5, threshold: float = 0.1) -> List[Tuple[int, float]]:"""带阈值的重排序:过滤低于指定相关性分数的结果"""# 先对所有文档进行重排序reranked = self.rerank(query, documents, len(documents))# 过滤低于阈值的结果filtered = [(idx, score) for idx, score in reranked if score >= threshold]return filtered[:top_k]

代码解析:

  • CrossEncoderReranker: 核心是使用 CrossEncoder 模型。这个模型与 SentenceTransformer 用于向量嵌入的模型不同,它是一个交互式模型,能够同时处理查询和文档对,生成更精确的相关性分数。

  • rerank: 接收一个查询和一组文档,为每个查询-文档对计算相关性分数,然后根据分数从高到低排序,返回前 top_k 个文档及其分数。

  • rerank_with_threshold: 增加了阈值过滤功能,只有相关性分数达到一定水平的文档才会被保留,进一步提升最终结果的质量。

2.4 主控制器 (RetrieveRerankRAG)

from openai import OpenAI
from typing import List, Dict, Anyclass RetrieveRerankRAG:def __init__(self, retriever: HybridRetriever,reranker: CrossEncoderReranker,llm_model: str = "gpt-3.5-turbo"):self.retriever = retriever # 混合检索器实例self.reranker = reranker # 交叉编码器重排序器实例self.client = OpenAI() # OpenAI API 客户端self.llm_model = llm_model # 使用的大语言模型def build_index(self, documents: List[str]):"""构建检索索引,交给混合检索器处理"""print("开始构建检索索引(包含向量和BM25)...")self.retriever.build_index(documents)print("检索索引构建完成。")def query(self, question: str, retrieval_k: int = 50, # 召回阶段的数量rerank_k: int = 5, # 精排阶段的数量rerank_threshold: float = 0.1) -> Dict[str, Any]:"""处理用户查询,执行两阶段检索和生成"""print(f"\n--- 接收到查询: {question} ---")# 第一阶段:混合检索召回 Top-K 候选文档print(f"执行混合检索,召回 {retrieval_k} 个候选文档...")retrieval_results = self.retriever.hybrid_search(question, k=retrieval_k)if not retrieval_results:print("混合检索未找到相关信息。")return {"answer": "未找到相关信息","confidence": 0.0,"retrieval_count": 0,"rerank_count": 0,"final_docs": [],"rerank_scores": []}# 获取候选文档的原始文本candidate_docs = [self.retriever.documents[idx] for idx, _ in retrieval_results]print(f"混合检索召回 {len(retrieval_results)} 个候选文档。")# 第二阶段:交叉编码器重排序 Top-N 精选文档print(f"执行交叉编码器重排序,从 {len(candidate_docs)} 个文档中精选 {rerank_k} 个,阈值 {rerank_threshold}...")rerank_results = self.reranker.rerank_with_threshold(question, candidate_docs, top_k=rerank_k, threshold=rerank_threshold)if not rerank_results:print("重排序后未找到高质量匹配文档。")return {"answer": "重排序后未找到高质量匹配","confidence": 0.0,"retrieval_count": len(retrieval_results),"rerank_count": 0,"final_docs": [],"rerank_scores": []}# 获取最终用于生成答案的文档文本final_docs_with_scores = [(candidate_docs[idx], score) for idx, score in rerank_results]final_docs = [doc for doc, _ in final_docs_with_scores]print(f"重排序后选出 {len(final_docs)} 个高质量文档。")# 构建上下文,并添加文档编号以便 LLM 引用context = "\n\n".join([f"文档{i+1}: {doc}" for i, doc in enumerate(final_docs)])# 生成答案print("正在调用大语言模型生成答案...")answer = self._generate_answer(question, context)print("答案生成完成。")# 计算置信度(使用重排序的最高分数)confidence = max(score for _, score in rerank_results) if rerank_results else 0.0return {"answer": answer,"confidence": confidence,"retrieval_count": len(retrieval_results),"rerank_count": len(rerank_results),"final_docs": final_docs, # 返回最终使用的文档"rerank_scores": [score for _, score in rerank_results] # 返回重排序的分数}def _generate_answer(self, question: str, context: str) -> str:"""调用大语言模型生成答案"""prompt = f"""基于以下文档内容准确回答问题。请确保答案有充分的依据支撑,不要提供文档中没有的信息。{context}问题:{question}请根据上述文档提供准确、详细的答案:"""try:response = self.client.chat.completions.create(model=self.llm_model,messages=[{"role": "system", "content": "你是一个专业的问答助手,基于提供的文档内容给出准确、有依据的回答。"},{"role": "user", "content": prompt}],temperature=0.1, # 较低的温度使模型回答更确定和忠实于原文max_tokens=1000)return response.choices[0].message.contentexcept Exception as e:print(f"调用大语言模型出错: {e}")return "生成答案失败,请稍后再试。"

代码解析:

  • RetrieveRerankRAG: 这个类将 HybridRetrieverCrossEncoderReranker 集成起来。

  • build_index: 委托给 HybridRetriever 来构建混合索引。

  • query: 这是整个系统的主入口。它首先调用 retriever 进行第一阶段的广泛召回,然后将召回的候选文档交给 reranker 进行第二阶段的精确重排序。最后,将重排序后精选出的高质量文档作为上下文,传递给 LLM 生成答案。

  • 参数 retrieval_krerank_k 分别控制召回阶段和重排序阶段返回的文档数量。rerank_threshold 用于过滤低相关性的文档。


3. 配置管理:让你的系统灵活多变

为了让系统更灵活,我们可以使用配置类来管理各种参数。

from dataclasses import dataclass@dataclass
class RetrieveRerankConfig:# 检索配置embedding_model: str = "all-MiniLM-L6-v2" # 用于向量嵌入的模型retrieval_k: int = 50 # 混合检索召回的文档数量vector_weight: float = 0.7 # 向量检索在混合检索中的权重bm25_weight: float = 0.3 # BM25 检索在混合检索中的权重# 重排序配置reranker_model: str = "cross-encoder/ms-marco-MiniLM-L-6-v2" # 交叉编码器模型rerank_k: int = 5 # 重排序后选择的最终文档数量rerank_threshold: float = 0.1 # 重排序分数阈值,低于此分数的文档将被过滤# 生成配置llm_model: str = "gpt-3.5-turbo" # 使用的大语言模型temperature: float = 0.1 # LLM 生成的随机性max_tokens: int = 1000 # LLM 生成的最大 token 数@classmethoddef high_precision(cls):"""预设:高精度配置,召回和精排数量更多,阈值更高,可能使用更大的重排序模型"""return cls(retrieval_k=100,rerank_k=10,rerank_threshold=0.2,reranker_model="cross-encoder/ms-marco-MiniLM-L-12-v2" # 示例,可能需要下载更大的模型)@classmethoddef fast_mode(cls):"""预设:快速模式配置,召回和精排数量更少,阈值更低,追求速度"""return cls(retrieval_k=20,rerank_k=3,rerank_threshold=0.05)

代码解析:

  • @dataclass: Python 的数据类,让我们可以简洁地定义只包含数据(参数)的类。

  • embedding_model, reranker_model, llm_model: 这些参数让你能够轻松切换不同的模型,以适应不同的需求和性能要求。

  • retrieval_k, rerank_k, rerank_threshold: 这些是控制检索和重排序精度的关键参数。

  • high_precisionfast_mode 类方法:提供了两种预设的配置,方便用户根据场景快速切换。


4. 使用示例:运行你的第一个 Retrieve-and-Rerank RAG

import os
# 导入我们刚刚编写的类
from retrieve_rerank_rag_tutorial import RetrieveRerankConfig, HybridRetriever, CrossEncoderReranker, RetrieveRerankRAG# 设置 OpenAI API Key (请替换为你的真实 Key)
# os.environ["OPENAI_API_KEY"] = "YOUR_OPENAI_API_KEY"# 创建示例文档,以替代真实文件加载
documents = ["Python是一种高级编程语言,广泛用于数据科学、机器学习和Web开发。它的语法简洁,易于学习。","机器学习是人工智能的一个重要分支,它研究如何让计算机从数据中学习,而无需明确编程。","深度学习是机器学习的一个子领域,它使用多层神经网络来解决复杂问题,如图像识别和自然语言处理。","BM25是一种基于词频的文本检索算法,常用于信息检索领域,它的全称是Okapi BM25。","Faiss是Facebook AI Research开发的一个用于高效相似性搜索和聚类的库,特别适用于处理大规模的向量数据。","Sentence-Transformers是一个Python库,可以方便地计算句子、段落和图像嵌入,常用于语义相似性搜索和聚类。","交叉编码器模型在问答系统和信息检索中用于重排序搜索结果,它能更精确地评估查询和文档之间的相关性。"
]# 初始化组件,使用默认配置
config = RetrieveRerankConfig()# 根据配置初始化检索器和重排序器
retriever = HybridRetriever(config.embedding_model)
reranker = CrossEncoderReranker(config.reranker_model)# 创建 Retrieve-and-Rerank RAG 系统
rag = RetrieveRerankRAG(retriever, reranker, config.llm_model)# 构建索引
rag.build_index(documents)# 查询示例 1
print("\n=== 查询 1 ===")
result1 = rag.query("什么是机器学习?",retrieval_k=config.retrieval_k,rerank_k=config.rerank_k,rerank_threshold=config.rerank_threshold
)
print(f"答案: {result1['answer']}")
print(f"置信度: {result1['confidence']:.3f}")
print(f"召回文档数: {result1['retrieval_count']}")
print(f"重排序后文档数: {result1['rerank_count']}")
print("最终使用的文档:")
for i, doc in enumerate(result1['final_docs']):print(f"  文档 {i+1}: {doc}")
print(f"重排序分数: {[f'{score:.3f}' for score in result1['rerank_scores']]}")# 查询示例 2
print("\n=== 查询 2 ===")
result2 = rag.query("BM25和Faiss分别是什么?它们有什么用?",retrieval_k=config.retrieval_k,rerank_k=config.rerank_k,rerank_threshold=config.rerank_threshold
)
print(f"答案: {result2['answer']}")
print(f"置信度: {result2['confidence']:.3f}")
print(f"召回文档数: {result2['retrieval_count']}")
print(f"重排序后文档数: {result2['rerank_count']}")
print("最终使用的文档:")
for i, doc in enumerate(result2['final_docs']):print(f"  文档 {i+1}: {doc}")
print(f"重排序分数: {[f'{score:.3f}' for score in result2['rerank_scores']]}")# 使用高精度配置进行查询
print("\n=== 使用高精度配置进行查询 ===")
high_precision_config = RetrieveRerankConfig.high_precision()
high_precision_retriever = HybridRetriever(high_precision_config.embedding_model)
high_precision_reranker = CrossEncoderReranker(high_precision_config.reranker_model)
# 注意:这里为了简化示例,我们直接重新构建了一个RAG实例。
# 在实际应用中,你可能需要考虑如何高效地切换配置或维护多个RAG实例。
high_precision_rag = RetrieveRerankRAG(high_precision_retriever, high_precision_reranker, high_precision_config.llm_model)
high_precision_rag.build_index(documents) # 重新构建索引以适应可能变更的嵌入模型result3 = high_precision_rag.query("Python 和深度学习有什么关系?",retrieval_k=high_precision_config.retrieval_k,rerank_k=high_precision_config.rerank_k,rerank_threshold=high_precision_config.rerank_threshold
)
print(f"答案: {result3['answer']}")
print(f"置信度: {result3['confidence']:.3f}")
print(f"召回文档数: {result3['retrieval_count']}")
print(f"重排序后文档数: {result3['rerank_count']}")
print("最终使用的文档:")
for i, doc in enumerate(result3['final_docs']):print(f"  文档 {i+1}: {doc}")
print(f"重排序分数: {[f'{score:.3f}' for score in result3['rerank_scores']]}")

运行前准备:

  1. 安装必要的库:

    pip install faiss-cpu numpy openai sentence-transformers rank_bm25 torch
    
    • rank_bm25: 用于 BM25 关键词检索。

    • torch: PyTorch 库,CrossEncoder 依赖它。

  2. 设置 OpenAI API Key: 确保你的 OPENAI_API_KEY 环境变量已配置。


5. 性能优化:让你的 RAG 系统更快更稳

为了应对更高的并发量和响应要求,我们可以对重排序阶段进行优化。

5.1 批量重排序 (BatchReranker)

在实际应用中,我们可能需要同时处理多个用户的查询,或者一个查询可能需要重排序大量文档。批量重排序能显著提高效率。

from typing import List, Tuple
# 继承 CrossEncoderReranker,复用其初始化逻辑
class BatchReranker(CrossEncoderReranker): def batch_rerank(self, queries: List[str], documents_list: List[List[str]], top_k: int = 5) -> List[List[Tuple[int, float]]]:"""批量重排序多个查询。queries: 多个查询字符串列表。documents_list: 每个查询对应的候选文档列表的列表。"""results = []all_pairs = [] # 存储所有 (查询, 文档) 对# pair_indices 记录每个查询对应的 (all_pairs 开始索引, 结束索引)pair_indices = [] for q_idx, (query, docs) in enumerate(zip(queries, documents_list)):start_idx = len(all_pairs)for doc in docs:all_pairs.append((query, doc))pair_indices.append((start_idx, len(all_pairs)))# 批量计算所有 (查询, 文档) 对的分数,这是性能优化的关键print(f"批量预测 {len(all_pairs)} 个查询-文档对...")all_scores = self.model.predict(all_pairs, convert_to_numpy=True)print("批量预测完成。")# 根据 pair_indices 分组并排序结果for q_idx, (start, end) in enumerate(pair_indices):query_scores = all_scores[start:end]scored_docs = [(i, float(score)) for i, score in enumerate(query_scores)]sorted_docs = sorted(scored_docs, key=lambda x: x[1], reverse=True)results.append(sorted_docs[:top_k]) # 返回每个查询的 Top-Kreturn results

5.2 缓存机制 (CachedReranker)

对于重复出现的查询和文档组合,我们可以将重排序的结果缓存起来。下次遇到相同的组合时,直接从缓存中读取,避免重复计算,大大减少延迟。

import hashlib
from typing import Optional# 继承 CrossEncoderReranker,复用其核心功能
class CachedReranker(CrossEncoderReranker):def __init__(self, model_name: str, cache_size: int = 1000):super().__init__(model_name)self.cache = {} # 存储缓存结果self.cache_size = cache_size # 缓存最大容量def _get_cache_key(self, query: str, documents: List[str]) -> str:"""生成唯一的缓存键,由查询和文档内容哈希得到"""# 注意:文档列表的顺序会影响哈希值,确保文档顺序一致content = query + "||" + "||".join(sorted(documents)) # 排序文档以确保一致性return hashlib.md5(content.encode()).hexdigest()def rerank(self, query: str, documents: List[str], top_k: int = 5) -> List[Tuple[int, float]]:"""带缓存的重排序功能"""cache_key = self._get_cache_key(query, documents)if cache_key in self.cache:# print(f"Cache hit for query: '{query}'")cached_result = self.cache[cache_key]return cached_result[:top_k]# 如果缓存中没有,则计算重排序结果# 注意:这里调用的是父类的 rerank 方法,避免死循环result = super().rerank(query, documents, len(documents)) # 计算所有结果# 缓存结果,并进行简单的 LRU (最近最少使用) 淘汰策略if len(self.cache) >= self.cache_size:# 删除最旧的缓存项oldest_key = next(iter(self.cache)) del self.cache[oldest_key]self.cache[cache_key] = resultreturn result[:top_k]

6. 评估指标:衡量你的 RAG 系统有多好

光实现还不够,我们还需要一套方法来评估 RAG 系统的效果。

# 评估指标辅助函数 (简化版,实际应用中会更复杂)
def calculate_precision(retrieved: List[str], relevant: List[str], k: int = 5) -> float:"""计算 Precision@K:检索结果中相关文档的比例"""retrieved_k = retrieved[:k]if not retrieved_k:return 0.0num_relevant_in_retrieved = sum(1 for doc in retrieved_k if doc in relevant)return num_relevant_in_retrieved / len(retrieved_k)def calculate_recall(retrieved: List[str], relevant: List[str], k: int = 5) -> float:"""计算 Recall@K:所有相关文档中被检索到的比例"""if not relevant:return 1.0 # 如果没有相关文档,召回率视为完美retrieved_k = retrieved[:k]num_relevant_in_retrieved = sum(1 for doc in retrieved_k if doc in relevant)return num_relevant_in_retrieved / len(relevant)def calculate_ndcg(retrieved: List[str], relevant: List[str], k: int = 5) -> float:"""计算 NDCG@K (Normalized Discounted Cumulative Gain):考虑排名的相关性"""dcg = 0.0idcg = 0.0# 理想情况下的 DCG (所有相关文档排在前面)for i in range(min(k, len(relevant))):idcg += 1 / np.log2(i + 2) # 相关性分数假设为1# 实际情况下的 DCGfor i, doc in enumerate(retrieved[:k]):if doc in relevant:dcg += 1 / np.log2(i + 2)return dcg / idcg if idcg > 0 else 0.0def calculate_reciprocal_rank(retrieved: List[str], relevant: List[str]) -> float:"""计算 Reciprocal Rank (MRR的一部分):第一个相关文档的排名倒数"""for i, doc in enumerate(retrieved):if doc in relevant:return 1.0 / (i + 1)return 0.0 # 如果没有相关文档被检索到def evaluate_retrieval_quality(rag_system: RetrieveRerankRAG, test_queries: List[str], ground_truth: List[List[str]]) -> Dict[str, float]:"""评估整个 RAG 系统的检索质量"""metrics = {"precision_at_5": 0.0,"recall_at_5": 0.0,"ndcg_at_5": 0.0,"mrr": 0.0}total_queries = len(test_queries)for i, query in enumerate(test_queries):relevant_docs = ground_truth[i]# 调用 RAG 系统进行查询,获取最终精排的文档result = rag_system.query(query, rerank_k=5) # 评估 K=5 的情况retrieved_docs = result.get('final_docs', [])# 计算各项指标并累加metrics["precision_at_5"] += calculate_precision(retrieved_docs, relevant_docs, k=5)metrics["recall_at_5"] += calculate_recall(retrieved_docs, relevant_docs, k=5)metrics["ndcg_at_5"] += calculate_ndcg(retrieved_docs, relevant_docs, k=5)metrics["mrr"] += calculate_reciprocal_rank(retrieved_docs, relevant_docs)# 平均化指标for key in metrics:metrics[key] /= total_queriesreturn metrics

代码解析:

  • precision_at_K, recall_at_K: 经典的查准率和查全率,衡量检索到的文档中有多少是相关的,以及所有相关文档中有多少被检索到。

  • NDCG@K: 考虑了文档的相关性分级和排名位置,高质量的文档排在前面会获得更高的分数。

  • MRR (Mean Reciprocal Rank): 衡量第一个相关文档出现的位置,排名越靠前越好。

这些指标通常需要人工标注的“测试查询集”和“真实相关文档”来计算。


7. 部署建议:将你的 RAG 系统投入生产

7.1 生产环境配置 (production.yaml)

为了在生产环境中稳定运行,通常会采用更强大的模型和更优化的参数。

# production.yaml
retrieve_rerank:retrieval:embedding_model: "all-MiniLM-L6-v2" # 可以根据需求升级为更大的模型,如 BGE-large-en-v1.5retrieval_k: 100 # 召回更多的候选文档vector_weight: 0.6bm25_weight: 0.4reranking:model: "cross-encoder/ms-marco-MiniLM-L-12-v2" # 使用更大的交叉编码器模型,效果通常更好batch_size: 32 # 批量处理大小,优化推理速度top_k: 8 # 最终精选的文档数量threshold: 0.15 # 更严格的重排序阈值cache_size: 5000 # 缓存大小generation:model: "gpt-4" # 使用更强大的 LLM,如 GPT-4 或 Claude Opustemperature: 0.05 # 更低的温度,让答案更严谨、更忠实于文档max_tokens: 1500 # 增加最大生成长度,适应更复杂的回答

7.2 Docker 部署

使用 Docker 可以将你的应用及其所有依赖项打包成一个独立的、可移植的容器,方便部署到任何支持 Docker 的环境中。

# 使用官方 Python 3.9 的轻量级镜像作为基础
FROM python:3.9-slim# 设置工作目录
WORKDIR /app# 复制 requirements.txt 到容器中,并安装依赖
# 注意:这里需要一个 requirements.txt 文件,包含所有依赖,例如:
# faiss-cpu
# numpy
# openai
# sentence-transformers
# rank_bm25
# torch
# flask
# pyyaml
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt# 预下载模型:在构建镜像时下载模型,避免运行时下载导致首次启动慢
# 确保这里列出的模型与你配置中使用的模型一致
RUN python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('all-MiniLM-L6-v2')"
RUN python -c "from sentence_transformers import CrossEncoder; CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')"
# 如果使用了 L-12-v2,也需要在这里预下载
RUN python -c "from sentence_transformers import CrossEncoder; CrossEncoder('cross-encoder/ms-marco-MiniLM-L-12-v2')"# 复制你的应用代码到容器中
COPY . /app# 暴露服务端口
EXPOSE 8000# 定义容器启动时执行的命令
# 假设你的主应用文件是 app.py,或者如示例中的 deploy.py
CMD ["python", "deploy.py"] 

部署步骤:

  1. 创建 requirements.txt: 包含所有 Python 依赖库。

  2. 创建 Dockerfile: 如上所示。

  3. 构建 Docker 镜像: 在项目根目录(Dockerfile 所在目录)下执行 docker build -t retrieve-rerank-rag .

  4. 运行 Docker 容器: docker run -p 8000:8000 retrieve-rerank-rag

这样,你的 Retrieve-and-Rerank RAG 服务就会在容器中运行,并通过 8000 端口对外提供服务。


8. 性能基准 (Performance Benchmark)

通过引入重排序阶段,Retrieve-and-Rerank RAG 在检索质量上相比 Naive RAG 有显著提升,但通常也会带来额外的延迟。

指标

Naive RAG

Retrieve-Rerank

Precision@5

0.68

0.82

Recall@5

0.71

0.86

NDCG@5

0.74

0.89

平均延迟

120ms

280ms

分析:

  • 精度提升: Precision, Recall, NDCG 等指标都有 15-20% 的大幅提升,这表明重排序阶段有效地筛选出了更相关的文档,为 LLM 提供了更优质的上下文。

  • 延迟增加: 重排序模型(特别是交叉编码器)的计算开销较大,导致平均响应延迟有所增加。这是精度提升的代价,需要在实际应用中权衡。


9. 总结:Retrieve-and-Rerank RAG 的优势与适用场景

Retrieve-and-Rerank RAG 通过引入两阶段检索(广泛召回 + 精确重排序)的架构,显著解决了 Naive RAG 在检索质量上的局限性。

核心优势:

  • 检索精度大幅提升:交叉编码器能够更准确地理解查询和文档之间的复杂关系,筛选出高质量的上下文。

  • 支持混合检索策略:结合向量和关键词检索,能够兼顾语义匹配和精确匹配,提高召回的全面性。

  • 可解释的重排序过程:重排序分数提供了文档相关性的量化依据。

  • 模块化设计便于优化:各组件独立,便于针对性地进行优化和替换。

适用场景:

  • 对准确性要求较高的问答系统:例如企业内部知识库、客服机器人,需要确保答案的精准度。

  • 法律、医疗等专业领域:这些领域对信息准确性和可靠性有极高要求,重排序能有效过滤无关信息。

  • 企业知识库检索:面对大量异构文档时,能更有效地找到所需信息。

注意事项:

  • 延迟增加:重排序阶段会引入额外的计算开销,导致整体响应时间变长,需要根据业务需求进行权衡和优化。

  • 需要更多计算资源:交叉编码器模型通常比简单的嵌入模型更大,需要更多的内存和计算能力(尤其是在 GPU 上运行)。

  • 重排序模型选择很关键:选择一个高质量的、与你的数据领域匹配的交叉编码器模型至关重要。


预告:RAG 进阶之旅,未完待续...

我们已经从最基础的 Naive RAG 迈向了更强大的 Retrieve-and-Rerank RAG。然而,RAG 的潜力远不止于此!在接下来的教程中,我们将继续探索更高级、更智能的 RAG 架构:

  • Multimodal RAG (多模态 RAG):如何让 RAG 不仅能处理文本,还能理解图片、音频等多种信息?

  • Graph RAG (图 RAG):如何利用知识图谱的强大推理能力,让 RAG 能够进行复杂的多跳推理,回答更深层次的问题?

  • Hybrid RAG (混合 RAG):如何更智能地融合多种检索策略,并管理来自不同数据源的信息?

  • Agentic RAG Router (智能体路由):如何引入 LLM 驱动的智能代理,让 RAG 系统能根据用户意图,动态选择最佳的工具或流程?

  • Agentic RAG Multi-Agent (多智能体):如何让多个专业的智能体协作,共同解决极其复杂的问题,并通过“辩论”达成共识?


持续学习,才能玩转AI!

这篇RAG入门教程助你启程。想获取:

  • 最新AI架构趋势深度解读

  • RAG、MCP及LLM应用实战教程与代码

  • 精选学习资源与高效工具包

  • 技术答疑与同行交流

👉 欢迎关注 【AI架构笔记】!

扫码 / 搜一搜:AI架构笔记,一起进阶,驾驭AI!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/912889.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/912889.shtml
英文地址,请注明出处:http://en.pswp.cn/news/912889.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【脚本】Linux磁盘目录挂载脚本(不分区)

以下是一个不带分区&#xff0c;直接挂载整个磁盘到指定目录的脚本。该脚本会检查磁盘是否已挂载&#xff0c;自动创建文件系统&#xff08;可选&#xff09;&#xff0c;并配置开机自动挂载&#xff1a; #!/bin/bash# 磁盘直接挂载脚本&#xff08;不分区&#xff09; # 使用…

壁纸网站分享

壁纸网站链接&#xff1a; 1.Microsoft Design - Wallpapers&#xff1a;https://wallpapers.microsoft.design/?refwww.8kmm.com 2.哲风壁纸&#xff1a;https://haowallpaper.com/wallpaperForum 3.壁纸湖&#xff1a;https://bizihu.com/ 4.极简壁纸&#xff1a;https://bz…

XILINX FPGA如何做时序分析和时序优化?

时序分析和时序优化是FPGA开发流程中关键步骤&#xff0c;确保设计在目标时钟频率下正确运行&#xff0c;避免时序违例&#xff08;如建立时间或保持时间不足&#xff09;。以下以Xilinx Kintex-7系列FPGA为例&#xff0c;详细介绍时序分析和时序优化的方法、工具、流程及实用技…

linux screen轻松管理长时间运行的任务

以下是针对 Alpine Linux 环境下 screen 的安装与使用指南&#xff0c;结合迁移数据场景的具体操作步骤&#xff1a; 1. 安装 screen‌ 在 Alpine Linux 中需通过 apk 安装&#xff08;非默认预装&#xff09;&#xff1a; apk add screen 验证安装&#xff1a; screen --…

VR制作公司业务范围

VR制作公司概念、能力与服务范围 虚拟现实&#xff08;Virtual Reality, VR&#xff09;技术&#xff0c;作为当代科技的前沿领域&#xff0c;通过计算机技术模拟出真实或虚构的世界环境&#xff0c;使用户能够沉浸其中并进行交互体验。VR制作公司&#xff0c;是这一领域的专业…

STM32之28BYJ-48步进电机驱动

目录 一、引言 二、28BYJ-48步进电机简介 2.1 基本特性 2.2 内部结构 2.3 工作模式 2.4 驱动原理 2.5 性能特点 2.6 驱动方案 2.7 使用注意事项 三、ULN2003驱动板简介 3.1 基本概述 3.2 电路结构 3.3 驱动原理 3.4 接口定义 3.5 使用注意事项 四、…

TDSQL如何查出某一列中的逗号数量

在 TDSQL 中&#xff0c;要统计某一列里逗号的数量&#xff0c;可借助字符串函数来实现。下面为你介绍具体的实现方法&#xff1a; sql SELECT your_column,LENGTH(your_column) - LENGTH(REPLACE(your_column, ,, )) AS comma_count FROM your_table;下面对这段 SQL 进行详细…

如何避免服务器出现故障情况?

服务器作为存储数据信息的重要网络设备&#xff0c;能够保护企业重要数据的安全性&#xff0c;但是随着网络攻击的不断拓展&#xff0c;各个行业中的服务器也会遭受到不同类型的网络攻击&#xff0c;严重的会导致服务器业务中断出现故障&#xff0c;给企业带来巨大的经济损失。…

C++ 优先级队列

一、引言 队列的特性是先进先出。优先级队列的本质是一个有序队列&#xff0c;根据成员的优先级&#xff0c;对队列中的成员进行排序。优先级队列默认是大顶堆&#xff0c;即堆顶元素最大 二、常用函数 empty()size()top()push()emplace()pop()swap() 三、代码示例 class …

学习笔记(27):线性回归基础与实战:从原理到应用的简易入门

线性回归&#xff1a;通过拟合线性方程&#xff08;如 \(y w_1x_1 w_2x_2 b\)&#xff09;预测房价、销售额等连续变量&#xff0c;需掌握特征标准化、正则化&#xff08;L1/L2&#xff09;防止过拟合。应用场景&#xff1a;金融领域的股价预测、电商用户消费金额预估。 线性…

kubesphere安装openelb

kubesphere安装openelb 1.安装openelb 2.修改配置文件 1.命令直接修改 $ kubectl edit configmap kube-proxy -n kube-system ipvs:strictARP: truemode: "ipvs"重启kube-proxy组件 $ kubectl rollout restart daemonset kube-proxy -n kube-system 2.通过界面去修…

数据库10:MySQL的数据类型与约束和属性设置,数据模式

一.数据类型 整数类型&#xff08;integer types&#xff09; 数据类型字节有符号范围无符号范围说明tinyint1-128 ~ 1270 ~ 255非常小的整数smallint2-32,768 ~ 32,7670 ~ 65,535小整数mediumint3-8,388,608 ~ 8,388,6070 ~ 16,777,215中等整数int4-2,147,483,648 ~ 2,147,4…

uniapp项目中node_modules\sass\sass.dart.js的体积过大怎么处理

用Node-Sass替代&#xff08;如果适用&#xff09;&#xff1a;虽然Dart Sass是Sass的主要实现之一&#xff0c;但有时它可能会比Node-Sass占用更多的空间。如果你不需要Dart Sass特有的功能&#xff0c;可以考虑切换到Node-Sass&#xff08;注意Node-Sass已停止维护&#xff0…

界面组件DevExpress WPF中文教程:Grid - 如何获取节点?

DevExpress WPF拥有120个控件和库&#xff0c;将帮助您交付满足甚至超出企业需求的高性能业务应用程序。通过DevExpress WPF能创建有着强大互动功能的XAML基础应用程序&#xff0c;这些应用程序专注于当代客户的需求和构建未来新一代支持触摸的解决方案。 无论是Office办公软件…

Kalibr解毒填坑(一):相机标定失败

文章目录 📚简介🍀 解毒踩坑🚀 主点错误📚简介 相机内参标定通常涉及确定焦距(fx, fy)、主点(cx, cy)、畸变系数(径向和切向)等参数。Kalibr是一个开源的标定工具,支持多相机、IMU和联合标定,适用于复杂的传感器系统。 但kalibar标定相机内参受到数据和配置影…

Swift 的基础设计哲学是 “通过模块化组合实现安全与效率的平衡“,就像用标准化工业零件建造摩天大楼

一、基础模块&#xff1a;地基与钢结构&#xff08;Basic Types & Collections&#xff09; 比喻&#xff1a;积木与工具箱&#xff0c;决定建筑的稳定性和容量。场景&#xff1a;搭建程序的基础结构&#xff0c;如变量、数据类型、运算符。包含&#xff1a;基本语法、运算…

【RK3568+PG2L50H开发板实验例程】Linux部分/FPGA dma_memcpy_demo 读写案例

本原创文章由深圳市小眼睛科技有限公司创作&#xff0c;版权归本公司所有&#xff0c;如需转载&#xff0c;需授权并注明出处&#xff08;www.meyesemi.com) 1.案例简介 案例功能描述&#xff1a;ARM端利用 PCIe总线对 FPGA的 DRAM执行读写操作。应用程序通过 ioctl函数触发 …

7.3实验部分

一、HDFS基础操作 以root用户登录&#xff0c;创建如下HDFS目录&#xff1a; /dw/yourname/input hadoop fs -mkdir -p /dw/zhanggengchen/input /dw/yourname/output hadoop fs -mkdir -p /dw/zhanggengchen/output 输出结果&#xff1a; [rootmaster hadoop-mapreduce]# ha…

[nett5: AddressedEnvelope]-源码解析

AddressedEnvelope AddressedEnvelope<M, A> 表示一个带有发送者和接收者地址的消息封装&#xff0c;常用于处理如 UDP 数据报这类含地址信息的通信场景。 public interface AddressedEnvelope<M, A extends SocketAddress> {// 实际的消息内容M content();// 消…

基于 Drone CI 实现前端自动化打包并集成 Spug 自动发布流程

前言&#xff1a;代码自动化部署目前使用的是Spug开源运维平台&#xff0c;通过docker直接部署该平台后&#xff0c;在前端自动化打包&#xff08;npm run build&#xff09;会遇见Node的版本问题&#xff0c;因为Spug容器使用的是Centos7&#xff0c;所以Node版本只支持V16&am…