前言:开启新篇章
欢迎来到《RAG 每日一技》的全新篇章------高级技巧篇!在过去九天里,我们已经从零到一构建并评估了一个功能完备的RAG系统。它很棒,但还不够"顶"。从今天起,我们将一起探索如何将它打磨成一个更强大、更鲁棒的"完全体"。
让我们从一个问题开始:我们引以为傲的向量检索,真的没有弱点吗?
想象一下,你向你的RAG系统提问:"请介绍一下BM25算法"。
你可能会惊讶地发现,系统返回的结果是一些关于"信息检索"、"搜索算法"的泛泛之谈,而那篇专门定义"BM25"的文章,可能排在很后面,甚至根本没被召回!
这就是纯向量检索的"死穴":它精通语义,却不擅长关键词 。对于那些特定的、稀有的、在向量空间中没有被精确建模的词(如产品型号 iPhone 14 Pro
、算法名称 BM25
、人名 Geoffrey Hinton
),向量检索往往表现不佳。
而这,恰恰是"老派"的关键词搜索的强项。那么,我们能否将两者结合,取长补短呢?
答案是肯定的。这项技术,就叫做混合搜索(Hybrid Search)。
什么是混合搜索?
混合搜索,顾名思义,就是将两种或多种搜索技术结合起来,以期获得比任何单一技术都更好的搜索结果。在RAG领域,它特指结合:
- 关键词搜索(Lexical Search) :基于词频、文档频率等统计信息,进行精确的字面匹配。最经典的算法是 BM25。
- 向量搜索(Semantic Search):基于语义向量的相似度,进行模糊的概念匹配。我们之前一直用的就是这种。
搜索类型 | 优点 | 缺点 |
---|---|---|
关键词搜索 (BM25) | 对关键词、专有名词、代码片段等精确匹配极佳。 | 无法理解同义词、近义词,无法理解句子意图。 |
向量搜索 | 能深刻理解语义和用户意图,能处理同义词等。 | 对关键词不敏感,有时会忽略文本的字面细节。 |
混合搜索的目标,就是同时利用两者的优点,实现1+1>2的效果。
融合的艺术:Reciprocal Rank Fusion (RRF)
现在我们有了两个搜索引擎,它们分别返回了两个不同的排序列表。我们该如何将这两个列表合并成一个最终的、更权威的排序呢?
这里介绍一种非常简单且高效的融合算法:倒数排名融合(Reciprocal Rank Fusion, RRF)。
RRF的计分公式极其简单:对于每一个文档,它的最终分数是它在所有 排名列表中分数的总和。而它在每个 列表中的分数被计算为 1 / (k + rank)
。
rank
是文档在该列表中的排名(从1开始)。k
是一个常数(通常设为60),用于降低低排名结果的影响。
为什么RRF好用?
- 无需归一化:BM25的分数和向量相似度分数范围天差地别,直接相加毫无意义。RRF只关心"排名",完全忽略了原始分数,完美避开了这个问题。
- 兼顾并包:一个文档即使只在一个列表中排名很高,也能获得不错的分数。如果它在两个列表中都排名很高,那它的分数就会非常高。这使得结果既全面又精准。
上手实战:从零实现一个混合搜索
我们将用Python代码,结合 rank-bm25
库来实现一个迷你的混合搜索系统。
首先,安装必要的库:
bash
pip install rank-bm25
python
from rank_bm25 import BM25Okapi
# --- 准备工作 ---
# 我们的文档库
documents = [
"向量数据库ChromaDB是一个对开发者友好的工具。",
"BM25是一种基于词频的信息检索算法,常用于关键词搜索。",
"混合搜索结合了关键词搜索和向量搜索的优点。",
"如何使用Reciprocal Rank Fusion(RRF)算法融合搜索结果?",
"RAG系统的核心是检索与生成。"
]
# 用户查询
query = "RRF算法和BM25"
# --- 1. 关键词搜索 (BM25) ---
# BM25需要先对文档进行分词
tokenized_docs = [doc.split(" ") for doc in documents]
bm25 = BM25Okapi(tokenized_docs)
tokenized_query = query.split(" ")
bm25_scores = bm25.get_scores(tokenized_query)
# 获取BM25的排序结果(文档索引和分数)
bm25_results = sorted(enumerate(bm25_scores), key=lambda x: x[1], reverse=True)
print("BM25 结果 (index, score):", bm25_results)
# --- 2. 向量搜索 (模拟) ---
# 在真实应用中,这里会调用ChromaDB或FAISS
# 我们这里手动模拟一个结果,假设向量搜索更理解"算法"这个概念
vector_results = [
(3, 0.95), # RRF算法
(1, 0.90), # BM25算法
(2, 0.80), # 混合搜索
(4, 0.70), # RAG系统
(0, 0.50) # ChromaDB
]
print("向量搜索结果 (index, score):", vector_results)
# --- 3. RRF 融合 ---
def reciprocal_rank_fusion(search_results_lists, k=60):
fused_scores = {}
# 遍历每一个搜索结果列表
for doc_list in search_results_lists:
# 遍历列表中的每一个文档(及其排名)
for rank, (doc_index, _) in enumerate(doc_list):
if doc_index not in fused_scores:
fused_scores[doc_index] = 0
# 计算RRF分数并累加
fused_scores[doc_index] += 1 / (k + rank + 1) # rank从0开始,所以+1
# 按RRF分数对文档进行降序排序
reranked_results = sorted(fused_scores.items(), key=lambda x: x[1], reverse=True)
return reranked_results
# 将BM25和向量搜索的结果列表传入RRF函数
final_ranking = reciprocal_rank_fusion([bm25_results, vector_results])
# --- 4. 查看最终结果 ---
print("\n--- 最终混合搜索排名 ---")
for doc_index, score in final_ranking:
print(f"Score: {score:.4f}\tDoc: {documents[doc_index]}")
结果分析: 你会发现,文档1(BM25...
)和文档3(...RRF算法...
)在最终排名中会名列前茅。因为它们各自在一个搜索列表中排名极高,在另一个列表中也尚可,通过RRF融合后,它们的分数会超越那些只在单个列表中表现突出的文档。这就是混合搜索的威力!
总结与预告
今日小结:
- 纯向量搜索对关键词不敏感是其一大弱点。
- 混合搜索通过结合关键词搜索(如BM25)和向量搜索,实现了优势互补。
- Reciprocal Rank Fusion (RRF) 是一种简单、高效、无需归一化的融合算法,是实现混合搜索的利器。
我们通过混合搜索,让召回的文档列表更加可靠了。但是,我们处理文档的方式还比较"粗糙"------我们只是把检索到的文档块(Chunks)直接丢给LLM。
如果一个问题的答案,恰好分散在多个不同的文档块里呢?我们有没有办法先让模型"阅读并总结"这些文档块,形成一个更精炼、更全面的中间答案,再最终生成回答呢?
明天预告:RAG 每日一技(十一):只检索还不够爽?迭代式文档精炼(Refine)了解一下!
明天,我们将学习一种更高级的RAG工作流。它不再是一次性地将所有上下文抛给LLM,而是让LLM像人一样,逐一阅读检索到的文档,并不断地"提炼"和"更新"自己的答案。敬请期待!