首页 / 文章

超越Top-K相似度:RAG中的BM25、混合搜索、重排与MMR技术

RAG中的超越Top-K相似度技术:包括用于精确匹配项的BM25、结合RRF的混合搜索、用于多样性的交叉编码器重排序以及MMR,这些技术被整合为单一处理流程。

发布者:REACTAPP.TOP阅读约 7 分钟1698 词

来源: Retrieval Strategies in RAG: Beyond Basic Similarity Search

大多数开发者构建的第一个RAG系统都遵循相同的流程:将文档嵌入向量数据库中,当有查询出现时,取出最相似的文档片段并传递给大语言模型。这种方法起初能正常工作,但后来会出现问题。用户开始抱怨有些显而易见的答案被遗漏了——文档中明明包含相关信息,但检索系统却无法找出来;或者虽然最相关的结果表面上看与查询相似,但实际上并不符合用户的需求。简单的相似度搜索虽是个不错的起点,但也存在明显局限,一旦这些局限显现,就需要更优的检索策略来解决。下文将阐述该方法失败的原因、混合搜索与重排序技术如何解决这些问题,以及如何将这些组件整合为一个完整的处理流程。

简要介绍相似度搜索

在基本配置中,用户的提问会被转化为一个嵌入向量,即一组代表其含义的数字。向量存储中的每个数据块也有对应的嵌入向量。系统会通过余弦相似度或点积来找出与提问嵌入向量最接近的那些数据块,然后返回排名靠前的K个结果。

# Basic similarity search — what most RAG systems do
results = vector_store.similarity_search(query, k=3)

它适用于一般的语义匹配场景,表现相当不错。即使表述不同,关于光合作用原理的查询也能找到描述植物将阳光转化为能量的相关内容。但它也存在缺陷。

其不足之处

精确的关键词会被忽略。语义搜索侧重于捕捉含义而非字面词汇,这通常是个优势。然而当有人搜索特定术语,如产品名称、缩写或人名时,嵌入向量可能无法实现足够精准的匹配。例如用户询问BERT模型架构,虽然存在说明BERT使用双向变换器的内容,但由于涉及更广泛的语言模型架构主题的内容在语义上更为宽泛,其排名可能会更高。

高分并不等同于最佳答案。相似度衡量的是两段文本之间的关联程度,而非其中一段对回答另一段问题的实用程度。那些说明RAG系统适用于问答的文本片段与查询“什么是RAG?”极为相似,而包含实际定义的文本片段则更具实用性。

排名靠前的结果也可能存在重复内容。真实文档在不同文本片段中会重复出现相同信息,因此前三名的内容往往大同小异,从而浪费上下文窗口的空间在重复内容上。

用于关键词检索的BM25算法

BM25是一种经典的信息检索算法,也是传统搜索引擎所采用的技术。它通过精确匹配关键词来筛选文档,会根据某个关键词在整个语料库中的出现频率为其赋予不同权重,且不使用嵌入向量,仅依赖原始文本。搜索“FAISS index”时,返回的结果会包含同时含有这两个词的文本片段,其中出现频率较低的词会比常见的词具有更高的权重。

from langchain_community.retrievers import BM25Retriever
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
# Load and split documents
loader = TextLoader("knowledge_base.txt")
documents = loader.load()
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
chunks = splitter.split_documents(documents)
# Create BM25 retriever
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 3
# Search
results = bm25_retriever.invoke("FAISS vector index")
for doc in results:
    print(doc.page_content[:200])

它擅长处理具体的产品名称、人物或技术术语,以及简短精准的查询,凡是精确的用词比含义更重要的场景都能很好地应对。但当用户的表达方式与文档中的不同(使用同义词或意译)、查询内容过长,或是含义比字面意思更重要的时候,它就难以发挥作用。

混合搜索

混合搜索同时运行语义检索和BM25算法。语义搜索负责处理含义和同义词,而BM25则处理精确的关键词,二者结合可以覆盖单独使用任一算法时会遗漏的情况。这两个结果列表会通过互反排名融合(RRF)技术合并,该技术无需让各列表的评分处于同一尺度,即可将它们整合为一个结果列表。

from langchain_community.retrievers import BM25Retriever
from langchain_google_genai import GoogleGenerativeAIEmbeddings
from langchain_community.vectorstores import FAISS
from langchain.retrievers import EnsembleRetriever
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import TextLoader
#Load and split
loader = TextLoader("knowledge_base.txt")
documents = loader.load()
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
chunks = splitter.split_documents(documents)
#Retriever 1 — Semantic (FAISS)
embeddings = GoogleGenerativeAIEmbeddings(
    model="models/embedding-001",
    google_api_key="YOUR_KEY"
)
vector_store = FAISS.from_documents(chunks, embeddings)
semantic_retriever = vector_store.as_retriever(search_kwargs={"k": 5})
#Retriever 2 — Keyword (BM25)
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 5
#Combine both — Hybrid Search
hybrid_retriever = EnsembleRetriever(
    retrievers=[semantic_retriever, bm25_retriever],
    weights=[0.6, 0.4]  # 60% semantic, 40% keyword
)
#Search
results = hybrid_retriever.invoke("What is FAISS and how does it store vectors?")
print(f"Retrieved {len(results)} chunks")
for i, doc in enumerate(results):
    print(f"\nChunk {i+1}: {doc.page_content[:150]}")

weights参数用于设定每个检索器所贡献的权重。在大多数情况下,略微偏向语义搜索较为合适,但最佳平衡点需根据具体数据而定。当文档中包含技术术语、产品名称或专有名词,或者用户同时提出对话式与关键词式的查询时,又或者希望在语义理解之外再以关键词匹配作为保障时,应选择混合检索方式。

重新排序

重新排序分为两个阶段。首先进行广泛检索,使用检索器获取排名靠前的20到30个结果,这种方式成本低且速度快。随后将这些候选结果送入专用的重新排序模型,该模型会根据每个结果回答问题的质量为其打分,最终仅保留最优秀的3到5个结果。

其原理在于,向量相似度是对相关性的一种快速近似,而重新排序算法虽然速度较慢,但精度要高得多。先通过相似度进行筛选,再重新排序,这样既能保证速度,又能提升精度。

from langchain_community.cross_encoders import HuggingFaceCrossEncoder
from langchain.retrievers.document_compressors import CrossEncoderReranker
from langchain.retrievers import ContextualCompressionRetriever
#Base retriever — fetch top 20 candidates
base_retriever = vector_store.as_retriever(search_kwargs={"k": 20})
#Reranker model
reranker_model = HuggingFaceCrossEncoder(
    model_name="BAAI/bge-reranker-base"
)
#Reranker compressor — keeps only top 3 after reranking
reranker = CrossEncoderReranker(model=reranker_model, top_n=3)
#Combine base retriever + reranker
reranking_retriever = ContextualCompressionRetriever(
    base_compressor=reranker,
    base_retriever=base_retriever
)
#Search — fetches 20, reranks, returns top 3
results = reranking_retriever.invoke("How does FAISS perform similarity search?")
print(f"Final chunks after reranking: {len(results)}")
for i, doc in enumerate(results):
    print(f"\nTop {i+1}: {doc.page_content[:200]}")

交叉编码重排器将问题与某个文本片段视为一对,判断该特定片段与特定问题的相关性。向量相似度则是分别分析每段文本,而不会直接考量它们之间的关联。当精确度至关重要且错误答案会造成严重后果、需要在特定文档上执行问答任务,或是答案质量会直接影响用户体验时,应采用重排技术。

最大边际相关性

MMR算法旨在解决冗余问题。它不会仅仅选择最相似的片段,而是挑选既相关又具有多样性的片段,从而避免重复返回相同信息。

# Standard similarity search — might return redundant chunks
standard_results = vector_store.similarity_search(query, k=3)
# MMR search — returns relevant AND diverse chunks
mmr_results = vector_store.max_marginal_relevance_search(
    query,
    k=3,           # final number of chunks to return
    fetch_k=20,    # candidates to consider before selecting diverse ones
    lambda_mult=0.5  # 0 = max diversity, 1 = max relevance
)

lambda_mult参数用于调整两者之间的平衡。值为1.0时表示纯粹的相关性,与普通的相似度搜索相同;值为0.0时表示纯粹的多样性,即让各个片段尽可能不同;值为0.5时可兼顾两者,通常是较好的初始设置。当文档具有重复性、需要涵盖某个主题的多个方面,或上下文窗口过窄而无法容纳重复片段时,MMR方法能发挥优势。

完整的处理流程

综合来看,混合检索方式能广泛搜索,重排机制可筛选出最相关的片段,而完整的RAG流程则用于生成最终答案。

from langchain_google_genai import ChatGoogleGenerativeAI, GoogleGenerativeAIEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_community.retrievers import BM25Retriever
from langchain_community.cross_encoders import HuggingFaceCrossEncoder
from langchain.retrievers import EnsembleRetriever, ContextualCompressionRetriever
from langchain.retrievers.document_compressors import CrossEncoderReranker
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import TextLoader
# Setup
llm = ChatGoogleGenerativeAI(model="gemini-1.5-flash", google_api_key="YOUR_KEY")
embeddings = GoogleGenerativeAIEmbeddings(model="models/embedding-001", google_api_key="YOUR_KEY")
# Load and split documents
loader = TextLoader("your_knowledge_base.txt")
documents = loader.load()
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
chunks = splitter.split_documents(documents)
# Build retrievers
vector_store = FAISS.from_documents(chunks, embeddings)
semantic_retriever = vector_store.as_retriever(search_kwargs={"k": 10})
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 10
# Hybrid retriever
hybrid_retriever = EnsembleRetriever(
    retrievers=[semantic_retriever, bm25_retriever],
    weights=[0.6, 0.4]
)
# Add reranking on top
reranker = CrossEncoderReranker(
    model=HuggingFaceCrossEncoder(model_name="BAAI/bge-reranker-base"),
    top_n=3
)
final_retriever = ContextualCompressionRetriever(
    base_compressor=reranker,
    base_retriever=hybrid_retriever
)
# RAG prompt
prompt = ChatPromptTemplate.from_messages([
    ("system", """Answer the question using only the context below.
    If the answer is not in the context, say "I don't have that information."

    Context: {context}"""),
    ("human", "{question}")
])
def format_docs(docs):
    return "\n\n".join(doc.page_content for doc in docs)
# Complete chain
rag_chain = (
    {"context": final_retriever | format_docs, "question": RunnablePassthrough()}
    | prompt
    | llm
    | StrOutputParser()
)
# Use it
answer = rag_chain.invoke("Your question here")
print(answer)

其检索质量明显优于单纯的相似度搜索。

选择策略

一份简短的决策指南可将不同情况与相应策略对应起来:

Just starting out / simple use case?
    → Basic similarity search is fine
Documents have specific technical terms or product names?
    → Add BM25 → use Hybrid Search
Answer quality is critical, wrong answers are costly?
    → Add Reranking on top of any retriever
Documents have lots of repeated content?
    → Use MMR instead of standard similarity search
Production system with high quality requirements?
    → Hybrid Search + Reranking together

从一开始就不要过度设计。先从基本的相似度搜索开始,一旦发现关键词匹配失败再引入混合搜索,而当需要提升答案质量时再添加重排功能。每增加一种功能都会带来复杂性、延迟和计算成本。对于生产环境中的 RAG 系统,混合搜索加重排是标准配置,只有在遇到特定问题时才需要增加复杂性。

关于重排功能的一个实用提示:需要执行 pip install sentence-transformers,首次使用时会自动下载约 1GB 大小的 BAAI/bge-reranker-base 模型。在生产环境中,使用 Cohere 或 Jina AI 提供的托管式重排 API 可以避免本地运行模型的繁琐流程。

相关阅读

  • 超越相似度搜索:基于 S3 Vectors 的 RAG 元数据过滤 — 过滤值的来源、何时使用分类器,以及如何在 Amazon S3 Vectors 中设置可过滤与不可过滤的元数据。