在上一篇教程中,我們了解了 Naive RAG 的基本原理和實現。它就像一個剛剛學會查找資料的新手,雖然能找到一些信息,但有時候找到的并不夠精準,甚至會有一些無關的干擾。
今天,我們將介紹 Retrieve-and-Rerank RAG,它就像給那位新手配了一個經驗豐富的“審閱員”和一位“分類能手”。它不再是“找到什么就給什么”,而是會:
-
先廣泛查找:快速從海量信息中撈出一大堆“可能相關”的資料(召回)。
-
再精挑細選:讓“審閱員”仔細閱讀這些資料,并根據與問題的真實相關性進行打分和排序,最終選出最精準的幾份(精排)。
這樣一來,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
: 這是一個融合了向量檢索(基于SentenceTransformer
和Faiss
)和關鍵詞檢索(基于BM25Okapi
)的組件。 -
build_index
: 同時構建兩種索引,為后續的混合檢索做準備。 -
vector_search
,bm25_search
: 分別執行各自的檢索邏輯。 -
hybrid_search
: 這是核心方法,它會:-
分別執行向量檢索和 BM25 檢索。
-
對兩種檢索得到的分數進行歸一化,確保它們在同一尺度上。
-
根據預設的
vector_weight
和bm25_weight
將分數融合。 -
最后,返回綜合分數最高的
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
: 這個類將HybridRetriever
和CrossEncoderReranker
集成起來。 -
build_index
: 委托給HybridRetriever
來構建混合索引。 -
query
: 這是整個系統的主入口。它首先調用retriever
進行第一階段的廣泛召回,然后將召回的候選文檔交給reranker
進行第二階段的精確重排序。最后,將重排序后精選出的高質量文檔作為上下文,傳遞給 LLM 生成答案。 -
參數
retrieval_k
和rerank_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_precision
和fast_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']]}")
運行前準備:
-
安裝必要的庫:
pip install faiss-cpu numpy openai sentence-transformers rank_bm25 torch
-
rank_bm25
: 用于 BM25 關鍵詞檢索。 -
torch
: PyTorch 庫,CrossEncoder
依賴它。
-
-
設置 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"]
部署步驟:
-
創建
requirements.txt
: 包含所有 Python 依賴庫。 -
創建 Dockerfile: 如上所示。
-
構建 Docker 鏡像: 在項目根目錄(
Dockerfile
所在目錄)下執行docker build -t retrieve-rerank-rag .
-
運行 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!