跳到主要内容

第 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-667):

def __or__(self, other) -> RunnableSerializable[Input, Any]:
return RunnableSequence(self, coerce_to_runnable(other))

两件事:

  1. a | b 等价于 RunnableSequence(a, b)——只是构造一个"序列"对象,还没执行任何东西
  2. 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:3063 起)持有一串 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:3815)能把流式逐级透传,不用每条链单独适配。

3.5 和 agent 层的关系

这块地基如何支撑上层?

  • 模型是 Runnable。 BaseChatModel 间接继承 Runnable,所以第 1 章里 model_.invoke(messages)(factory.py:1419)走的就是 Runnable 接口;init_chat_model 的可配置模型 _ConfigurableModel 也是冲着这套接口实现的。
  • 工具是 Runnable。 BaseTool(RunnableSerializable[...])(tools/base.py:427)——工具也能 .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-1523815)。
  • 自动驯化非 Runnable。 coerce_to_runnable 让裸函数、dict 也能进链,大幅降低拼链门槛(runnables/base.py:667)。

3.7 边界与局限

  • 默认 batch 的并行靠线程池,对纯 CPU 任务受 GIL 限制;真正受益的是 IO 型(网络调模型)。
  • LCEL 链是"线性/并行管道",表达不了带循环和条件分支的复杂控制流——那正是上层为什么用 LangGraph 状态图(第 1 章)而不是一条 LCEL 链来实现 agent 循环的原因。

3.8 代码地图

主题文件路径关键符号
统一接口与契约libs/core/langchain_core/runnables/base.pyRunnable
`` 构造序列libs/core/langchain_core/runnables/base.py
序列执行libs/core/langchain_core/runnables/base.pyRunnableSequence
函数→Runnablelibs/core/langchain_core/runnables/base.pyRunnableLambda(经 coerce_to_runnable)
模型抽象(也是 Runnable)libs/core/langchain_core/language_models/chat_models.pyBaseChatModel
工具抽象(也是 Runnable)libs/core/langchain_core/tools/base.pyBaseTool