LangChain LCEL 源码深度解析:prompt | llm | parser 与 chain.invoke() 执行全流程
一、前言
在使用 LangChain 开发应用时,prompt | llm | parser 这种管道式写法是 LCEL(LangChain Expression Language)最具代表性的用法,简洁优雅且可读性极强。很多开发者只会使用,但并不清楚背后 Python 运算符重载 、Runnable 体系、RunnableSequence 串行执行的底层逻辑。
本文从Python 原生语法 、源码分层 、调用链路 三个维度,完整拆解 | 组合链条 + chain.invoke() 的全生命周期,同时补充 RunnableSequence 完整构造逻辑,带你吃透 LCEL 核心原理。
二、前置核心基础知识点
2.1 Python 原生 or 与 | 运算符
Python 中运算符 | 本质是调用对象的 __or__ 魔术方法,语法规则:
python
a | b # 等价于 a.__or__(b)
- 原生场景:
int代表位或运算 ,set代表集合并集; - 注意区分:逻辑或
or和__or__毫无关系,不要混淆。
开发者可以自定义重载 __or__ ,改写 | 的执行逻辑,这也是 LCEL 链式写法的底层语法支撑。
2.2 LangChain Runnable 顶层抽象
LCEL 中所有可串联组件(PromptTemplate、llm、parser、自定义函数等)全部继承自 Runnable 抽象类,并统一遵守一套执行协议:
- 重载
__or__方法,支持|管道拼接; - 统一对外执行入口:
invoke()/stream()/batch(); - 采用惰性求值 :
|仅组装链路,不执行业务逻辑;只有调用invoke()才真正运行。
三、第一阶段:链式组合 prompt | llm | parser(链路构建)
3.1 表达式分步拆解
我们日常编写的链式代码:
python
chain = prompt | llm | parser
Python 会从左到右依次执行运算符,等价分步逻辑:
python
# 第一步:prompt 和 llm 拼接
temp_seq = prompt | llm
# 第二步:在上一层序列基础上追加 parser
chain = temp_seq | parser
核心结论 :整个组合过程只做对象组装,无模型调用、无文本格式化、无 IO 操作。
3.2 源码:Runnable.or 实现管道拼接
LangChain 底层 Runnable 对 __or__ 进行重载,核心作用是将两个 Runnable 封装为 RunnableSequence(串行管道器)。
简化源码伪代码:
python
from abc import ABC
class Runnable(ABC, Generic[Input, Output]):
# 重载 | 运算符
def __or__(
self,
other: Runnable[Output, Other]
| Callable[[Iterator[Output]], Iterator[Other]]
| Callable[[AsyncIterator[Output]], AsyncIterator[Other]]
| Callable[[Output], Other]
| Mapping[str, Runnable[Output, Any] | Callable[[Output], Any] | Any],
) -> RunnableSerializable[Input, Any]:
# coerce_to_runnable()将不同类型转换为Runable,
## Callable -> RunnableLambda
## generator -> RunnableGenerator
return RunnableSequence(self, coerce_to_runnable(other))
3.3 RunnableSequence 完整构造逻辑(重点补充)
RunnableSequence 是 LCEL 串行链路的核心容器,专门用来有序管理多个 Runnable 节点,下面拆解构造方法、成员属性、链式追加逻辑。
3.3.1 核心属性与构造函数
RunnableSequence 提供两种初始化方式:传入首尾节点、直接传入节点列表,同时做扁平化处理,避免多层嵌套序列。
python
class RunnableSequence(Runnable):
# 内部存储有序执行节点列表
steps: list[Runnable]
def __init__(
self,
first: Runnable | None = None,
last: Runnable | None = None,
steps: list[Runnable] | None = None
):
"""
构造函数:支持两种初始化模式
1. 传入 first + last:拼接两个 Runnable
2. 直接传入 steps 列表:批量初始化节点
"""
if steps is not None:
# 直接使用节点列表,并自动扁平化嵌套的 RunnableSequence
self.steps = self._flatten_steps(steps)
elif first is not None and last is not None:
# 拼接两个节点,分别扁平化后合并
first_steps = self._flatten_steps([first])
last_steps = self._flatten_steps([last])
self.steps = first_steps + last_steps
else:
raise ValueError("必须传入 steps 或同时传入 first、last")
def _flatten_steps(self, raw_steps: list[Runnable]) -> list[Runnable]:
"""扁平化处理:拆解嵌套的 RunnableSequence,保证最终都是原子节点"""
flatten_list = []
for item in raw_steps:
if isinstance(item, RunnableSequence):
# 如果当前节点本身是序列,递归展开内部 steps
flatten_list.extend(item.steps)
else:
flatten_list.append(item)
return flatten_list
3.3.2 重载 or 实现链式追加
当左侧已经是 RunnableSequence 时,再次使用 | 会复用构造逻辑,追加新节点并持续扁平化:
python
class RunnableSequence(Runnable):
# 省略 __init__、_flatten_steps 方法...
def __or__(self, other: Runnable) -> "RunnableSequence":
"""支持序列继续拼接新节点"""
# 基于当前已有节点 + 新节点,新建扁平化序列
return RunnableSequence(steps=self.steps + [other])
3.3.3 组合过程推演
结合构造逻辑,重新梳理 prompt | llm | parser 执行过程:
-
执行
prompt | llm- 调用
prompt.__or__(llm),进入RunnableSequence(first=prompt, last=llm) - 内部扁平化后,
steps = [prompt, llm]
- 调用
-
执行
temp_seq | parsertemp_seq本身是RunnableSequence,调用自身__or__(parser)- 执行
RunnableSequence(steps=[prompt, llm, parser]) - 扁平化后最终
steps = [PromptTemplate, llm, parser]
关键特性:全程自动扁平化 ,无论嵌套多少层
RunnableSequence,最终都会合并为一维节点列表,不会出现多层嵌套结构。
组合完成后,chain 变量本质就是一个 RunnableSequence 实例。
四、第二阶段:执行 chain.invoke(input)(真正运行)
链路构建完成后,调用 chain.invoke(输入参数) 才会触发完整业务流程,下面逐层拆解调用栈。
4.1 统一入口:Runnable.invoke
所有 Runnable 组件共用一套入口方法,invoke 为对外暴露接口,内部委派给子类实现 _invoke 抽象方法:
python
class Runnable(ABC):
# 对外统一调用入口
def invoke(self, input, config=None):
return self._invoke(input, config or {})
# 抽象方法,由具体子类实现业务逻辑
def _invoke(self, input, config):
raise NotImplementedError("子类必须实现 _invoke 方法")
因此 chain.invoke(input) 会直接进入 RunnableSequence._invoke。
4.2 核心:RunnableSequence._invoke 串行调度
RunnableSequence 的核心逻辑:遍历内部组件列表,串行执行,上一步输出作为下一步输入。
简化源码伪代码:
python
class RunnableSequence(Runnable):
# 存储串行执行的所有组件
steps: list[Runnable]
def _invoke(self, input, config):
# 初始输入
current_data = input
# 依次执行每一个组件,逐级传递数据
for step in self.steps:
current_data = step.invoke(current_data, config)
# 返回最后一个组件的执行结果
return current_data
执行流转方向:
原始输入 → PromptTemplate → llm → parser → 最终结果
4.3 逐个解析链路内组件执行逻辑
4.3.1 第一步:PromptTemplate 提示词格式化
接收外部传入的字典参数,填充模板变量,生成 llm 可识别的消息结构:
python
class PromptTemplate(Runnable):
def _invoke(self, input: dict, config):
# 1. 填充模板变量,得到完整提示文本
format_text = self.format(**input)
# 2. 转为 LangChain 标准消息对象(HumanMessage)
return self._build_messages(format_text)
4.3.2 第二步:llm 调用大模型接口
接收格式化后的消息列表,发起网络请求调用大模型,返回模型原生响应对象:
python
class BaseChatModel(Runnable):
def _invoke(self, messages, config):
# 1. 拼装请求参数、上下文、回调等配置
# 2. 发起远程接口调用(核心 IO 操作)
llm_resp = self._call_llm_api(messages, config)
# 3. 返回 AIMessage 模型响应对象
return llm_resp
4.3.3 第三步:parser 结果解析
接收模型原生响应,提取、格式化内容,输出最终业务可用结果(以字符串解析器为例):
python
class StrOutputParser(Runnable):
def _invoke(self, ai_message, config):
# 提取模型返回的文本内容
return ai_message.content.strip()
五、完整调用栈总览
1. 业务代码:chain.invoke({"query": "你的问题"})
↓
2. 顶层入口:Runnable.invoke()
↓
3. 串行调度:RunnableSequence._invoke()
├─ 执行 1:PromptTemplate.invoke() 【提示词格式化】
├─ 执行 2:llm.invoke() 【调用大模型】
└─ 执行 3:parser.invoke() 【结果解析】
↓
4. 结果逐层回传,得到最终返回值
六、 RunnableLambda
RunnableLambda 是 LangChain 提供的函数包装器 ,作用是将普通 Python 函数 / lambda 表达式 封装为标准 Runnable 对象。
封装后即可无缝接入 LCEL 管道,支持 | 拼接、invoke 调用,和 prompt、llm、parser 行为完全一致,是扩展管道能力最常用的组件。
适用场景:数据预处理、结果二次加工、简单逻辑判断、格式转换等自定义逻辑。
6.1 底层实现原理
RunnableLambda 同样继承自 Runnable,遵守统一协议,核心就是在 _invoke 内部执行传入的自定义函数。
简化源码伪代码:
python
class RunnableLambda(Runnable):
def __init__(self, func):
# 接收外部传入的可调用对象(普通函数 / lambda)
self.func = func
def _invoke(self, input, config):
# 执行自定义函数,输入为上一个节点的输出
return self.func(input)
核心特点:
- 天然支持
__or__运算符,可自由和其他Runnable拼接; - 输入 = 管道上一级节点输出,输出 = 传递给下一级节点;
- 不破坏原有链路结构,即插即用。
6.2 基础使用示例
python
from langchain_core.runnables import RunnableLambda
# 1. 使用 lambda 表达式封装
add_prefix = RunnableLambda(lambda x: f"【预处理】{x}")
# 2. 使用普通函数封装
def data_transform(input_data):
# 自定义数据处理逻辑
if isinstance(input_data, dict) and "question" in input_data:
return f"用户提问:{input_data['question']}"
return input_data
transform_runnable = RunnableLambda(data_transform)
6.3 融入完整管道
将 RunnableLambda 插入 prompt | llm | parser 链路中,实现多级自定义处理:
python
# 模拟原有组件
prompt = PromptTemplate()
llm = BaseChatModel()
parser = StrOutputParser()
# 在 prompt 之前增加预处理、parser 之后增加后处理
chain = transform_runnable | prompt | llm | parser | add_prefix
# 执行调用
res = chain.invoke({"question": "什么是 LCEL"})
执行流转:
原始输入 → transform_runnable(自定义处理) → PromptTemplate → llm → parser → add_prefix(后置处理)
6.4 完整调用栈总览(+RunnableLambda)
1. 业务代码:chain.invoke({"query": "你的问题"})
↓
2. 顶层入口:Runnable.invoke()
↓
3. 串行调度:RunnableSequence._invoke()
├─ 执行 1:RunnableLambda.invoke() 【自定义预处理】
├─ 执行 2:PromptTemplate.invoke() 【提示词格式化】
├─ 执行 3:llm.invoke() 【调用大模型】
├─ 执行 4:parser.invoke() 【结果解析】
└─ 执行 5:RunnableLambda.invoke() 【自定义后处理】
↓
4. 结果逐层回传,得到最终返回值