LangChain 向量检索为什么要用 as_retriever?从 LCEL 到 RunnableParallel 一次讲清

第一次用 LangChain 搭建 RAG 链时,很多人都会遇到同一个疑问:向量库明明可以通过 similarity_search() 检索文档,为什么还要调用 as_retriever(),才能接入 LCEL?

更让人困惑的是,代码中一个普通的字典,为什么能让同一个用户问题同时流向 inputcontext 两个分支?

本文通过一个完整示例,解释以下几个问题:

  • 为什么 VectorStore 不能直接作为 LCEL 链中的检索步骤?
  • as_retriever() 做了什么?
  • similarity_search()as_retriever() 有什么区别?
  • 字典为什么可以把同一个输入分发给多个分支?
  • RunnablePassthrough() 在检索链中有什么作用?

一、示例:

python 复制代码
from langchain_qwq import ChatQwen
from langchain_community.embeddings import DashScopeEmbeddings
from langchain_core.documents import Document
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.vectorstores import InMemoryVectorStore


def print_prompt(prompt):
    """打印发送给模型的提示词,并把提示词继续传给下一个步骤。"""
    print("发送给模型的提示词:")
    print(prompt.to_string())
    print("=" * 40)
    return prompt


# 当前官方 Qwen 集成使用 ChatQwen。
model = ChatQwen(model="qwen3-max")

prompt = ChatPromptTemplate.from_messages(
    [
        (
            "system",
            "请优先依据参考资料回答问题,回答要简洁、专业。\n\n"
            "参考资料:\n{context}",
        ),
        ("human", "用户问题:{input}"),
    ]
)

# InMemoryVectorStore 只适合演示,进程结束后数据会丢失。
embeddings = DashScopeEmbeddings(model="text-embedding-v4")
vector_store = InMemoryVectorStore(embedding=embeddings)

# add_texts 接收原始字符串,并调用 embedding 模型生成向量后写入向量库。
# 如果数据已经经过文档加载、切分,并且需要保留 metadata,通常使用 add_documents。
vector_store.add_texts(
    texts=[
        "减肥就是要少吃多练。",
        "减脂期间需要合理饮食,清淡少油,控制热量摄入,并保持适量运动。",
        "跑步是一种常见的有氧运动。",
    ]
)

input_text = "怎么减肥?"

# as_retriever() 返回 VectorStoreRetriever。
# 它实现了 Runnable 接口,因此可以直接接入 LCEL。
# 指定返回两个相关的向量 作用与 similarity_search 这个方法相似 两者区别在下文会展示
# 进行向量检索,返回向量
retriever = vector_store.as_retriever(search_kwargs={"k": 2})


def format_docs(docs: list[Document]) -> str:
    """把检索到的 Document 列表整理成提示词中的上下文字符串。"""
    if not docs:  #如果为空说明没有检索到
        return "无相关参考资料"

    return "\n\n".join(
        f"[资料 {index}]\n{doc.page_content}"
        for index, doc in enumerate(docs, start=1)
    )


chain = (
    {
        # 保留原始用户问题,供 Prompt 的 {input} 使用。
        "input": RunnablePassthrough(),
        # 检索文档并格式化,供 Prompt 的 {context} 使用。
        "context": retriever | format_docs,
    }
    | prompt
    | print_prompt
    | model
    | StrOutputParser()
)

result = chain.invoke(input_text)
print(result)

这条链中,各组件的输入和输出如下:

组件 输入 输出
retriever str 类型的用户问题 list[Document]
format_docs list[Document] str 类型的上下文
字典分支 同一个用户问题 {"input": ..., "context": ...}
prompt 包含问题和上下文的字典 ChatPromptValue
model 格式化后的消息 AIMessage
StrOutputParser AIMessage str

如图是对这个流程的详细解释:

二、similarity_search()as_retriever()

1. similarity_search():直接执行搜索

向量库可以直接调用 similarity_search()

python 复制代码
docs = vector_store.similarity_search(
    query="怎么减肥?",
    k=2,
)

print(docs)  # list[Document]

这个方法会立即执行搜索,并直接返回一个 list[Document]

similarity_search() 返回的是已经计算完成的数据,而不是一个可以等待输入、再执行搜索的 Runnable 步骤。把它直接写在链外,会导致搜索在构建链时就执行,无法使用每次调用时传入的用户问题。

如果确实需要把 similarity_search() 包装成 LCEL 步骤,也可以使用 RunnableLambda

python 复制代码
from langchain_core.runnables import RunnableLambda

search = RunnableLambda(
    lambda query: vector_store.similarity_search(query, k=2)
)

不过,在向量检索场景中更常用的写法是 as_retriever(),因为它提供了统一的 Retriever/Runnable 接口。

2. as_retriever():把向量库转换成检索器

python 复制代码
retriever = vector_store.as_retriever(
    search_kwargs={"k": 2}
)

docs = retriever.invoke("怎么减肥?")

as_retriever() 并不是删除或替代 similarity_search(),而是把向量库的搜索能力封装成 VectorStoreRetriever。检索器遵循标准 Runnable 接口,可以使用 invoke()ainvoke()batch() 等方法,因此能够自然地放入 LCEL 链中。

从职责上看,可以这样区分:

对象 主要职责 常见调用方式
VectorStore 保存向量,并提供底层搜索方法 similarity_search()
VectorStoreRetriever 用向量库实现 Retriever 接口 vector_store.as_retriever()
1. 方法作用

as_retriever() 的作用是将底层向量库的原生检索能力,封装成统一、可插拔的 VectorStoreRetriever 对象。

2. 核心参数解析

该方法接受关键字参数(**kwargs),主要包含两个核心配置:

  • search_type(检索类型)

    :定义检索器执行的搜索策略,默认是 similarity

    支持以下三种类型:

    • similarity:基础相似度搜索,优先返回与查询最相关的文档。
    • mmr:最大边际相关性搜索(Maximal Marginal Relevance),在保持相关性的同时,最大化结果的多样性,避免返回大量重复内容。
    • similarity_score_threshold:带相似度阈值的搜索,只返回得分高于指定阈值的文档,用于精准筛选。
  • search_kwargs(检索参数字典)

    传递给具体搜索函数的参数,常见的有:

    • k:返回的文档数量(默认为 4)。
    • score_threshold:用于 similarity_score_threshold 模式,设定最低相关性得分。
    • fetch_k:用于 mmr 模式,表示先拉取多少条候选文档来进行多样性计算(默认为 20)。
    • lambda_mult:用于 mmr 模式,控制多样性权重。值为 1 表示最小多样性(纯相关),值为 0 表示最大多样性(默认为 0.5)。
    • filter:通过文档的元数据(metadata)进行过滤。

三、哪些对象可以接入 LCEL?

使用 | 组合 LCEL 时,最直接的对象是实现了 Runnable 接口的组件。除此之外,LCEL 还会把部分 Python 对象自动转换成 Runnable。

1. 原生 Runnable 组件

常见组件包括:

  • Prompt 模板,例如 ChatPromptTemplatePromptTemplate
  • 聊天模型,例如 ChatQwenChatOpenAI
  • 输出解析器,例如 StrOutputParserJsonOutputParser
  • 检索器,例如 VectorStoreRetriever

Runnable 的核心特点是可以接收输入并产生输出,常用方法包括 invoke()ainvoke()batch()stream()

2. 普通函数会被转换为 RunnableLambda

像下面这样的普通函数,可以直接放在 | 后面:

python 复制代码
def format_docs(docs: list[Document]) -> str:
    return "\n\n".join(doc.page_content for doc in docs)


chain = retriever | format_docs

LCEL 会把 format_docs 转换成一个 RunnableLambda,因此它可以接收上一步的输出。

3. 字典会被转换为 RunnableParallel

在 LCEL 表达式中,字典表示多个命名分支:

python 复制代码
parallel_step = {
    "input": RunnablePassthrough(),
    "context": retriever | format_docs,
}

chain = parallel_step | prompt

字典的 key 会成为输出字典的 key,value 则表示对应的处理步骤。这个字典在链中会被转换为 RunnableParallel。 源码如下:

  • Mapping[str, Runnable[...] | Callable[...] | Any]
    • 这是最强大的部分之一。它允许右边是一个字典
    • 作用 :这会自动创建一个并行处理的结构(RunnableParallel)。字典的 Key 是输出结果的键名,Value 是处理逻辑。
    • 例子RunnablePassthrough() | {"joke": joke_chain, "poem": poem_chain}。这会同时运行两个链,最后把结果拼成一个字典 {"joke": "...", "poem": "..."} 返回。

4. 静态值需要包装成 Runnable

普通字符串或数字只是数据,不是可以执行的链步骤。如果要在链中生成固定值,应使用函数或 RunnableLambda 包装:

python 复制代码
from langchain_core.runnables import RunnableLambda

fixed_context = RunnableLambda(lambda _: "这是固定的参考资料")

这样,fixed_context 才能在被调用时返回固定内容。

四、字符串会同时流向字典里的两个分支 这怎么实现的?

这个机制在 LangChain 中被称为 RunnableParallel(并行执行)

虽然代码里只写了一个普通的 Python 字典 {...},但 LangChain 的 LCEL 语法糖会自动把它转换成并行执行的逻辑。

具体实现原理如下:

核心机制:字典即并行

当你写 {"key": value} 并将其放入链中时,LangChain 会执行以下操作:

  1. 自动转换 :它会将这个字典包装成一个 RunnableParallel 对象。
  2. 输入广播 :当链运行到这一步时,上游传下来的数据(在这里是用户输入的字符串 "怎么减肥?")会被同时复制并发送给字典里的每一个值(Value)。
  3. 独立执行:字典里的每个分支独立运行,互不干扰。
  4. 结果合并:所有分支运行结束后,它们的结果会被重新组装成一个新的字典,传递给下一步。

字符串 "怎么减肥?" 是如何流向两个分支的:

python 复制代码
{
   # 分支 1:直接透传
   "input": RunnablePassthrough(), 
    
    # 分支 2:检索并格式化
    "context": retriever | format_func 
}

1. 数据分发(广播)

上游传来的字符串 "怎么减肥?" 同时进入两个分支:

  • 给分支 1RunnablePassthrough() 接收到 "怎么减肥?"
  • 给分支 2retriever 也接收到 "怎么减肥?"

这种同时流向两个分支的效果,不是靠多线程或异步并发实现的(虽然底层支持异步),而是靠 LCEL 的调度器逻辑实现的:它知道这是一个并行结构,因此会将同一个输入变量分别注入到字典的每一个值中去执行

2. 分支处理

  • 分支 1 的处理:
    • RunnablePassthrough() 什么都不做,直接原样返回 "怎么减肥?"
  • 分支 2 的处理:
    • retriever 拿着 "怎么减肥?" 去向量库搜索,返回 [Document1, Document2]
    • format_func 拿着这个列表,将其拼接成字符串 "[减肥就是要少吃多练...]"

3. 结果汇聚

当两个分支都跑完后,LangChain 会把结果拼起来,生成最终传给 Prompt 的字典:

python 复制代码
{
    "input": "怎么减肥?",                # 来自分支 1
    "context": "[减肥就是要少吃多练...]"    # 来自分支 2
}

五、为什么需要 RunnablePassthrough?

既然字符串会自动分发给 retriever,为什么 input 还要专门写RunnablePassthrough()

python 复制代码
这是因为字典的值"(Value)"必须是一个可执行的对象(Runnable)。

当 LCEL 链运行到 "RunnableParallel"(也就是字典)时,调度器会遍历字典里的每一个 value。
"它拿到这个 value 后,必须调用一个统一的方法来启动它。这个方法就是 invoke()"。

+ 如果 value 是 `Runnable` 子类,调度器就可以直接调用 `value.invoke(输入数据)`。
+ 如果 value 是一个普通的字符串 `"怎么减肥?"`,调度器去调用 `"怎么减肥?".invoke() 就会直接报错(
AttributeError),因为普通字符串根本没有这个执行方法。
  • retriever | format_func 本身就是一个复杂的 Runnable 链。

  • 而原始的字符串 "怎么减肥?" 只是一个数据,不是 Runnable。

  • 所以我们需要 RunnablePassthrough() 作为一个"占位符"或"搬运工",它的唯一任务就是接住数据并原样交出去,从而满足字典值的格式要求。

总结

  1. similarity_search() 是向量库的直接搜索方法,调用后立即返回 list[Document]
  2. as_retriever() 会返回 VectorStoreRetriever,让向量搜索具备标准 Runnable 接口。
  3. 普通函数可以自动转换成 RunnableLambda,字典可以自动转换成 RunnableParallel
  4. RunnableParallel 会把同一个输入交给多个分支,并发执行后合并结果。
  5. RunnablePassthrough() 负责保留原始输入,使用户问题可以和检索上下文一起交给 Prompt。
相关推荐
lzjava20243 小时前
LangGraph Subgraphs 子图
langchain
元Y亨H3 小时前
大模型技术-RAG 常用的向量数据库
langchain·llm
元Y亨H3 小时前
大模型技术 Agents 概述
langchain·llm
元Y亨H3 小时前
大模型技术 Agent添加MCP工具
langchain·llm
元Y亨H3 小时前
大模型技术-RAG 向量存储和检索 概述
langchain·llm
元Y亨H3 小时前
大模型技术 LangChain使用MCP
langchain·llm
starzy199011 小时前
LangChain深度解析:模型、链、Agent三大核心,解锁大模型落地的正确姿势
chatgpt·langchain
mingo_敏13 小时前
DeepAgents : 检索(Retrieval)
人工智能·深度学习·langchain
DLYSB_13 小时前
AIOps 大模型实战:基于 LangChain 运维 Agent 的根因诊断与现场声光播报闭环
运维·langchain·报警灯