LangChain-Retriever检索器深度解析
RAG(检索增强生成)应用的标准构建流程是:文档切块、embedding(嵌入:把文本转成一串数字向量,相似内容的向量在数学上距离相近)、存向量库、用户问题来了做向量检索、把检索结果塞进 Prompt、让模型回答。这个流程跑通之后,效果通常还可以,但往往有 30% 的问题回答不好。
LangChain Retriever:检索器深度解析
RAG(检索增强生成)应用的标准构建流程是:文档切块、embedding(嵌入:把文本转成一串数字向量,相似内容的向量在数学上距离相近)、存向量库、用户问题来了做向量检索、把检索结果塞进 Prompt、让模型回答。这个流程跑通之后,效果通常还可以,但往往有 30% 的问题回答不好。
向量检索是 RAG 的起点,不是终点。检索器的选择比 embedding 模型的选择影响更大——换一个更好的 embedding 模型,召回率可能提升 5%;换一个合适的检索策略,提升 20% 是完全可能的。
本章把 LangChain 里主要的 Retriever 捋一遍,并说明如何选择。
1. Retriever 接口
LangChain Retriever 架构 — 统一接口下的多种检索策略
LangChain 的 Retriever 是一个统一接口,核心方法就一个:
retriever.invoke(query) # 新版 LangChain
# 或
retriever.get_relevant_documents(query) # 旧版,同样可用
传入查询字符串,返回 List[Document]。每个 Document 包含 page_content(文本内容)和 metadata(来源、页码等元信息)。
统一接口的好处是:可以随时换掉检索器,上层的 RAG 链不用改。
2. VectorStoreRetriever:基础,但不够用
最常见的检索器,直接包装向量数据库:
import os
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_community.vectorstores import Chroma
# 创建向量库(示例用内存模式)
embeddings = OpenAIEmbeddings(
model="text-embedding-3-small",
api_key=os.getenv("OPENAI_API_KEY"),
)
vectorstore = Chroma(embedding_function=embeddings)
# 默认:向量相似度检索,返回 top-4
retriever = vectorstore.as_retriever()
# 指定返回数量
retriever = vectorstore.as_retriever(search_kwargs={"k": 6})
# MMR 检索(后面重点讲)
retriever = vectorstore.as_retriever(
search_type="mmr",
search_kwargs={"k": 6, "fetch_k": 20, "lambda_mult": 0.5},
)
# 带分数阈值的相似度检索
retriever = vectorstore.as_retriever(
search_type="similarity_score_threshold",
search_kwargs={"score_threshold": 0.7},
)
search_type 有三个选项:
similarity:纯向量相似度,默认,最快mmr:在相关性和多样性之间取平衡(后面详细说)similarity_score_threshold:设置相似度阈值,低于阈值的文档不返回
阈值检索适合"这个问题在文档里没有答案"的场景——相似度太低,说明文档库里可能根本没有相关内容,与其返回不相关内容让模型瞎编,不如返回空结果。
3. MMR:相关性 vs 多样性
向量数据库里同一个主题的内容往往有多个段落,用纯向量相似度检索,返回的 top-6 可能有 5 个是几乎一样的内容——相关性高,但完全重复,浪费 context window,回答质量也不会好。
MMR(Maximal Marginal Relevance,最大边际相关性)做的事情是:在保证相关性的同时,让返回的文档尽量多样。
具体机制是贪心算法:每次从候选集里选一个文档,要求它和查询相关、同时和已选文档不太相似。lambda_mult 参数控制平衡——接近 1 偏向相关性,接近 0 偏向多样性,0.5 是折中。
# fetch_k=20:先召回 20 个候选,再从中 MMR 选 6 个
retriever = vectorstore.as_retriever(
search_type="mmr",
search_kwargs={"k": 6, "fetch_k": 20, "lambda_mult": 0.5},
)
有重复内容的文档库,用 MMR 几乎总比纯相似度效果好。
4. MultiQueryRetriever:用 LLM 扩展查询
用户的提问往往很简短,甚至表述不准确。"怎么做缓存"和"Redis 最佳实践"在语义上有交集,但向量不一定接近。用一个查询只能命中一部分相关文档。
MultiQueryRetriever 的思路是:让 LLM 把原始查询改写成多个不同角度的查询,然后并行检索,合并去重结果:
from langchain.retrievers.multi_query import MultiQueryRetriever
from langchain_openai import ChatOpenAI
import os
llm = ChatOpenAI(
model="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com",
temperature=0,
)
multi_query_retriever = MultiQueryRetriever.from_llm(
retriever=vectorstore.as_retriever(search_kwargs={"k": 4}),
llm=llm,
)
# 开启日志,看生成了哪些查询
import logging
logging.basicConfig()
logging.getLogger("langchain.retrievers.multi_query").setLevel(logging.INFO)
docs = multi_query_retriever.invoke("Java 项目怎么接入 AI 能力?")
# 模型会生成类似这样的多个查询:
# - "Java 应用集成大语言模型的方法"
# - "Spring Boot 项目调用 OpenAI API"
# - "Java 开发者如何使用 LangChain4j"
# 三个查询分别检索,结果合并去重
print(f"检索到 {len(docs)} 个文档")
代价是多了几次 LLM 调用,延迟会增加。查询生成用便宜快速的模型(如 DeepSeek 或 GPT-3.5),成本不高。
适合查询复杂、用户表述模糊的场景,不适合对延迟要求极高的场景。
5. ContextualCompressionRetriever:检索后过滤
向量检索返回的是整个文档块,一块通常有几百 token。但用户的问题可能只和这块里的两三句话有关,其他内容是噪音,占用 context window,还可能干扰模型推理。
ContextualCompressionRetriever 在检索之后加一道过滤:用 LLM 或者提取器,从每个检索到的文档里只保留和问题真正相关的部分:
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor
# LLMChainExtractor:用 LLM 从文档里提取相关内容
compressor = LLMChainExtractor.from_llm(llm)
compression_retriever = ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=vectorstore.as_retriever(search_kwargs={"k": 6}),
)
docs = compression_retriever.invoke("Redis 分布式锁的实现原理?")
# 返回的文档内容是被 LLM 提炼过的,只包含和问题相关的部分
for doc in docs:
print(f"来源:{doc.metadata.get('source', '未知')}")
print(f"内容:{doc.page_content[:200]}")
print("---")
LLMChainExtractor 会对每个文档做一次 LLM 调用,成本不低。如果检索到 6 个文档,就是 6 次额外调用。
更轻量的选择是 LLMChainFilter,它不提取内容,只做一个二分类决策——这个文档相关还是不相关,过滤掉不相关的,相关的原样保留。比 Extractor 便宜很多:
from langchain.retrievers.document_compressors import LLMChainFilter
filter_compressor = LLMChainFilter.from_llm(llm)
compression_retriever = ContextualCompressionRetriever(
base_compressor=filter_compressor,
base_retriever=vectorstore.as_retriever(search_kwargs={"k": 8}),
)
6. EnsembleRetriever:混合检索
向量检索擅长语义相似度,但对关键词精确匹配不够好。"用户问题里有'UUID',但文档里写的是'唯一标识符'"——语义接近,但向量可能不够近。
BM25(一种经典的基于关键词频率的文本检索算法,对精确词匹配特别有效)是传统的基于关键词的检索算法,精确词匹配特别稳。两者混合,取长补短:
from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
from langchain_core.documents import Document
# 准备文档(实际应用中从文档库加载)
documents = [
Document(page_content="Redis 实现分布式锁,使用 SET key value NX EX seconds 命令"),
Document(page_content="分布式系统中的唯一标识符(UUID)生成策略"),
Document(page_content="Spring Boot 整合 Redis 实现缓存方案"),
# ... 更多文档
]
# BM25 检索器
bm25_retriever = BM25Retriever.from_documents(documents)
bm25_retriever.k = 4
# 向量检索器
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 4})
# 混合,各占 50% 权重,用 RRF 算法融合排名
ensemble_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, vector_retriever],
weights=[0.5, 0.5],
)
docs = ensemble_retriever.invoke("分布式锁怎么用 Redis 实现?")
weights 控制两个检索器的权重。如果用户倾向于用技术术语精确提问,可以调高 BM25 的权重;如果更多是自然语言描述,调高向量权重。
RRF(Reciprocal Rank Fusion,倒数排名融合)是融合算法:对两个结果列表分别按排名打分,然后合并求和,排名高的文档得分高,最终按总分排序。这比简单拼接再去重要合理得多。
7. SelfQueryRetriever:结构化过滤
向量检索不擅长处理带条件的查询,比如"2024 年发布的关于 Java 的教程"——这里有时间条件、技术条件,纯语义检索很难处理准确。
SelfQueryRetriever 让 LLM 把自然语言查询拆解成两部分:语义查询 + 结构化过滤条件,然后同时用两个维度过滤:
from langchain.retrievers.self_query.base import SelfQueryRetriever
from langchain.chains.query_constructor.base import AttributeInfo
# 告诉 LLM 文档有哪些元数据字段可以过滤
metadata_field_info = [
AttributeInfo(name="year", description="文档发布年份", type="integer"),
AttributeInfo(name="category", description="技术分类,如 Java、Python、DevOps", type="string"),
AttributeInfo(name="difficulty", description="难度级别:beginner/intermediate/advanced", type="string"),
]
self_query_retriever = SelfQueryRetriever.from_llm(
llm=llm,
vectorstore=vectorstore,
document_contents="技术教程和文档",
metadata_field_info=metadata_field_info,
verbose=True,
)
# LLM 会自动解析出:语义="Spring Boot 教程" + 过滤条件 year>=2023 AND category="Java"
docs = self_query_retriever.invoke("2023 年以后的 Java Spring Boot 中级教程")
前提是向量库支持元数据过滤(Chroma、Pinecone、Weaviate 都支持),而且文档存入时要把元数据带上。
8. 多检索器组合:完整示例
实际项目里,最强的方案往往是多个检索器的组合。下面是一个 MultiQueryRetriever + ContextualCompressionRetriever 的完整可运行示例:
import os
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
from langchain.retrievers.multi_query import MultiQueryRetriever
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainFilter
from langchain_core.documents import Document
# 初始化
llm = ChatOpenAI(
model="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com",
temperature=0,
)
embeddings = OpenAIEmbeddings(
model="text-embedding-3-small",
api_key=os.getenv("OPENAI_API_KEY"),
)
# 准备示例文档
docs = [
Document(page_content="Java 中使用 ThreadLocal 可以实现线程级别的变量隔离,每个线程有独立的副本。"),
Document(page_content="Spring Boot 的 @Transactional 注解底层通过 AOP 代理实现事务管理。"),
Document(page_content="Redis 的 SETNX 命令可以实现简单的分布式锁,但需要设置过期时间防止死锁。"),
Document(page_content="Kafka 的消费者组机制保证了同一个消息只被组内一个消费者处理。"),
Document(page_content="MySQL 的 MVCC 机制通过 undo log 实现了读已提交和可重复读隔离级别。"),
Document(page_content="线程安全的 HashMap 可以用 ConcurrentHashMap 替代,分段锁减少竞争。"),
]
# 构建向量库
vectorstore = Chroma.from_documents(docs, embeddings)
# 第一层:MultiQueryRetriever,扩大召回
base_retriever = vectorstore.as_retriever(search_kwargs={"k": 4})
multi_query_retriever = MultiQueryRetriever.from_llm(
retriever=base_retriever,
llm=llm,
)
# 第二层:ContextualCompressionRetriever,过滤不相关文档
filter_compressor = LLMChainFilter.from_llm(llm)
final_retriever = ContextualCompressionRetriever(
base_compressor=filter_compressor,
base_retriever=multi_query_retriever,
)
# 测试
query = "Java 并发编程里怎么保证线程安全?"
results = final_retriever.invoke(query)
print(f"查询:{query}")
print(f"检索到 {len(results)} 个相关文档:")
for i, doc in enumerate(results, 1):
print(f"\n{i}. {doc.page_content}")
这个组合的效果是:MultiQueryRetriever 扩大召回(不容易漏相关内容),LLMChainFilter 过滤噪音(精确率更高)。代价是延迟更高、成本更贵,适合对准确率要求高、对延迟不敏感的场景。
9. 如何选择 Retriever
几个经验原则:
先从基础向量检索开始,建立一个评估基线。有了基线才知道改进了多少。
文档质量 > 检索策略 > Embedding 模型。 文档写得乱、切块不合理,换再好的检索器也救不了。先把文档处理好。
MultiQueryRetriever 是性价比最高的升级,实现简单,对召回率提升明显。如果 RAG 系统只能做一个升级,优先考虑这个。
EnsembleRetriever 适合技术文档。技术文档里有大量的专有名词、函数名、命令,BM25 的精确匹配对这类内容特别有效。
检索是 RAG 的命脉。检索做好了,模型只需要做"整理和表达";检索做不好,模型再强也是在瞎编。