LangChain设计与实现-第10章-向量存储与检索器

0 阅读16分钟

第10章 向量存储与检索器

本书章节导航


在 RAG 系统的架构中,向量存储(VectorStore)和检索器(Retriever)是连接知识库与语言模型的关键桥梁。前者负责将文档嵌入为向量并高效存储,后者负责根据查询从知识库中检索最相关的文档。LangChain 为这两个层次分别构建了精心设计的抽象,使得开发者可以在不改变应用代码的前提下,轻松切换底层的向量数据库实现。

本章将从 Embeddings 接口开始,逐步展开 VectorStore 的核心抽象和 InMemoryVectorStore 的参考实现,深入分析 BaseRetriever 的 Runnable 集成设计,以及 VectorStoreRetriever 的适配器模式。最后,我们将探讨多种高级检索策略的实现思路。

:::tip 本章要点

  • 理解 Embeddings 接口的 embed_documents / embed_query 双方法设计
  • 掌握 VectorStore 抽象的核心方法族:similarity_search / MMR / relevance_scores
  • 深入理解 InMemoryVectorStore 的完整实现,包括余弦相似度和 MMR 算法
  • 理解 BaseRetriever 的 Runnable 协议集成与回调机制
  • 掌握 VectorStoreRetriever 的适配器模式与搜索策略选择
  • 了解高级检索器(ParentDocumentRetriever / MultiVectorRetriever / ContextualCompressionRetriever / MergerRetriever)的设计思路 :::

10.1 Embeddings:向量化接口

Embeddings 是向量存储系统的前置依赖 -- 它负责将文本转换为高维向量表示。

# langchain_core/embeddings/embeddings.py
class Embeddings(ABC):
    @abstractmethod
    def embed_documents(self, texts: list[str]) -> list[list[float]]:
        """Embed search docs."""

    @abstractmethod
    def embed_query(self, text: str) -> list[float]:
        """Embed query text."""

    async def aembed_documents(self, texts: list[str]) -> list[list[float]]:
        return await run_in_executor(None, self.embed_documents, texts)

    async def aembed_query(self, text: str) -> list[float]:
        return await run_in_executor(None, self.embed_query, text)

10.1.1 为什么区分 embed_documents 和 embed_query?

这个双方法设计看似冗余,实际上有深刻的技术原因。某些嵌入模型(如 Google 的 embedding-001、BGE 系列模型)会根据文本的角色使用不同的编码策略:

  • 文档嵌入:优化为捕捉文档的全局语义,可能使用更长的上下文窗口
  • 查询嵌入:优化为捕捉用户意图,可能添加特殊的前缀指令

大多数模型(如 OpenAI 的 text-embedding-3-small)的两个方法实现相同,但这种接口分离确保了在需要区分时有扩展空间。

flowchart LR
    subgraph 文档索引阶段
        A[文档列表] --> B[embed_documents]
        B --> C[向量列表]
        C --> D[存入 VectorStore]
    end

    subgraph 查询检索阶段
        E[用户查询] --> F[embed_query]
        F --> G[查询向量]
        G --> H[VectorStore 相似搜索]
        H --> I[最相关文档]
    end

10.1.2 异步接口的默认实现

aembed_documentsaembed_query 的默认实现使用 run_in_executor 将同步方法包装到线程池中。这遵循了 LangChain 的一贯模式:提供同步的基础实现,通过线程池桥接异步接口,同时允许子类覆盖为原生异步实现。

10.2 VectorStore:向量存储抽象

VectorStore 是 LangChain 中体量最大的抽象类之一,它定义了向量存储系统的完整接口。

# langchain_core/vectorstores/base.py
class VectorStore(ABC):
    # 写入接口
    def add_texts(self, texts, metadatas=None, *, ids=None, **kwargs) -> list[str]: ...
    def add_documents(self, documents, **kwargs) -> list[str]: ...
    def delete(self, ids=None, **kwargs) -> bool | None: ...
    def get_by_ids(self, ids: Sequence[str], /) -> list[Document]: ...

    # 搜索接口
    @abstractmethod
    def similarity_search(self, query, k=4, **kwargs) -> list[Document]: ...
    def similarity_search_with_score(self, *args, **kwargs) -> list[tuple[Document, float]]: ...
    def similarity_search_with_relevance_scores(self, query, k=4, **kwargs) -> list[tuple[Document, float]]: ...
    def max_marginal_relevance_search(self, query, k=4, fetch_k=20, lambda_mult=0.5, **kwargs) -> list[Document]: ...

    # 构造接口
    @classmethod
    @abstractmethod
    def from_texts(cls, texts, embedding, metadatas=None, **kwargs) -> Self: ...
    def as_retriever(self, **kwargs) -> VectorStoreRetriever: ...
classDiagram
    class VectorStore {
        <<abstract>>
        +embeddings: Embeddings
        +add_texts(texts, metadatas, ids) list~str~
        +add_documents(documents) list~str~
        +delete(ids) bool
        +get_by_ids(ids) list~Document~
        +similarity_search(query, k) list~Document~*
        +similarity_search_with_score() list~tuple~
        +similarity_search_with_relevance_scores(query, k) list~tuple~
        +max_marginal_relevance_search(query, k, fetch_k, lambda_mult) list~Document~
        +search(query, search_type) list~Document~
        +from_texts(texts, embedding) Self*
        +from_documents(documents, embedding) Self
        +as_retriever() VectorStoreRetriever
        #_select_relevance_score_fn() Callable
        #_euclidean_relevance_score_fn(distance) float$
        #_cosine_relevance_score_fn(distance) float$
        #_max_inner_product_relevance_score_fn(distance) float$
    }

    class InMemoryVectorStore {
        +store: dict
        +embedding: Embeddings
        +similarity_search(query, k) list~Document~
        +max_marginal_relevance_search(query, k) list~Document~
        +dump(path) void
        +load(path, embedding) InMemoryVectorStore$
    }

    VectorStore <|-- InMemoryVectorStore

10.2.1 写入接口的双路径设计

VectorStore 的写入接口存在 add_textsadd_documents 两个方法,它们之间有一个精巧的互相委托关系:

def add_texts(self, texts, metadatas=None, *, ids=None, **kwargs):
    if type(self).add_documents != VectorStore.add_documents:
        # 子类实现了 add_documents,委托给它
        docs = [Document(id=id_, page_content=text, metadata=metadata_)
                for text, metadata_, id_ in zip(texts, metadatas_, ids_, strict=False)]
        return self.add_documents(docs, **kwargs)
    raise NotImplementedError(...)

def add_documents(self, documents, **kwargs):
    if type(self).add_texts != VectorStore.add_texts:
        # 子类实现了 add_texts,委托给它
        texts = [doc.page_content for doc in documents]
        metadatas = [doc.metadata for doc in documents]
        return self.add_texts(texts, metadatas, **kwargs)
    raise NotImplementedError(...)

这种设计允许子类只实现其中一个方法,另一个会自动适配。通过 type(self).method != VectorStore.method 检测子类是否覆盖了特定方法,避免了无限递归。

10.2.2 搜索方法族

VectorStore 提供了三种搜索策略:

方法返回类型特点
similarity_searchlist[Document]最基础的相似度搜索
similarity_search_with_relevance_scoreslist[tuple[Document, float]]返回归一化到 [0,1] 的相关性分数
max_marginal_relevance_searchlist[Document]MMR 算法,平衡相关性和多样性
相关性分数归一化

不同的向量数据库返回不同度量的原始分数(余弦距离、欧氏距离、内积等)。LangChain 通过 _select_relevance_score_fn 模板方法,让子类选择合适的归一化函数:

@staticmethod
def _euclidean_relevance_score_fn(distance: float) -> float:
    return 1.0 - distance / math.sqrt(2)

@staticmethod
def _cosine_relevance_score_fn(distance: float) -> float:
    return 1.0 - distance

@staticmethod
def _max_inner_product_relevance_score_fn(distance: float) -> float:
    if distance > 0:
        return 1.0 - distance
    return -1.0 * distance

所有归一化函数都将分数映射到 [0, 1] 区间,其中 1 表示最相似。这使得 similarity_search_with_relevance_scores 的返回值在不同后端之间具有可比性。

分数阈值过滤

similarity_search_with_relevance_scores 支持 score_threshold 参数:

def similarity_search_with_relevance_scores(self, query, k=4, **kwargs):
    score_threshold = kwargs.pop("score_threshold", None)
    docs_and_similarities = self._similarity_search_with_relevance_scores(query, k=k, **kwargs)

    if score_threshold is not None:
        docs_and_similarities = [
            (doc, similarity)
            for doc, similarity in docs_and_similarities
            if similarity >= score_threshold
        ]
    return docs_and_similarities

这个过滤机制使得检索系统可以动态控制结果质量 -- 当没有足够相关的文档时,返回空列表优于返回不相关的结果。

10.2.3 as_retriever:从存储到检索的桥梁

def as_retriever(self, **kwargs) -> VectorStoreRetriever:
    tags = kwargs.pop("tags", None) or [*self._get_retriever_tags()]
    return VectorStoreRetriever(vectorstore=self, tags=tags, **kwargs)

这个方法是适配器模式的经典应用 -- 它将 VectorStore 包装为符合 BaseRetriever 接口的对象,使其能够直接用于 LCEL 链。_get_retriever_tags 自动生成包含 VectorStore 类名和 Embeddings 类名的标签,用于追踪和调试。

10.3 InMemoryVectorStore:参考实现

InMemoryVectorStore 是 LangChain 内置的基于内存的向量存储实现。虽然不适用于生产环境的大规模数据,但它作为参考实现完整展示了 VectorStore 接口的正确用法。

10.3.1 数据存储结构

class InMemoryVectorStore(VectorStore):
    def __init__(self, embedding: Embeddings) -> None:
        self.store: dict[str, dict[str, Any]] = {}
        self.embedding = embedding

每条记录存储为一个字典:

self.store[doc_id] = {
    "id": doc_id,
    "vector": vector,      # list[float] - 嵌入向量
    "text": doc.page_content,
    "metadata": doc.metadata,
}

10.3.2 余弦相似度搜索

核心搜索方法使用 numpy 实现的余弦相似度:

def _similarity_search_with_score_by_vector(self, embedding, k=4, filter=None):
    docs = list(self.store.values())
    if filter is not None:
        docs = [doc for doc in docs
                if filter(Document(id=doc["id"], page_content=doc["text"], metadata=doc["metadata"]))]

    if not docs:
        return []

    similarity = cosine_similarity([embedding], [doc["vector"] for doc in docs])[0]
    top_k_idx = similarity.argsort()[::-1][:k]

    return [
        (Document(id=doc_dict["id"], page_content=doc_dict["text"], metadata=doc_dict["metadata"]),
         float(similarity[idx].item()),
         doc_dict["vector"])
        for idx in top_k_idx
        if (doc_dict := docs[idx])
    ]

这段代码的执行流程:

  1. 如果提供了 filter 函数,先过滤文档
  2. 计算查询向量与所有文档向量的余弦相似度
  3. 按相似度降序排序,取 top-k

filter 参数接受一个 Callable[[Document], bool],允许在向量搜索的基础上进行元数据过滤。这比在搜索后再过滤更高效,因为它减少了需要排序的候选集大小。

flowchart TD
    A[查询向量] --> B[计算与所有文档的余弦相似度]
    B --> C{有 filter?}
    C -->|是| D[先按元数据过滤]
    D --> E[对过滤后的文档计算相似度]
    C -->|否| E[对所有文档计算相似度]
    E --> F[argsort 降序排序]
    F --> G[取 top-k 个索引]
    G --> H[构造 Document + score 返回]

10.3.3 余弦相似度的实现

# langchain_core/vectorstores/utils.py
def _cosine_similarity(x: Matrix, y: Matrix) -> np.ndarray:
    x = np.array(x)
    y = np.array(y)
    if not _HAS_SIMSIMD:
        x_norm = np.linalg.norm(x, axis=1)
        y_norm = np.linalg.norm(y, axis=1)
        with np.errstate(divide="ignore", invalid="ignore"):
            similarity = np.dot(x, y.T) / np.outer(x_norm, y_norm)
        similarity[np.isnan(similarity) | np.isinf(similarity)] = 0.0
        return similarity
    # 使用 simsimd 加速
    x = np.array(x, dtype=np.float32)
    y = np.array(y, dtype=np.float32)
    return 1 - np.array(simd.cdist(x, y, metric="cosine"))

实现中有几个值得注意的细节:

  1. simsimd 加速:优先使用 simsimd 库,它是用 C 实现的 SIMD 优化距离计算库,比纯 numpy 快数倍
  2. NaN/Inf 处理:对于零向量等边界情况,相似度计算会产生 NaN/Inf,统一替换为 0.0
  3. 安全检查:提前检测输入中的 NaN 和 Inf,发出警告

10.3.4 MMR 算法

最大边际相关性(Maximal Marginal Relevance)算法在保证相关性的同时增加结果的多样性:

def maximal_marginal_relevance(query_embedding, embedding_list, lambda_mult=0.5, k=4):
    if min(k, len(embedding_list)) <= 0:
        return []

    similarity_to_query = _cosine_similarity(query_embedding, embedding_list)[0]
    most_similar = int(np.argmax(similarity_to_query))
    idxs = [most_similar]
    selected = np.array([embedding_list[most_similar]])

    while len(idxs) < min(k, len(embedding_list)):
        best_score = -np.inf
        idx_to_add = -1
        similarity_to_selected = _cosine_similarity(embedding_list, selected)

        for i, query_score in enumerate(similarity_to_query):
            if i in idxs:
                continue
            redundant_score = max(similarity_to_selected[i])
            equation_score = lambda_mult * query_score - (1 - lambda_mult) * redundant_score
            if equation_score > best_score:
                best_score = equation_score
                idx_to_add = i

        idxs.append(idx_to_add)
        selected = np.append(selected, [embedding_list[idx_to_add]], axis=0)
    return idxs

MMR 公式:score = lambda * sim(doc, query) - (1 - lambda) * max(sim(doc, selected_docs))

flowchart TD
    A[计算所有文档与查询的相似度] --> B[选择最相似的文档加入结果集]
    B --> C{结果集达到 k?}
    C -->|否| D[对每个未选文档计算 MMR 分数]
    D --> E["MMR = lambda * 查询相似度 - (1-lambda) * 与已选最大相似度"]
    E --> F[选择 MMR 分数最高的文档]
    F --> G[加入结果集]
    G --> C
    C -->|是| H[返回结果索引]

lambda_mult 参数控制相关性和多样性的平衡:

  • lambda_mult = 1.0:等同于普通相似度搜索
  • lambda_mult = 0.0:最大化多样性
  • lambda_mult = 0.5:默认值,相关性和多样性各占一半

10.3.5 持久化支持

InMemoryVectorStore 提供了简单的持久化方法:

def dump(self, path: str) -> None:
    path_ = Path(path)
    path_.parent.mkdir(exist_ok=True, parents=True)
    with path_.open("w", encoding="utf-8") as f:
        json.dump(dumpd(self.store), f, indent=2)

@classmethod
def load(cls, path, embedding, **kwargs):
    with Path(path).open("r", encoding="utf-8") as f:
        store = load(json.load(f), allowed_objects=[Document])
    vectorstore = cls(embedding=embedding, **kwargs)
    vectorstore.store = store
    return vectorstore

使用 LangChain 的序列化系统(dumpd/load)确保了 Document 对象的正确序列化和反序列化。

10.4 BaseRetriever:检索器的 Runnable 抽象

BaseRetriever 是 LangChain 检索系统的核心抽象。它继承自 RunnableSerializable[str, list[Document]],这意味着检索器接收字符串查询,返回文档列表,并且完全兼容 LCEL。

# langchain_core/retrievers.py
RetrieverInput = str
RetrieverOutput = list[Document]

class BaseRetriever(RunnableSerializable[RetrieverInput, RetrieverOutput], ABC):
    tags: list[str] | None = None
    metadata: dict[str, Any] | None = None

    @abstractmethod
    def _get_relevant_documents(
        self, query: str, *, run_manager: CallbackManagerForRetrieverRun
    ) -> list[Document]:
        """Get documents relevant to a query."""

    async def _aget_relevant_documents(
        self, query: str, *, run_manager: AsyncCallbackManagerForRetrieverRun
    ) -> list[Document]:
        return await run_in_executor(
            None, self._get_relevant_documents, query, run_manager=run_manager.get_sync(),
        )

10.4.1 invoke 方法的完整回调集成

BaseRetriever.invoke 的实现展示了 LangChain 回调系统的完整工作流:

def invoke(self, input: str, config: RunnableConfig | None = None, **kwargs) -> list[Document]:
    config = ensure_config(config)
    inheritable_metadata = {
        **(config.get("metadata") or {}),
        **self._get_ls_params(**kwargs),
    }
    callback_manager = CallbackManager.configure(
        config.get("callbacks"), None,
        verbose=kwargs.get("verbose", False),
        inheritable_tags=config.get("tags"),
        local_tags=self.tags,
        inheritable_metadata=inheritable_metadata,
        local_metadata=self.metadata,
    )
    run_manager = callback_manager.on_retriever_start(
        None, input, name=config.get("run_name") or self.get_name(), ...
    )
    try:
        result = self._get_relevant_documents(input, run_manager=run_manager, ...)
    except Exception as e:
        run_manager.on_retriever_error(e)
        raise
    else:
        run_manager.on_retriever_end(result)
        return result
sequenceDiagram
    participant C as Caller
    participant R as BaseRetriever
    participant CM as CallbackManager
    participant I as Implementation

    C->>R: invoke(query, config)
    R->>CM: configure(callbacks, tags, metadata)
    CM->>CM: on_retriever_start(query)
    R->>I: _get_relevant_documents(query, run_manager)
    alt 成功
        I-->>R: documents
        R->>CM: on_retriever_end(documents)
        R-->>C: documents
    else 异常
        I-->>R: Exception
        R->>CM: on_retriever_error(exception)
        R-->>C: raise Exception
    end

这段代码有几个关键设计点:

  1. LangSmith 集成_get_ls_params 返回 LangSmithRetrieverParams,包含检索器名称、向量存储提供商、嵌入模型等信息,用于追踪
  2. 标签继承:通过 inheritable_tagslocal_tags 的区分,支持标签在链式调用中的传播
  3. 错误追踪:无论成功还是失败,都会通知回调管理器,确保可观测性

10.4.2 init_subclass 的兼容性处理

def __init_subclass__(cls, **kwargs):
    super().__init_subclass__(**kwargs)
    parameters = signature(cls._get_relevant_documents).parameters
    cls._new_arg_supported = parameters.get("run_manager") is not None
    # 如果子类未实现 run_manager 参数,自动适配
    if not cls._new_arg_supported and cls._aget_relevant_documents == BaseRetriever._aget_relevant_documents:
        async def _aget_relevant_documents(self, query):
            return await run_in_executor(None, self._get_relevant_documents, query)
        cls._aget_relevant_documents = _aget_relevant_documents

这段代码处理了一个向后兼容问题:早期版本的 _get_relevant_documents 不接受 run_manager 参数。__init_subclass__ 在子类定义时自动检测其签名,如果发现旧式签名(没有 run_manager),就会自动生成一个适配版本的 _aget_relevant_documents

10.5 VectorStoreRetriever:适配器模式

VectorStoreRetrieverVectorStore 适配为 BaseRetriever 接口:

class VectorStoreRetriever(BaseRetriever):
    vectorstore: VectorStore
    search_type: str = "similarity"
    search_kwargs: dict = Field(default_factory=dict)
    allowed_search_types: ClassVar[Collection[str]] = (
        "similarity", "similarity_score_threshold", "mmr",
    )

    @model_validator(mode="before")
    @classmethod
    def validate_search_type(cls, values):
        search_type = values.get("search_type", "similarity")
        if search_type not in cls.allowed_search_types:
            raise ValueError(...)
        if search_type == "similarity_score_threshold":
            score_threshold = values.get("search_kwargs", {}).get("score_threshold")
            if not isinstance(score_threshold, float):
                raise ValueError("score_threshold must be a float")
        return values

10.5.1 搜索策略路由

def _get_relevant_documents(self, query, *, run_manager, **kwargs):
    kwargs_ = self.search_kwargs | kwargs
    if self.search_type == "similarity":
        docs = self.vectorstore.similarity_search(query, **kwargs_)
    elif self.search_type == "similarity_score_threshold":
        docs_and_similarities = self.vectorstore.similarity_search_with_relevance_scores(query, **kwargs_)
        docs = [doc for doc, _ in docs_and_similarities]
    elif self.search_type == "mmr":
        docs = self.vectorstore.max_marginal_relevance_search(query, **kwargs_)
    return docs

三种搜索策略的适用场景:

flowchart TD
    A[VectorStoreRetriever] --> B{search_type?}
    B -->|similarity| C["similarity_search(query, k=4)"]
    C --> D[返回 top-k 最相似文档]
    B -->|similarity_score_threshold| E["similarity_search_with_relevance_scores(query, score_threshold=0.8)"]
    E --> F[返回分数高于阈值的文档]
    B -->|mmr| G["max_marginal_relevance_search(query, k=4, fetch_k=20, lambda_mult=0.5)"]
    G --> H[返回兼顾相关性和多样性的文档]

10.5.2 使用示例

# 基本相似度搜索
retriever = vectorstore.as_retriever(search_kwargs={"k": 5})

# MMR 搜索
retriever = vectorstore.as_retriever(
    search_type="mmr",
    search_kwargs={"k": 5, "fetch_k": 20, "lambda_mult": 0.5}
)

# 分数阈值过滤
retriever = vectorstore.as_retriever(
    search_type="similarity_score_threshold",
    search_kwargs={"score_threshold": 0.8}
)

# 在 LCEL 链中使用
chain = retriever | format_docs | prompt | model | StrOutputParser()

10.6 高级检索策略

LangChain 在基础检索器之上构建了多种高级检索策略,每种策略都针对特定的检索质量问题提供解决方案。

10.6.1 MultiVectorRetriever:多向量检索

MultiVectorRetriever 的核心思想是:用于搜索的向量和最终返回的文档可以是不同的。

class MultiVectorRetriever(BaseRetriever):
    vectorstore: VectorStore        # 存储子文档向量
    docstore: BaseStore[str, Document]  # 存储父文档
    id_key: str = "doc_id"          # 子文档 metadata 中关联父文档的键
    search_type: SearchType = SearchType.similarity

    def _get_relevant_documents(self, query, *, run_manager):
        if self.search_type == SearchType.mmr:
            sub_docs = self.vectorstore.max_marginal_relevance_search(query, **self.search_kwargs)
        else:
            sub_docs = self.vectorstore.similarity_search(query, **self.search_kwargs)

        # 从子文档的 metadata 中提取父文档 ID
        ids = []
        for d in sub_docs:
            if self.id_key in d.metadata and d.metadata[self.id_key] not in ids:
                ids.append(d.metadata[self.id_key])

        # 从 docstore 中获取父文档
        docs = self.docstore.mget(ids)
        return [d for d in docs if d is not None]
flowchart TD
    subgraph 索引阶段
        A[原始文档] --> B[分割为子文档]
        B --> C[为每个子文档生成嵌入]
        C --> D[存入 VectorStore]
        A --> E[存入 DocStore]
        B --> F["子文档 metadata 记录 doc_id"]
    end

    subgraph 检索阶段
        G[查询] --> H[在 VectorStore 中搜索子文档]
        H --> I[提取唯一的 doc_id 集合]
        I --> J[从 DocStore 获取父文档]
        J --> K[返回完整父文档]
    end

10.6.2 ParentDocumentRetriever:父文档检索

ParentDocumentRetriever 继承自 MultiVectorRetriever,自动化了"小块搜索、大块返回"的过程:

class ParentDocumentRetriever(MultiVectorRetriever):
    child_splitter: TextSplitter      # 创建子文档的分割器
    parent_splitter: TextSplitter | None = None  # 创建父文档的分割器
    child_metadata_fields: Sequence[str] | None = None

    def add_documents(self, documents, ids=None, add_to_docstore=True, **kwargs):
        # 可选:先用 parent_splitter 分割为中等大小的父文档
        if self.parent_splitter is not None:
            documents = self.parent_splitter.split_documents(documents)

        # 为每个父文档生成唯一 ID
        doc_ids = [str(uuid.uuid4()) for _ in documents]

        docs = []
        full_docs = []
        for i, doc in enumerate(documents):
            _id = doc_ids[i]
            # 用 child_splitter 进一步分割为子文档
            sub_docs = self.child_splitter.split_documents([doc])
            for _doc in sub_docs:
                _doc.metadata[self.id_key] = _id  # 关联父文档 ID
            docs.extend(sub_docs)
            full_docs.append((_id, doc))

        # 子文档存入向量库,父文档存入文档库
        self.vectorstore.add_documents(docs, **kwargs)
        if add_to_docstore:
            self.docstore.mset(full_docs)

这种设计解决了 RAG 中的经典矛盾:

  • 小块嵌入更精确地捕捉语义,提高检索准确性
  • 大块(父文档)提供更完整的上下文,提高生成质量

10.6.3 ContextualCompressionRetriever:压缩检索

ContextualCompressionRetriever 在基础检索器的结果上应用后处理压缩:

class ContextualCompressionRetriever(BaseRetriever):
    base_compressor: BaseDocumentCompressor
    base_retriever: RetrieverLike

    def _get_relevant_documents(self, query, *, run_manager, **kwargs):
        docs = self.base_retriever.invoke(
            query, config={"callbacks": run_manager.get_child()}, **kwargs,
        )
        if docs:
            compressed_docs = self.base_compressor.compress_documents(
                docs, query, callbacks=run_manager.get_child(),
            )
            return list(compressed_docs)
        return []

这是装饰器模式的典型应用。base_compressor 可以是:

  • LLM 摘要压缩器:用 LLM 提取文档中与查询最相关的段落
  • 重排序器(Reranker):使用交叉编码器模型对结果重新排序
  • 冗余过滤器:去除语义重复的文档

10.6.4 MergerRetriever:融合检索

MergerRetriever 将多个检索器的结果合并:

class MergerRetriever(BaseRetriever):
    retrievers: list[BaseRetriever]

    def merge_documents(self, query, run_manager):
        retriever_docs = [
            retriever.invoke(query, config={"callbacks": run_manager.get_child(f"retriever_{i + 1}")})
            for i, retriever in enumerate(self.retrievers)
        ]
        # 交叉合并:轮流从每个检索器取文档
        merged_documents = []
        max_docs = max(map(len, retriever_docs), default=0)
        for i in range(max_docs):
            for _retriever, doc in zip(self.retrievers, retriever_docs, strict=False):
                if i < len(doc):
                    merged_documents.append(doc[i])
        return merged_documents
flowchart TD
    Q[查询] --> R1[检索器 1: 稠密向量]
    Q --> R2[检索器 2: 稀疏 BM25]
    Q --> R3[检索器 3: 关键词]
    R1 --> M[MergerRetriever]
    R2 --> M
    R3 --> M
    M --> D["交叉合并排序"]
    D --> F["融合结果"]

交叉合并策略(round-robin)确保了每个检索器的高优先级结果都能靠前出现。异步版本使用 asyncio.gather 并行调用所有检索器,显著减少延迟。

10.7 LangSmith 检索追踪

BaseRetriever 集成了 LangSmith 追踪参数:

class LangSmithRetrieverParams(TypedDict, total=False):
    ls_retriever_name: str
    ls_vector_store_provider: str | None
    ls_embedding_provider: str | None
    ls_embedding_model: str | None

VectorStoreRetriever 覆盖了 _get_ls_params 方法,自动填充这些追踪参数:

def _get_ls_params(self, **kwargs):
    ls_params = super()._get_ls_params(**kwargs)
    ls_params["ls_vector_store_provider"] = self.vectorstore.__class__.__name__
    if self.vectorstore.embeddings:
        ls_params["ls_embedding_provider"] = self.vectorstore.embeddings.__class__.__name__
    return ls_params

这使得在 LangSmith 中可以按向量存储提供商和嵌入模型进行过滤和分析检索性能。

10.8 设计决策分析

VectorStore 为什么不是 Runnable?

BaseRetriever 不同,VectorStore 不继承 Runnable。这是因为 VectorStore 的操作语义更复杂 -- 它不仅仅是"输入查询、输出文档",还涉及写入、删除、ID 查询等操作。as_retriever() 方法将 VectorStore 的搜索能力投射为 Runnable 接口,保持了 VectorStore 自身接口的完整性。

为什么 similarity_search 是唯一的抽象方法?

在 VectorStore 的众多搜索方法中,只有 similarity_searchfrom_texts 是抽象方法。其他方法(MMR、relevance_scores 等)都有基于 similarity_search_with_score 的默认实现。这降低了实现一个新 VectorStore 后端的门槛 -- 只需实现最基本的相似度搜索,其他搜索策略就能自动可用。

MMR 的 fetch_k 参数

MMR 搜索有一个 fetch_k 参数(默认 20),它决定了先获取多少候选文档,再从中通过 MMR 算法选取 k 个。fetch_k > k 确保了 MMR 有足够的候选集来优化多样性。fetch_k 过小会限制多样性的空间,过大会增加计算成本。

BaseRetriever 的 _new_arg_supported 机制

这是 LangChain 处理 API 演进的一个典型模式。当框架需要为已有的抽象方法添加新参数时,不能简单地修改签名(会破坏所有现有实现)。通过 __init_subclass__ 动态检测子类签名并自动适配,既保持了新 API 的功能,又不破坏旧代码。

10.9 小结

LangChain 的向量存储与检索器系统构建了从嵌入到检索的完整链路。Embeddings 接口通过 embed_documents/embed_query 的分离,为不同角色的文本嵌入留下了优化空间。VectorStore 抽象通过精心设计的方法族,支持从简单相似度搜索到 MMR 多样性搜索的全方位检索能力。InMemoryVectorStore 作为参考实现,完整展示了余弦相似度计算、MMR 算法、持久化等核心功能。

BaseRetriever 作为 Runnable 的子类,使得检索器天然融入 LCEL 链。其 invoke 方法中精心编排的回调管理、LangSmith 追踪参数、以及 __init_subclass__ 的兼容性处理,体现了 LangChain 在可观测性和向后兼容方面的深思熟虑。VectorStoreRetriever 通过适配器模式优雅地连接了存储层和检索层。

高级检索策略 -- MultiVectorRetriever 的"小块搜索、大块返回"、ParentDocumentRetriever 的自动化父子文档管理、ContextualCompressionRetriever 的后处理压缩、MergerRetriever 的多源融合 -- 展示了 LangChain 检索系统的丰富生态。这些组件通过统一的 BaseRetriever 接口可以自由组合,构建出适应各种复杂检索需求的解决方案。

检索质量是 RAG 系统的生命线。理解这些抽象的设计意图和实现细节,是构建高质量 RAG 应用的基础。