两个Runnable核心类的讲解与使用
概述
本章节学习 LangChain 中两个核心的 Runnable 工具类:RunnableParallel 和 RunnablePassthrough。这两个工具类用于构建复杂的 LLM 应用流程,实现并行处理和数据传递。
1. RunnableParallel - 并行执行
是什么
RunnableParallel 是 LangChain 中的一个工具类,用于并行执行多个 Runnable 对象。它允许同时调用多个链,并将所有结果组合成一个字典返回。
有什么用
- 提高执行效率:多个独立的任务可以并行执行,减少总等待时间
- 结果聚合:将多个执行结果自动组合成一个统一的字典结构
- 简化代码:避免手动管理多个异步或并发任务
使用场景
- 同时生成多种类型的内容(如笑话、诗歌、摘要等)
- 并行调用不同的模型或提示词模板
- 需要将多个结果合并后传递给下一步骤
基础示例
python
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableParallel
from langchain_openai import ChatOpenAI
import dotenv
dotenv.load_dotenv()
# 1. 编排prompt
joke_prompt = ChatPromptTemplate.from_template("请讲一个关于{subject}的冷笑话,尽可能短一些")
poem_prompt = ChatPromptTemplate.from_template("请写一篇关于{subject}的诗,尽可能短一些")
# 2. 创建大语言模型
llm = ChatOpenAI(model="moonshot-v1-8k")
# 3. 创建输出解析器
parser = StrOutputParser()
# 4. 编排链
joke_chain = joke_prompt | llm | parser
poem_chain = poem_prompt | llm | parser
# 5. 并行链
map_chain = RunnableParallel(joke=joke_chain, poem=poem_chain)
# 6. 调用
res = map_chain.invoke({"subject": "程序员"})
print(res)
输出结果
python
{
'joke': '问:为什么程序员总是混淆圣诞节和万圣节?\n答:因为他们喜欢 Oct 31(十月三十一日)胜过 Dec 25(十二月二十五日)。',
'poem': '在数字世界里,代码编织梦,\n程序员,夜以继日,键盘敲击声。\n逻辑的海洋,算法的风,\n创造奇迹,于无形。'
}
另一种写法
python
# 使用字典形式创建
map_chain = RunnableParallel({
"joke": joke_chain,
"poem": poem_chain,
})
执行流程
json
输入: {"subject": "程序员"}
↓
RunnableParallel (并行执行)
├─ joke_chain → "冷笑话内容"
└─ poem_chain → "诗歌内容"
↓
输出: {"joke": "...", "poem": "..."}
2. RunnableParallel 模拟检索
是什么
这是 RunnableParallel 在 RAG(检索增强生成)场景中的典型应用。通过并行执行检索函数和原始数据传递,为后续的 LLM 调用准备完整的上下文。
有什么用
- 数据预处理:在调用 LLM 之前,先执行检索或其他数据处理
- 保持原始数据:使用 itemgetter 保留原始输入字段
- 数据增强:为原始数据添加额外的上下文信息
核心组件
itemgetter
Python 标准库 operator 模块中的函数,用于从字典中提取指定字段。在 LCEL 链中用于保留或选择特定的输入字段。
示例代码
python
from operator import itemgetter
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
import dotenv
dotenv.load_dotenv()
def retrieval(query: str) -> str:
"""一个模拟的检索器函数"""
print("正在检索:", query)
return "我是慕小课"
# 1. 编排prompt
prompt = ChatPromptTemplate.from_template("""请根据用户的问题回答,可以参考对应的上下文进行生成。
<context>
{context}
</context>
用户的提问是: {query}""")
# 2. 构建大语言模型
llm = ChatOpenAI(model="moonshot-v1-8k")
# 3. 输出解析器
parser = StrOutputParser()
# 4. 构建链
chain = {
"context": lambda x: retrieval(x["query"]), # 执行检索
"query": itemgetter("query"), # 保留原始query字段
} | prompt | llm | parser
# 5. 调用链
content = chain.invoke({"query": "你好,我是谁?"})
print(content)
输出结果
makefile
正在检索: 你好,我是谁?
你好,根据你提供的上下文,你是慕小课。
执行流程
erlang
输入: {"query": "你好,我是谁?"}
↓
RunnableParallel (并行处理)
├─ context → retrieval函数 → "我是慕小课"
└─ query → itemgetter("query") → "你好,我是谁?"
↓
Prompt模板 → 填充context和query
↓
LLM → 生成回答
↓
输出: "你好,根据你提供的上下文,你是慕小课。"
3. RunnablePassthrough - 数据透传
是什么
RunnablePassthrough 是一个特殊的 Runnable,主要用于在链中传递原始输入数据。配合 assign 方法使用时,可以在保留原始数据的同时添加新字段。
有什么用
- 简化链的构建:避免显式使用 RunnableParallel 和 itemgetter
- 保留原始输入:自动将所有输入字段传递到下一步
- 动态添加字段:通过 assign 方法在执行过程中添加新字段
与 RunnableParallel 的区别
| 特性 | RunnableParallel | RunnablePassthrough.assign |
|---|---|---|
| 原始数据传递 | 需要手动指定每个字段 | 自动保留所有原始输入 |
| 添加新字段 | 通过字典指定 | 通过 assign 方法添加 |
| 代码简洁度 | 较冗长 | 更简洁 |
| 适用场景 | 需要精确控制字段 | 只需添加少量字段 |
示例代码
python
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_openai import ChatOpenAI
import dotenv
dotenv.load_dotenv()
def retrieval(query: str) -> str:
"""一个模拟的检索器函数"""
print("正在检索:", query)
return "我是慕小课"
# 1. 编排prompt
prompt = ChatPromptTemplate.from_template("""请根据用户的问题回答,可以参考对应的上下文进行生成。
<context>
{context}
</context>
用户的提问是: {query}""")
# 2. 构建大语言模型
llm = ChatOpenAI(model="moonshot-v1-8k")
# 3. 输出解析器
parser = StrOutputParser()
# 4. 构建链
chain = RunnablePassthrough.assign(
context=lambda x: retrieval(x["query"])
) | prompt | llm | parser
# 5. 调用链
content = chain.invoke({"query": "你好,我是谁?"})
print(content)
输出结果
makefile
正在检索: 你好,我是谁?
你好,根据你提供的上下文,你是慕小课。
执行流程
css
输入: {"query": "你好,我是谁?"}
↓
RunnablePassthrough.assign
├─ 保留原始字段 → {"query": "你好,我是谁?"}
└─ 添加context字段 → {"query": "...", "context": "我是慕小课"}
↓
Prompt模板 → 填充context和query
↓
LLM → 生成回答
↓
输出: "你好,根据你提供的上下文,你是慕小课。"
代码对比
使用 RunnableParallel(方式1)
python
chain = {
"context": lambda x: retrieval(x["query"]),
"query": itemgetter("query"),
} | prompt | llm | parser
使用 RunnablePassthrough(方式2)
python
chain = RunnablePassthrough.assign(
context=lambda x: retrieval(x["query"])
) | prompt | llm | parser
方式2的优势:
- 更简洁,不需要导入 itemgetter
- 自动保留所有原始输入字段
- 代码可读性更好
总结
工具类对比
| 工具类 | 主要用途 | 典型场景 |
|---|---|---|
| RunnableParallel | 并行执行多个链,聚合结果 | 同时生成多种内容、并行调用不同模型 |
| RunnablePassthrough | 透传数据,动态添加字段 | RAG检索、数据增强处理 |
选择建议
- 需要并行执行多个独立任务时,使用 RunnableParallel
- 只需要在原始数据基础上添加少量字段时,使用 RunnablePassthrough.assign
- 需要精确控制传递哪些字段时,使用 RunnableParallel + itemgetter
RAG 模式推荐写法
在检索增强生成场景中,推荐使用 RunnablePassthrough.assign:
python
chain = RunnablePassthrough.assign(
context=lambda x: retrieval(x["query"])
) | prompt | llm | parser
这种写法简洁明了,易于维护,是 LangChain 官方推荐的模式。