超越余弦相似度:五种RAG检索技术的对比分析
通过代码讲解余弦相似度、BM25、混合RRF、交叉编码器重排序及MMR,分析其优缺点,并提供一种以查询优先的方式来确定RAG需要哪种检索技术。
来源: **Beyond Cosine Similarity: The 5 RAG Retrieval Techniques That Actually Move the Needle**
“猫坐在垫子上”和“一只猫科动物躺在地毯上”这两句话完全没有相同的词汇,但嵌入模型给它们的相似度评分却约为0.85。这就是余弦相似度获得盛名的原因,也是大多数RAG教程的结束点:展示余弦相似度、粘贴一段文本,然后就结束了。
collection.query(query_embeddings=embedding, n_results=5)
那种方法确实有效。但它只是五种检索技术中的一种,每种技术都是为不同类型的问题设计的。仅依赖余弦相似度意味着整类查询会无声无息地失败,不会产生任何错误,用户只能得到错误的答案。下面将介绍余弦相似度实际测量的内容、它的局限性以及另外四种技术,还会提供一种选择合适技术的方法。
余弦相似度测量的是什么
嵌入模型会将文本转换为向量,即由数百或数千个数字构成的列表。含义相似的文本对应的向量方向也会相同。余弦相似度比较的是两个向量之间的夹角,而非它们之间的距离。
这才是关键所在。一句简短的句子和一段完整的段落可能表达相同的意思。如果它们的向量方向一致,余弦相似度就会给出较高的分数。向量大小所代表的长度只是干扰因素,而方向才蕴含意义。
典型的得分如下:
| 句子对 | 得分 |
|---|---|
| “猫坐在垫子上”与“一只猫科动物躺在地毯上” | 0.85 |
| “注意力是如何工作的?”与“解释Transformer机制” | 0.79 |
| “Python编程语言”与“蛇类中的蟒蛇” | 0.38 |
| “我喜欢晨间咖啡”与“量子纠缠理论” | 0.03 |
第一行正是余弦相似度令人惊叹之处——这两句话没有共同词汇,得分却高达0.85。模型已经认识到“cat”和“feline”在语义空间中处于相近位置,而余弦相似度无需任何关键词匹配就能捕捉到这一点。
为何不用欧几里得距离
显而易见的替代方案是欧几里得距离,即两个向量之间的实际距离。问题在于:如果将同一份文档重复十次,其对应的向量大小也会增大十倍,因此尽管内容没有任何变化,它看起来却与原文档相去甚远。而余弦相似度则不会考虑这一点,只有方向才是重要的。
关于得分的警示
余弦相似度得分并非通用标准。all-MiniLM-L6-v2模型给出的0.7分与text-embedding-3-large模型给出的0.7分含义不同,因为每个模型都有属于自己的嵌入空间及密度分布。请在自身的评估数据集上设定合适的得分阈值,切勿直接照搬使用其他模型的教程中的数值。
为非工程师准备的节日类比
在了解代码之前,先建立对这五种技术的概念模型。想象你身处一场音乐节上,要在人群中寻找朋友,此时你有三种可用的策略。
- 通过方向判断(余弦值)。双方都面朝舞台。只要方向相同,无论相距多远都是同一个人。重要的是指针的方向,距离则无关紧要。
- 通过姓名牌判断(关键词搜索,BM25算法)。你喊“有叫亚历克斯的人吗?”,如果姓名牌完全匹配则能准确找到,但那些使用不同名称的相同人物,如亚历克西斯、艾尔或桑迪,则会被漏掉。
- 通过面容和姓名牌判断(混合方式)。同时考虑两者。同时符合条件的人会排在最前面,仅符合其中一项条件的人也会被注意到。这种方式覆盖范围最广。
大多数教程都忽略了一个要点:没有哪种方法能找到所有人。你选择的技术决定了你会永久错过哪些答案以及哪些人。
五种技术,逐一介绍
1. 余弦相似度,默认选项
作用。比较查询向量与文档向量在语义上的方向。
适用场景。查询为自然语言且无需精确匹配的术语:通用RAG系统、基于文档的问答系统,以及那些存在大量同义表述的场景。
局限性。对于字面字符串,产品代码、发票号码或在训练数据中极少出现的人名都可能导致余弦相似度计算完全失效。“GPT-4o”与“GPT-4”的向量可能距离过于接近,从而产生噪声而非有效信号。
代码示例:
collection.query(
query_embeddings=query_embedding,
n_results=5
)
权衡取舍。 意义优先于精确的措辞。这种方法速度快、准确度尚可,且无需额外设置。
2. BM25关键词搜索:精准度工具
工作原理。 根据查询词在文档中出现的频率为文档打分,同时会根据这些词在整个语料库中的普遍程度进行折减。它不使用嵌入模型,仅依靠计数。
适用场景。 当查询依赖于必须逐字符匹配的精确标识符时,比如发票号、带有发布日期的型号名称或CVE编号。
缺陷所在。 完全无法理解语义。“Feline”不会与写有“cat”的文档匹配,同义词和意译内容对其而言也是不可见的。
一个具有代表性的结果。在一个小型从零开始的项目中,将BM25与余弦相似度算法结合使用后,召回率从0.25提升到了0.75。其原因在于实际用户查询中包含精确的产品名称,而余弦相似度算法会将这些名称视为近似匹配而非直接匹配。这只是一个数据点,不能作为普遍规律。
权衡取舍。以完全丧失语义泛化能力为代价来获得精确匹配的精度。
3. 混合搜索——生产环境中的默认方案
其工作原理。同时运行余弦相似度算法和BM25,然后通过互反排名融合(RRF)方法将两个排序结果列表合并。单独使用任一算法都无法覆盖所有类型的查询。
适用场景。当查询同时包含自然语言表达和特定术语时,这种技术就非常适用,而几乎所有的生产系统都符合这种情况。例如,用户询问“订单#8821的退款政策是什么?”时,其中一部分是语义查询(退款政策),另一部分则是精确匹配查询(#8821)。
融合步骤。每种技术都会生成自己的排序列表。RRF会根据文档在各个列表中的位置为其分配分数,计算公式为1除以(k加上排名),其中k是一个常数,通常为60,随后会将各种技术给出的分数相加。在多个列表中排名都较高的文档会位居前列。
# Pseudocode for RRF merge
def rrf_score(rank, k=60):
return 1 / (k + rank)
def merge_results(cosine_results, bm25_results):
scores = {}
for rank, doc in enumerate(cosine_results):
scores[doc.id] = scores.get(doc.id, 0) + rrf_score(rank)
for rank, doc in enumerate(bm25_results):
scores[doc.id] = scores.get(doc.id, 0) + rrf_score(rank)
return sorted(scores.items(), key=lambda x: x[1], reverse=True)
在 AWS 上,OpenSearch 可以原生支持此功能:您只需设置带有权重参数的混合查询,它便会内部处理融合操作。
权衡点。虽然检索成本大约会增加一倍,但能获得最佳的总体召回率,这通常来说是值得的。
4. 使用交叉编码器进行重排序
工作原理。采用两阶段方法。第一阶段通过余弦相似度或混合搜索获取排名前20的候选项;第二阶段将每个候选项与查询一起输入交叉编码器,该编码器能同时考虑两者来评估相关性。
适用场景。当顶级结果虽然相关但顺序有误,且精确度比延迟更重要的时候。此方法应用于第二阶段,绝不能在初次检索时就使用。
为何要分两个阶段?交叉编码器在判断相关性方面的表现远优于余弦相似度,但无法对整个数据库进行遍历处理。使用20次模型调用而非1次是可以接受的,但在查询时使用1万次则不可行。
from sentence_transformers import CrossEncoder
reranker = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')# Stage 1: retrieve candidates
candidates = collection.query(query_embeddings=embedding, n_results=20)# Stage 2: rerank
pairs = [(query, chunk) for chunk in candidates['documents'][0]]
scores = reranker.predict(pairs)
top_5 = sorted(zip(scores, candidates['documents'][0]), reverse=True)[:5]
严肃警告。切勿将重排序作为第一阶段处理方式。其延迟成本极高,因为需要用二十次完整模型推理来替代一次嵌入查找。
权衡点。虽然精度最高,但成本也最大,因此仅应作为第二阶段使用。
5. 最大边际相关性(MMR)以实现多样性
工作原理。在查询相似度与已选内容的一致性之间进行权衡。每个新选内容必须具有相关性,且与已有选择不同。
适用场景。当所有顶级内容都在重复相同信息时,例如针对宽泛的总结类问题,所有结果都在阐述同一事实。
lambda参数调节。
lambda = 1.0表示仅考虑相关性,与标准余弦相似度相同
lambda = 0.0表示纯粹的多样性,完全忽略相关性lambda = 0.5是一个可供调整的平衡起点def mmr_select(query_embedding, candidates, lambda_param=0.5, k=5):
selected = []
remaining = list(candidates)
while len(selected) < k and remaining:
mmr_scores = []
for doc in remaining:
relevance = cosine_similarity(query_embedding, doc.embedding)
if selected:
redundancy = max(cosine_similarity(doc.embedding, s.embedding)
for s in selected)
else:
redundancy = 0
mmr_scores.append(lambda_param * relevance - (1 - lambda_param) * redundancy)
best = remaining[mmr_scores.index(max(mmr_scores))]
selected.append(best)
remaining.remove(best)
return selected
适用场景。适用于需要概括内容、提出研究问题,或是读者希望涵盖多个观点而非深入探讨某一主题的情况。
权衡点。该方法能消除冗余,但需对参数进行调整。
选择方式:让查询结果来决定
- 80%及以上为普通自然语言。此时余弦相似度算法是最佳选择,因为它速度快、准确率高且无需额外基础设施。
- 30%及以上包含精确词汇、代码、编号或专有名词。此时应加入BM25算法。一旦出现这种模式,混合搜索将成为生产环境的首选方案,因此需实现RRF融合技术。这样通常能快速显著提升检索覆盖率。
教程中未提及的内容
各种技术手段之间可能存在冲突,而非简单叠加效果。在某次实验中,人们在混合检索的基础上再加入查询扩展功能——即为同一查询生成多个变体并分别进行检索——本以为这能带来更好效果。更高的覆盖率理应意味着更优的结果,但实际上分数从 0.807 下降到了 0.702。过多的检索信号既会放大有用信息,也会增加噪声,而且额外的查询变体会引入相关但错误的内容。混合检索本身已表现良好,而查询扩展功能反而带来了干扰。应一次只添加一种技术,在组合使用前分别测试其效果,并记住顺序很重要,因为单独时有用的方法在结合使用时可能会适得其反。
点积并非余弦值。某些向量数据库默认使用点积作为计算方式。只有当向量经过单位归一化处理后,点积与余弦值才会相等。如果你的嵌入模型没有对其输出进行归一化,不同查询的点积得分就无法相互比较,因此某个查询得到0.9分而另一个也得到0.9分时,其实际含义是不同的。请确认你的数据库究竟使用何种计算方式。例如,ChromaDB通常被认为采用与Qdrant不同的默认度量标准,因此需在各自的最新文档中加以核实。
是查询决定了使用何种技术,而非文档本身。工程师们常常花费时间将语料库分类为技术性或对话型、长文本或短文本。这些分类对分块处理很重要,但在选择检索技术时,查询分析更为关键。用自然语言问题搜索技术性语料库属于余弦相似度问题,而用部件编号搜索同一语料库则属于混合类型问题。
完整流程
以下是许多实际应用的RAG系统最终采用的分层设计:
User Query
↓
[Optional: HyDE — if query is vague]
↓
Stage 1 Retrieval: Hybrid (Cosine + BM25 → RRF merge)
→ Top 20 candidates
↓
[Optional: Cross-encoder reranking — if precision matters]
→ Top 5 candidates
↓
[Optional: MMR — if results are redundant]
→ Final k candidates
↓
LLM Context Assembly + Generation
并非每个系统都需要所有层次的功能。先从余弦相似度开始,当出现完全匹配失败时再加入混合搜索算法,排序出错时加入重排机制,结果重复时加入MMR算法。每一步都要进行评估,因为对某个系统有效的方案可能会对另一个系统造成负面影响。
关键数字
检索实验的报告通常会显示,在混合查询集中,仅使用余弦相似度的搜索与混合搜索之间的召回率存在差距,有个小型项目中的这一差距从0.25上升到了0.75。即便这一差距在您的数据中只是大致存在,也意味着在无需提示工程优化、模型升级或基础设施改造的情况下,语言模型在生成第一个字之前就能获得大约三倍数量的正确答案。归根结底,关键在于做出正确的检索选择。