数据截至 (上游 commit 2019bf5ebe50)
第 3 章:Runnable / LCEL 地基
agent 层很风光,但它站在
langchain_core的一块地基上:Runnable。本章讲为什么模型、工具、提示词都能用|串起来,并白嫖 stream/batch/async。
3.1 要解决的小问题
LLM 应用里有一堆形状各异的组件:提示词模板(吃 dict 吐字符串)、模型(吃消息吐 AIMessage)、输出解析器(吃 AIMessage 吐结构体)、工具……如果每个组件接口都不一样,你想把它们"串成一条流水线"就得写一堆胶水。
LangChain 的答案:让所有组件实现同一个接口 Runnable。接口统一后,串联、流式、批处理、异步这些能力可以"一次实现,处处复用"。这套用 | 拼链的写法就叫 LCEL(LangChain Expression Language)。
3.2 Runnable 是什么
Runnable 是个抽象基类,核心契约写在它的 docstring 里(runnables/base.py:133-152):
class Runnable(ABC, Generic[Input, Output]):
# 关键方法:
# invoke / ainvoke 单个输入 → 单个输出
# batch / abatch 多个输入 → 多个输出(默认线程池并行)
# stream / astream 单个输入 → 流式产出
白送的能力(docstring 明说,runnables/base.py:145-152):
- batch 默认用线程池并行跑多个
invoke; - async(
a前缀方法)默认把同步版本丢进 asyncio 线程池跑。
所以你只要实现 invoke,batch / ainvoke / stream 就自动有了基础版本——想优化再覆写。这是"实现一个、得到一套"的关键。
3.3 | 的魔法:它只是构造一个对象
a | b 看着像魔法,其实就一行(runnables/base.py:648-673):
def __or__(self, other) -> RunnableSerializable[Input, Any]:
return RunnableSequence(self, coerce_to_runnable(other))
两件事:
a | b等价于RunnableSequence(a, b)——只是构造一个"序列"对象,还没执行任何东西。coerce_to_runnable(other)会把右边"驯化"成 Runnable:普通函数自动包成RunnableLambda,dict 包成RunnableParallel。所以你能写model | (lambda x: x.content),那个裸 lambda 会被自动收编。
官方示例(runnables/base.py:182-188):
from langchain_core.runnables import RunnableLambda
sequence = RunnableLambda(lambda x: x + 1) | RunnableLambda(lambda x: x * 2)
sequence.invoke(1) # 4
sequence.batch([1, 2, 3]) # [4, 6, 8]
3.4 链怎么跑:RunnableSequence
RunnableSequence(runnables/base.py:3075 起)持有一串 step。它的 invoke 本质是"把上一个的输出喂给下一个":
# 示意,非源码:RunnableSequence.invoke 的核心
def invoke(self, input, config):
for step in self.steps:
input = step.invoke(input, config) # 上一步输出 = 下一步输入
return input
关键收益:整条链自己也是一个 Runnable,所以链能再被 | 进更大的链,而且自动获得 stream/batch/async——RunnableSequence 实现了自己的 stream(runnables/base.py:3827)能把流式逐级透传,不用每条链单独适配。
3.5 和 agent 层的关系
这块地基如何支撑上层?
- 模型是 Runnable。
BaseChatModel间接继承Runnable,所以第 1 章里model_.invoke(messages)(factory.py:1454)走的就是 Runnable 接口;init_chat_model的可配置模型_ConfigurableModel也是冲着这套接口实现的。 - 工具是 Runnable。
BaseTool(RunnableSerializable[...])(tools/base.py:434)——工具也能.invoke、也能进链。 - 图节点用
RunnableCallable包裹。 第 1 章那些graph.add_node("model", RunnableCallable(...))正是把普通函数适配成 Runnable 接口,让 LangGraph 统一调度。
一句话:agent 层的"积木 能互相拼",根因就是地基层人人都是 Runnable。
3.6 巧妙之处
- 延迟执行。
a | b只建对象不执行,执行推迟到.invoke/.stream——所以同一条链能 invoke、能 stream、能 batch,行为由你最后调哪个方法决定(runnables/base.py:__or__)。 - 默认实现兜底,覆写做优化。
batch默认线程池、async默认丢线程池,既保证"人人都有这些方法",又允许像RunnableSequence那样覆写出原生流式(runnables/base.py:145-152、3815)。 - 自动驯化非 Runnable。
coerce_to_runnable让裸函数、dict 也能进链,大幅降低拼链门槛(runnables/base.py:673)。
3.7 边界与局限
- 默认
batch的并行靠线程池,对纯 CPU 任务受 GIL 限制;真正受益的是 IO 型(网络调模型)。 - LCEL 链是"线性/并行管道",表达不了带循环和条件分支的复杂控制流——那正是上层为什么用 LangGraph 状态图(第 1 章)而不是一条 LCEL 链来实现 agent 循环的原因。
3.8 代码地图
| 主题 | 文件路径 | 关键符号 |
|---|---|---|
| 统一接口与契约 | libs/core/langchain_core/runnables/base.py | Runnable |
| ` | ` 构造序列 | libs/core/langchain_core/runnables/base.py |
| 序列执行 | libs/core/langchain_core/runnables/base.py | RunnableSequence |
| 函数→Runnable | libs/core/langchain_core/runnables/base.py | RunnableLambda(经 coerce_to_runnable) |
| 模型抽象(也是 Runnable) | libs/core/langchain_core/language_models/chat_models.py | BaseChatModel |
| 工具抽象(也是 Runnable) | libs/core/langchain_core/tools/base.py | BaseTool |