上一篇我們用向量檢索找出了幾個相似的 chunk。但有時候即使找到了 top_k 的候選文件,相關性的排序也還不是最完美的。有些內容雖然有相關,但對回答問題的幫助可能不大。
這時候,就可以進行 Reranking 重排序!
圖片來源:https://www.linkedin.com/pulse/cross-encoder-vector-search-re-ranking-viktor-qvarfordt-fnmzf
Reranking 是一個二次篩選機制,它會再用另一個模型來判斷候選 chunk 與 問題 的相關性,然後重新給每個 chunk 打分數,把最有價值的資訊排在前面。
這裡我們採用的是 Cross-Encoder(交叉編碼器) 架構。
它會將 「問題」和「文件」一起輸入模型,讓模型直接判斷兩者的語意關聯程度,並輸出一個相關性分數。
這種方式的優點是,模型能同時理解問題與文件之間的語境,判斷會更精準。但相對地,它的缺點就是每次都要讓問題與每個候選文件配對計算一次,因此運算成本較高,不適合用在即時處理大量資料的場景。
我們今天要實作的部分就是:
本系列實作的架構圖:
ithomeNLP_RAG/
├── requirements.txt
├── data/
├── db/ # qdrant vector db
├── indexer.py # store vector
├── retriever.py # vector search
├── reranker.txt # rerank
└── frontend.py # generation + UI
我們今天會做的是 reranker.py
from FlagEmbedding import FlagReranker
from retriever import VectorStore
EMBEDDING_MODEL = "BAAI/bge-m3"
RERANK_MODEL = "BAAI/bge-reranker-base"
def rerank(query: str, results: list):
reranker = FlagReranker(
RERANK_MODEL,
cache_dir=".",
use_fp16=False
)
docs = [res['content'] for res in results]
# 生成 query-document pair
pairs = [[query, doc] for doc in docs]
# 計算相關性分數
scores = reranker.compute_score(pairs)
reranked_results = []
for score, res in sorted(zip(scores, results), key=lambda x: x[0], reverse=True):
rerank_result = {
'similarity_score': float(score), # 使用 reranker 分數
'source': res.get('source', ''),
'chunk_index': res.get('chunk_index', ''),
'content': res.get('content', '')
}
reranked_results.append(rerank_result)
return reranked_results
def main(query, top_k):
vector_store = VectorStore()
search_results = vector_store.search(
query=query,
top_k=top_k
)
unranked_list = search_results["results"]
reranked_list = rerank(query, unranked_list)
return unranked_list, reranked_list
if __name__ == "__main__":
query = "tfidf算法是什麼?"
top_k = 10
original_results, reranked_results = main(query, top_k)
# 印出原本結果
print(" \n=== Original Documents ===")
for i, res in enumerate(original_results[:5], 1):
print(f"{i}. Source: {res.get('source', '')}, Score: {res.get('similarity_score', 0):.4f}")
# 印出 rerank 結果
print("\n=== Reranked Documents ===")
for i, res in enumerate(reranked_results[:5], 1):
print(f"{i}. Source: {res.get('source', '')}, Score: {res.get('similarity_score', 0):.4f}")
=== Original Documents ===
1. Source: day11.txt, Score: 0.5558
2. Source: day11.txt, Score: 0.5507
3. Source: day11.txt, Score: 0.5499
4. Source: day11.txt, Score: 0.5368
5. Source: day11.txt, Score: 0.4771
=== Reranked Documents ===
1. Source: day11.txt, Score: 0.4533
2. Source: day11.txt, Score: 0.3088
3. Source: day11.txt, Score: 0.1541
4. Source: day11.txt, Score: -0.0640
5. Source: day11.txt, Score: -1.1218
今天我們用 Cross-Encoder 的架構做了 Reranking 的任務。就像前面說的,Cross-Encoder 的計算量其實蠻大的,所以它通常被放在最後一步「重排序」的階段,來對前面初步檢索出的文件做更精細的評估,挑出 更符合查詢意圖的內容 來生成最終答案!
那走到今天我們終於算是正式完成了 RAG 中,Retrival 的部分了!
明天我們就要進入 Generation(生成),讓 LLM 來生成最終答案。然後我們也會做一個簡單的互動式介面來完成我們的 RAG 系統大合體!!
我們明天見啦~~~