LangChain LCEL源码深度剖析

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 中所有可串联组件(PromptTemplatellmparser、自定义函数等)全部继承自 Runnable 抽象类,并统一遵守一套执行协议:

  1. 重载 __or__ 方法,支持 | 管道拼接;
  2. 统一对外执行入口:invoke() / stream() / batch()
  3. 采用惰性求值| 仅组装链路,不执行业务逻辑;只有调用 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 执行过程:

  1. 执行 prompt | llm

    • 调用 prompt.__or__(llm),进入 RunnableSequence(first=prompt, last=llm)
    • 内部扁平化后,steps = [prompt, llm]
  2. 执行 temp_seq | parser

    • temp_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 调用,和 promptllmparser 行为完全一致,是扩展管道能力最常用的组件。

适用场景:数据预处理、结果二次加工、简单逻辑判断、格式转换等自定义逻辑。

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)

核心特点:

  1. 天然支持 __or__ 运算符,可自由和其他 Runnable 拼接;
  2. 输入 = 管道上一级节点输出,输出 = 传递给下一级节点;
  3. 不破坏原有链路结构,即插即用。

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. 结果逐层回传,得到最终返回值
相关推荐
用心_承载未来1 小时前
从“复制链接→打开APP“到“一键解析“:我做了个短视频去水印工具
python·去水印·短视频去水印
TYUT_xiaoming1 小时前
yolo模型训练
人工智能·python·yolo
沪漂阿龙2 小时前
《LangChain 系列》Human-in-the-loop:什么时候必须让人工介入?
人工智能·架构·langchain
MageGojo2 小时前
百度热搜API接入实战:数据结构解析与工程化调用指南
python·数据抓取·api集成·热点数据·接口调试
TechWayfarer2 小时前
查IP归属地接入实战:保险理赔如何做动态风险监控与预警
网络·python·tcp/ip·安全·flask
speop2 小时前
AMD | task02
python
桜吹雪3 小时前
所有智能体架构(3):Planning(计划任务)
javascript·人工智能·langchain
lili00123 小时前
2026 企业 AI 选型新范式:OpenRouter Fusion 证明多模型融合性价比远超单模型,企业该如何重构技术栈? - 微元算力(weytoken)
java·人工智能·python·重构·ai编程
Keano Reurink3 小时前
搜索API与GSC数据对比:发现数据盲区
数据库·python·数据挖掘