【LangChain】检索器完全指南:从向量检索到生产级 RAG 架构

LangChain 检索器完全指南:从向量检索到生产级 RAG 架构

系列前言:本文是 LangChain Expression Language (LCEL) 系列文章的开篇,后续将深入探讨 LCEL 的链式组合与高级用法。系列文章将遵循统一的 Markdown 格式规范,标题层级清晰,代码示例可复现。


目录


一、检索器是什么?

在 RAG(Retrieval-Augmented Generation)架构中,检索器(Retriever)是连接外部知识库与大语言模型的桥梁。它接受一个字符串查询,返回一组相关的 Document 对象。

LangChain 中检索器的核心接口极其简洁:

python 复制代码
class BaseRetriever(ABC):
    @abstractmethod
    def _get_relevant_documents(self, query: str) -> List[Document]:
        """根据查询返回相关文档"""

所有检索器都遵循这一契约,这意味着它们可以无缝替换和组合


二、为什么需要检索器抽象?

2.1 解耦存储与检索

向量存储(VectorStore)负责存储和相似度计算 ,检索器负责检索策略 。通过 as_retriever() 方法,LangChain 将向量库"包装"成标准检索器:

python 复制代码
from langchain_community.vectorstores import Chroma, FAISS, Qdrant

# 三种不同的向量库,接口完全一致
chroma = Chroma.from_documents(docs, embeddings)
faiss = FAISS.from_documents(docs, embeddings)

# 都通过 as_retriever() 得到统一接口
retriever_chroma = chroma.as_retriever()
retriever_faiss = faiss.as_retriever()

# 上层代码完全不关心底层实现
def build_rag_chain(retriever):
    return {"context": retriever, "question": RunnablePassthrough()} | prompt | llm

2.2 装饰器模式的威力

检索器的设计遵循装饰器模式 (Decorator Pattern)------每一层都在底层检索器的基础上增强功能,但对外保持统一的 invoke(query) -> List[Document] 接口:

复制代码
基础能力:invoke(query) -> docs

+ EnsembleRetriever        -> 多路召回
+ MultiQueryRetriever      -> 查询扩展  
+ ContextualCompression    -> 结果压缩

每一层增强功能,接口不变

三、核心检索器类型详解

3.1 向量存储检索器(VectorStoreRetriever)

最基础的语义检索,基于向量相似度:

python 复制代码
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings

vectorstore = Chroma.from_documents(docs, OpenAIEmbeddings())

# 三种检索策略
retriever = vectorstore.as_retriever(
    search_type="similarity",           # 标准相似度检索
    search_kwargs={"k": 4}
)

retriever_mmr = vectorstore.as_retriever(
    search_type="mmr",                  # 最大边际相关性,兼顾多样性
    search_kwargs={"k": 4, "lambda_mult": 0.5}
)

retriever_threshold = vectorstore.as_retriever(
    search_type="similarity_score_threshold",  # 阈值过滤
    search_kwargs={"k": 4, "score_threshold": 0.8}
)

3.2 多查询检索器(MultiQueryRetriever)

解决用户查询表述不准确的问题。LLM 生成多个查询变体,分别检索后合并去重:

python 复制代码
from langchain.retrievers.multi_query import MultiQueryRetriever
from langchain_openai import ChatOpenAI

retriever = MultiQueryRetriever.from_llm(
    retriever=base_retriever,
    llm=ChatOpenAI(temperature=0)
)

# 用户输入:"LangChain 咋用"
# 内部自动扩展为:
#   "LangChain 入门教程"
#   "LangChain 快速开始指南"
#   "LangChain 使用方法"

3.3 上下文压缩检索器(ContextualCompressionRetriever)

解决检索结果冗余问题。先检索,再用 LLM 压缩每个文档,只保留与查询相关的片段:

python 复制代码
from langchain.retrievers.contextual_compression import ContextualCompressionRetriever
from langchain_community.document_transformers import LLMChainExtractor

compressor = LLMChainExtractor.from_llm(llm)
compression_retriever = ContextualCompressionRetriever(
    base_compressor=compressor,
    base_retriever=base_retriever
)

# 原始文档 1000 字 -> 压缩为 50 字精华
# 只保留与查询相关的片段

3.4 父文档检索器(ParentDocumentRetriever)

解决分块导致上下文丢失的问题。检索子块(用于精准匹配),但返回父文档(保持完整上下文):

python 复制代码
from langchain.retrievers import ParentDocumentRetriever
from langchain.storage import InMemoryStore

retriever = ParentDocumentRetriever(
    vectorstore=child_vectorstore,      # 存储小片段向量
    docstore=InMemoryStore(),           # 存储完整父文档
    child_splitter=RecursiveCharacterTextSplitter(chunk_size=200),
    parent_splitter=RecursiveCharacterTextSplitter(chunk_size=2000)
)

3.5 集成检索器(EnsembleRetriever)

多路召回 + RRF 重排序。结合多个检索器的结果(如 BM25 + 向量检索),使用 Reciprocal Rank Fusion 算法排序:

python 复制代码
from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever

bm25_retriever = BM25Retriever.from_documents(docs)
bm25_retriever.k = 2

ensemble_retriever = EnsembleRetriever(
    retrievers=[bm25_retriever, vector_retriever],
    weights=[0.5, 0.5]
)

3.6 自查询检索器(SelfQueryRetriever)

结构化过滤 + 语义检索。让 LLM 从自然语言查询中提取结构化过滤条件:

python 复制代码
from langchain.retrievers.self_query.base import SelfQueryRetriever

retriever = SelfQueryRetriever.from_llm(
    llm=llm,
    vectorstore=vectorstore,
    document_content_description="电影简介",
    metadata_field_info=[
        AttributeInfo(name="genre", description="电影类型", type="string"),
        AttributeInfo(name="year", description="上映年份", type="integer"),
        AttributeInfo(name="rating", description="评分1-10", type="float"),
    ]
)

# 查询:"2020年后评分高于8分的科幻电影"
# 自动提取:{"year": {"$gt": 2020}, "rating": {"$gt": 8}, "genre": "科幻"}

四、检索器组合:从单层到多层架构

4.1 单层架构(原型阶段)

python 复制代码
# 最简单,直接向量检索
retriever = vectorstore.as_retriever(search_kwargs={"k": 4})

4.2 双层架构(通用场景)

python 复制代码
# 多查询扩展 + 向量检索
from langchain.retrievers.multi_query import MultiQueryRetriever

retriever = MultiQueryRetriever.from_llm(
    retriever=vectorstore.as_retriever(search_kwargs={"k": 6}),
    llm=ChatOpenAI(temperature=0)
)

4.3 三层架构(生产推荐)

python 复制代码
# 多路召回 + 查询扩展
from langchain.retrievers import EnsembleRetriever, MultiQueryRetriever
from langchain_community.retrievers import BM25Retriever

# 语义通道
semantic = vectorstore.as_retriever(search_kwargs={"k": 8})

# 关键词通道
keyword = BM25Retriever.from_documents(documents)
keyword.k = 4

# 融合 + 查询扩展
ensemble = EnsembleRetriever(retrievers=[semantic, keyword], weights=[0.7, 0.3])
retriever = MultiQueryRetriever.from_llm(retriever=ensemble, llm=llm)

4.4 四层架构(极致效果)

python 复制代码
# 多路召回 + 查询扩展 + 结果压缩
from langchain.retrievers.contextual_compression import ContextualCompressionRetriever
from langchain_community.document_transformers import LLMChainExtractor

# 先构建三层
semantic = vectorstore.as_retriever(search_kwargs={"k": 8})
keyword = BM25Retriever.from_documents(documents)
keyword.k = 4
ensemble = EnsembleRetriever(retrievers=[semantic, keyword], weights=[0.7, 0.3])
multi_query = MultiQueryRetriever.from_llm(retriever=ensemble, llm=llm)

# 再加压缩层
compressor = LLMChainExtractor.from_llm(llm)
final_retriever = ContextualCompressionRetriever(
    base_compressor=compressor,
    base_retriever=multi_query
)

五、生产级检索器构建实践

5.1 完整可复现代码

python 复制代码
from langchain.retrievers import (
    EnsembleRetriever,
    MultiQueryRetriever,
    ContextualCompressionRetriever,
    ParentDocumentRetriever
)
from langchain_community.retrievers import BM25Retriever
from langchain_community.document_transformers import LLMChainExtractor
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain.storage import InMemoryStore
from langchain.text_splitter import RecursiveCharacterTextSplitter

def build_production_retriever(documents, llm=None):
    """
    构建生产级检索器: 四层架构
    - 语义检索 + 关键词检索(多路召回)
    - 查询扩展(MultiQuery)
    - 结果压缩(Compression)
    """
    if llm is None:
        llm = ChatOpenAI(temperature=0)

    # ========== 第1层: 语义通道 ==========
    # 向量相似度, 找"意思相近"的
    embeddings = OpenAIEmbeddings()
    vectorstore = Chroma.from_documents(documents, embeddings)
    semantic = vectorstore.as_retriever(search_kwargs={"k": 8})

    # ========== 第2层: 关键词通道 ==========
    # BM25 字面匹配, 找"词对上了"的
    keyword = BM25Retriever.from_documents(documents)
    keyword.k = 4

    # ========== 第3层: 多路融合 ==========
    # 两个通道结果合并, RRF 算法排序
    ensemble = EnsembleRetriever(
        retrievers=[semantic, keyword],
        weights=[0.7, 0.3]
    )

    # ========== 第4层: 查询扩展 ==========
    # 用户口语化 -> LLM 改写为多个专业查询
    multi_query = MultiQueryRetriever.from_llm(
        retriever=ensemble,
        llm=llm
    )

    # ========== 第5层: 结果压缩 ==========
    # 每篇文档让 LLM 压缩, 只保留相关片段
    compressor = LLMChainExtractor.from_llm(llm)
    final = ContextualCompressionRetriever(
        base_compressor=compressor,
        base_retriever=multi_query
    )

    return final


# ========== 使用示例 ==========
from langchain_core.documents import Document

# 准备文档
documents = [
    Document(page_content="LangChain Expression Language (LCEL) 允许用 | 符号连接组件...", metadata={"source": "docs1"}),
    Document(page_content="LCEL 是 LangChain 的声明式语法, 用于组合链...", metadata={"source": "docs2"}),
    # ... 更多文档
]

# 构建检索器
llm = ChatOpenAI(temperature=0)
retriever = build_production_retriever(documents, llm)

# 调用(自动走完所有层)
docs = retriever.invoke("LangChain 怎么链式调用?")

5.2 调用链内部流程

复制代码
用户输入: "LangChain 怎么链式调用?"
    |
    v
[ContextualCompressionRetriever]
    |-- 调用 base_retriever.invoke()
    |       |
    |       v
    |   [MultiQueryRetriever]
    |       |-- LLM 改写为 ["LCEL 用法", "链式调用教程", "pipe 操作符"]
    |       |
    |       |-- 调用 retriever.invoke("LCEL 用法")
    |       |       |
    |       |       v
    |       |   [EnsembleRetriever]
    |       |       |-- semantic.invoke("LCEL 用法") -> 向量库查
    |       |       |-- keyword.invoke("LCEL 用法") -> BM25查
    |       |       |-- RRF 融合返回
    |       |
    |       |-- 调用 retriever.invoke("链式调用教程")
    |       |       |-- [EnsembleRetriever] 同样流程...
    |       |
    |       |-- 调用 retriever.invoke("pipe 操作符")
    |       |       |-- [EnsembleRetriever] 同样流程...
    |       |
    |       |-- 合并 3 次结果, 去重
    |
    |-- 对每篇文档调用 LLM 压缩
    |   "这篇跟'链式调用'有关吗? 提取相关片段"
    |
    |-- 返回压缩后的精华文档

六、性能与成本的权衡

组合层级 召回率 精准度 LLM 调用次数 适用场景
裸 VectorStore 0 原型/测试
+ MultiQuery 1次/查询 通用首选
+ Compression 1次/文档 长文档问答
+ ParentDocument 0 结构化长文档
Ensemble + 多路 0 术语密集型
全套组合 2-3次/查询 高价值场景

6.1 成本优化策略

python 复制代码
# 策略1: 去掉 Compression(最省成本)
retriever = MultiQueryRetriever.from_llm(
    retriever=EnsembleRetriever(
        retrievers=[semantic, keyword],
        weights=[0.7, 0.3]
    ),
    llm=llm
)

# 策略2: 减少 MultiQuery 生成数量
retriever = MultiQueryRetriever.from_llm(
    retriever=base_retriever,
    llm=llm,
    parser_key="lines",  # 只生成3个查询
)

# 策略3: 用 ParentDocument 替代 Compression(零 LLM 成本)
retriever = ParentDocumentRetriever(
    vectorstore=child_vectorstore,
    docstore=parent_docstore,
    child_splitter=RecursiveCharacterTextSplitter(chunk_size=200),
    parent_splitter=RecursiveCharacterTextSplitter(chunk_size=2000)
)

七、总结与选型指南

7.1 决策树

复制代码
你的检索失败属于哪种情况?
|
|-- 查询表述太口语化/模糊?
|   |-- 加 MultiQueryRetriever
|
|-- 检索结果太长、夹杂无关内容?
|   |-- 加 ContextualCompressionRetriever
|
|-- 分块后语义断裂?(代码、表格跨块)
|   |-- 用 ParentDocumentRetriever
|
|-- 需要精确匹配(ID、术语、人名)?
|   |-- 加 BM25, 做 EnsembleRetriever
|
|-- 有结构化元数据(时间、类别、价格)?
|   |-- 用 SelfQueryRetriever
|
|-- 知识时效性强?(新闻、政策)
|   |-- 用 TimeWeightedVectorStoreRetriever
|
|-- 以上多种情况并存?
    |-- 叠加组合, 从简单开始逐步增强

7.2 核心原则

  1. 先上监控: 没有评估指标(如 Ragas)谈优化是盲目的
  2. 从简单开始: 裸 VectorStore -> 加 MultiQuery -> 看数据加其他
  3. 组合不是越多越好: 每加一层就多一次延迟和成本, 只加能解决实际问题的层
  4. ParentDocument 和 Ensemble 是零成本增强(不额外调 LLM), 优先尝试

7.3 在 LCEL 中使用

python 复制代码
from langchain_core.runnables import RunnablePassthrough
from langchain_core.prompts import ChatPromptTemplate

template = """基于以下上下文回答问题:
{context}

问题: {question}
"""
prompt = ChatPromptTemplate.from_template(template)

# 构建 RAG 链
rag_chain = (
    {"context": retriever, "question": RunnablePassthrough()}
    | prompt
    | ChatOpenAI()
)

# 直接调用, 自动完成检索+生成
response = rag_chain.invoke("什么是 LCEL?")

结语

LangChain 的检索器设计遵循单一职责原则可组合性。理解每一层的能力边界和组合方式, 是构建生产级 RAG 系统的关键。没有银弹, 但有套路------从简单开始, 用数据驱动优化。

相关推荐
大白菜和MySQL1 小时前
java应用排查高线程
java·python
KobeSacre2 小时前
ReentrantLock源码
java
LabVIEW开发2 小时前
LabVIEW + MATLAB 混合编程:爆炸场测试数据精准采集方案
开发语言·matlab·labview
嵌入式协会20240722 小时前
(已解决)MinIO python 获取预签名出现forbidden、errornetwork等错误
java·开发语言·python
宸丶一2 小时前
Day 14:任务追踪 - 让 Agent 拥有项目管理能力
开发语言·python
小短腿的代码世界2 小时前
Qt行情协议解析与二进制编解码优化:从FIX到自定义协议的全链路架构
开发语言·qt·架构
不才不才不不才2 小时前
Spring AI 实战:聊天、提示词、记忆三件套
java·人工智能·spring·ai
skylar02 小时前
小白1分钟安装flash-attn
开发语言·python
默子昂2 小时前
ollama 自定义ui
开发语言·python·ui