大模型:Retriever,BM25

下面给你一个完整的、可直接运行的 Retriever Demo,涵盖了 4 种最常用的 Retriever 类型,并配有详细的注释和输出示例。你可以把它当作面试时的"武器库"来使用。


📦 环境准备

bash 复制代码
pip install langchain langchain-community chromadb rank_bm25 sentence-transformers openai

🚀 完整 Demo 代码

python 复制代码
import os
from langchain_core.documents import Document
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain.retrievers import BM25Retriever, MultiQueryRetriever, EnsembleRetriever
from langchain.retrievers.multi_query import MultiQueryRetriever
from langchain.retrievers.ensemble import EnsembleRetriever
from langchain_core.retrievers import BaseRetriever
from langchain_openai import ChatOpenAI

# ============================================================
# 1. 准备测试数据(10 篇关于宠物的文档)
# ============================================================
docs = [
    Document(page_content="猫是独立而优雅的宠物,喜欢清洁和安静的环境。", metadata={"source": "cat_article"}),
    Document(page_content="狗是人类最忠实的朋友,需要大量的运动和社交。", metadata={"source": "dog_article"}),
    Document(page_content="金鱼是常见的观赏鱼,适合在室内鱼缸中饲养。", metadata={"source": "fish_article"}),
    Document(page_content="仓鼠是小型啮齿动物,夜间活跃,适合城市家庭。", metadata={"source": "hamster_article"}),
    Document(page_content="鹦鹉是非常聪明的鸟类,能模仿人类语言。", metadata={"source": "parrot_article"}),
    Document(page_content="兔子是温顺的食草动物,需要宽敞的笼子和新鲜蔬菜。", metadata={"source": "rabbit_article"}),
    Document(page_content="乌龟是长寿的爬行动物,适应力强,但需要特定的温度和光照。", metadata={"source": "turtle_article"}),
    Document(page_content="猫和狗可以和平共处,但需要适当的引导和训练。", metadata={"source": "pet_guide"}),
    Document(page_content="饲养宠物需要考虑空间、时间和经济成本。", metadata={"source": "pet_care"}),
    Document(page_content="宠物可以缓解压力,提高生活质量。", metadata={"source": "pet_benefits"}),
]

# ============================================================
# 2. Retriever 类型 1:基于向量库的检索(最常用)
# ============================================================
print("=" * 50)
print("1. VectorStoreRetriever(向量检索)")
print("=" * 50)

# 使用轻量级中文嵌入模型
embeddings = HuggingFaceEmbeddings(
    model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
)

# 创建向量库
vectorstore = Chroma.from_documents(
    documents=docs,
    embedding=embeddings,
    collection_name="pet_demo"
)

# 转换成 Retriever
vector_retriever = vectorstore.as_retriever(
    search_kwargs={"k": 3}  # 返回 Top-3
)

query = "适合家庭养的宠物有哪些?"
result = vector_retriever.invoke(query)
print(f"查询:{query}")
for i, doc in enumerate(result):
    print(f"  {i+1}. {doc.page_content} (来源: {doc.metadata['source']})")

# ============================================================
# 3. Retriever 类型 2:BM25 关键词检索
# ============================================================
print("\n" + "=" * 50)
print("2. BM25Retriever(关键词检索)")
print("=" * 50)

bm25_retriever = BM25Retriever.from_documents(docs, k=3)
result = bm25_retriever.invoke("猫 狗")
print(f"查询:{'猫 狗'}")
for i, doc in enumerate(result):
    print(f"  {i+1}. {doc.page_content}")

# ============================================================
# 4. Retriever 类型 3:MultiQueryRetriever(多查询增强)
# ============================================================
print("\n" + "=" * 50)
print("3. MultiQueryRetriever(多角度生成查询)")
print("=" * 50)

# 需要 LLM 来生成多个不同表述的查询
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)  # 可换成通义千问等
multi_retriever = MultiQueryRetriever.from_llm(
    retriever=vectorstore.as_retriever(k=3),
    llm=llm,
    include_original=True  # 包含原始查询
)

result = multi_retriever.invoke("哪种宠物好养又可爱?")
print(f"查询:哪种宠物好养又可爱?")
# MultiQueryRetriever 不保证结果数量固定,这里打印所有结果
for i, doc in enumerate(result[:5]):
    print(f"  {i+1}. {doc.page_content}")

# ============================================================
# 5. Retriever 类型 4:EnsembleRetriever(混合检索)
# ============================================================
print("\n" + "=" * 50)
print("4. EnsembleRetriever(混合检索:BM25 + 向量)")
print("=" * 50)

ensemble_retriever = EnsembleRetriever(
    retrievers=[bm25_retriever, vector_retriever],
    weights=[0.5, 0.5]  # 两者权重相等
)

result = ensemble_retriever.invoke("宠物 陪伴 减压")
print(f"查询:宠物 陪伴 减压")
for i, doc in enumerate(result):
    print(f"  {i+1}. {doc.page_content}")

# ============================================================
# 6. 自定义 Retriever(演示扩展能力)
# ============================================================
print("\n" + "=" * 50)
print("5. 自定义 Retriever(按文档长度过滤)")
print("=" * 50)

class LengthFilterRetriever(BaseRetriever):
    """只返回长度小于 30 个字符的文档(演示自定义逻辑)"""
    retriever: BaseRetriever
    max_len: int = 30

    def _get_relevant_documents(self, query: str) -> list[Document]:
        docs = self.retriever.invoke(query)
        return [d for d in docs if len(d.page_content) <= self.max_len]

    async def _aget_relevant_documents(self, query: str) -> list[Document]:
        docs = await self.retriever.ainvoke(query)
        return [d for d in docs if len(d.page_content) <= self.max_len]

custom_retriever = LengthFilterRetriever(
    retriever=vector_retriever,
    max_len=30
)

result = custom_retriever.invoke("宠物")
print(f"查询:宠物(仅返回长度 <= 30 个字符的文档)")
for i, doc in enumerate(result):
    print(f"  {i+1}. {doc.page_content} (长度: {len(doc.page_content)})")

print("\n✅ Demo 执行完毕!")

📤 预期输出(部分节选)

复制代码
==================================================
1. VectorStoreRetriever(向量检索)
==================================================
查询:适合家庭养的宠物有哪些?
  1. 仓鼠是小型啮齿动物,夜间活跃,适合城市家庭。 (来源: hamster_article)
  2. 兔子是温顺的食草动物,需要宽敞的笼子和新鲜蔬菜。 (来源: rabbit_article)
  3. 宠物可以缓解压力,提高生活质量。 (来源: pet_benefits)

==================================================
2. BM25Retriever(关键词检索)
==================================================
查询:猫 狗
  1. 猫是独立而优雅的宠物,喜欢清洁和安静的环境。
  2. 狗是人类最忠实的朋友,需要大量的运动和社交。
  3. 猫和狗可以和平共处,但需要适当的引导和训练。

...

🎯 面试时如何讲解这个 Demo

Retriever 类型 一句话讲清楚 适用场景
VectorStoreRetriever 用向量相似度找语义相关的文档 通用 RAG 标配
BM25Retriever 用关键词精确匹配 产品名、代码等精确检索
MultiQueryRetriever 让 LLM 从多角度生成查询,提高召回率 用户表述模糊时
EnsembleRetriever 混合关键词和向量检索,取长补短 生产环境推荐
自定义 Retriever 继承 BaseRetriever 实现业务逻辑 有特殊过滤需求的场景

面试加分句 :"在生产环境中,我通常采用 EnsembleRetriever 组合 BM25 和向量检索,因为用户既可能输入口语化的描述,也可能输入精确的专有名词,混合检索能保证两种情况的召回率。同时我会配置一个 Reranker 对结果重排序,进一步优化相关性。"

你可以直接复制上述代码到本地运行(需要配置 OPENAI_API_KEY 或替换成通义千问),亲身体验每种 Retriever 的效果差异。如果遇到任何问题,欢迎继续提问!

📖 什么是 BM25?

在深入面试题之前,我们首先需要理解 BM25 是什么。BM25(Best Matching 25)是一种词袋检索模型,用于信息检索系统,评估文档与给定搜索查询的相关性。它基于词频(TF)和逆文档频率(IDF)等统计信息进行打分,是 TF-IDF 的进化版:它根据查询词项在文档中出现的频率进行打分,同时对那些在整个语料库中过于常见的词进行惩罚。

其核心思想是:如果一篇文档包含了用户查询中的关键词,且这些词在其他文档中不常见,那么这篇文档的相关性就越高


⚔️ BM25 与向量检索:差异与互补

这是面试中的核心问题,理解两者的差异是回答所有后续问题的基础。

特性 BM25 (稀疏检索) 向量检索 (密集检索)
检索方式 基于关键词匹配和词频统计。 基于语义相似度,将文本映射为向量。
擅长处理 专有名词、ID、代码函数、产品型号等精确词汇。 同义词、语义相近但词汇不同的查询。
优点 精确匹配能力强,对稀有词汇敏感,可解释性强。 泛化能力强,能理解用户意图,处理自然语言。
缺点 无法理解同义词,对"一词多义"场景无能为力。 对精确的专有名词不敏感,可能召回语义相关但非精确的内容。
典型场景 用户搜索"ERR_CONN_RESET_4XX"等具体错误码。 用户搜索"如何解决网络连接问题"等意图类问题。

面试金句:向量检索懂"语义",BM25 懂"字词"。两者是互补关系,而非替代关系。


🤔 为什么真实 RAG 系统通常需要两者?

面试官常会问:"为什么有的项目用 BM25,有的用 Embedding,还有的非要上混合检索?"。

答案是:没有一个检索器是万能的

  • 只用向量检索:当用户查询一个只在文档中出现过一次的专有名词(如"重排序")时,仅靠语义相似度可能无法召回该文档。
  • 只用 BM25:当用户查询"如何让应用响应更快"时,它无法匹配到内容为"性能优化"的文档,因为缺少共同的关键词。

混合检索(Hybrid Search)正是为了解决这个矛盾。它在生产环境中的效果往往优于仅使用密集检索的系统,通过结合 BM25 的关键词匹配能力和向量检索的语义理解能力,实现 1+1 > 2 的效果。


💻 BM25Retriever 实战:LangChain 中的用法

在 LangChain 中,BM25Retriever 是一个开箱即用的组件。

1. 安装

bash 复制代码
pip install rank_bm25

2. 基本使用

python 复制代码
from langchain_community.retrievers import BM25Retriever
from langchain_core.documents import Document

# 从文档列表创建检索器
docs = [
    Document(page_content="猫是独立而优雅的宠物。"),
    Document(page_content="狗是人类最忠实的朋友。"),
]
retriever = BM25Retriever.from_documents(docs, k=2)

3. 检索

python 复制代码
results = retriever.invoke("狗")
# 输出: [Document(page_content='狗是人类最忠实的朋友。'), ...]

4. 自定义分词

为了更好的效果,可以传入自定义的分词函数。

python 复制代码
import nltk
from nltk.tokenize import word_tokenize

retriever = BM25Retriever.from_documents(
    docs,
    preprocess_func=word_tokenize  # 使用 NLTK 分词
)

5. 使用 BM25Plus 变体

对于短文本或分块文档,BM25Plus 变体能有效提升召回率。

python 复制代码
retriever = BM25Retriever.from_documents(
    docs,
    bm25_variant="plus",  # 启用 BM25Plus
    bm25_params={"delta": 0.5}  # 可调参数
)

🎯 高频面试题与解答

1. 为什么在 RAG 系统中要引入 BM25?

参考答案

引入 BM25 是为了弥补向量检索在精确关键词匹配上的不足。向量检索擅长理解语义,但当用户查询的是精确的专有名词、产品型号或代码函数时,它可能会召回语义相关但不包含精确关键词的内容。BM25 通过词频统计,能精准地捕获这些关键词,两者结合能显著提升系统的召回率。

2. 混合检索的流程是什么?BM25 和向量检索的结果如何融合?

参考答案

典型的混合检索流程是:

  1. 多路召回:分别使用 BM25 检索器和向量检索器对同一查询进行检索,各自得到一组候选文档。
  2. 结果融合 :使用 RRF(倒数排名融合) 算法将两路结果合并。RRF 通过公式 RRF(d) = Σ 1/(k + rank_i(d)) 计算文档的最终得分,能有效平衡不同检索器的排名。
  3. 重排序(可选) :对融合后的 Top-N 结果,使用一个更强大的模型(如 Cohere Rerank)进行重排序,进一步提升顶部结果的相关性。
3. 如何确定混合检索中 BM25 和向量检索的权重?

参考答案

权重的设置没有固定公式,通常需要根据具体业务场景和数据特性进行实验调优。可以从 1:1 的比例(或使用 RRF) 开始作为基线。然后,通过评估指标(如 Recall@k)在验证集上进行迭代优化。此外,也可以设计一套动态权重策略,例如,当检测到查询中包含明显的专有名词时,自动提升 BM25 的权重。

4. BM25 有什么局限性?

参考答案

BM25 的主要局限在于它是词袋模型,完全基于词汇匹配,无法理解同义词或语义。当查询和文档使用不同词汇表达相同含义时,它会失效。此外,当知识库规模增大(例如超过 1000 个条目)时,其效率会显著下降。在超大规模检索场景下,近似最近邻(ANN)的密集检索在效率上更具优势。


💎 总结

BM25Retriever 是 LangChain 生态中实现关键词检索的核心组件,是构建高性能 RAG 系统不可或缺的基石。

面试时,你需要清晰地阐述其基于词频统计的原理,准确说明它与向量检索的优势互补关系,并能够结合 RRF 等算法说明混合检索的具体实现流程。展示你对检索质量如何影响生成质量的深刻理解,将是你从众多候选人中脱颖而出的关键。