22_Runnable接口源码拆解_LCEL管道语法背后_invoke_stream_batch究竟做了什么

概述

前面第 5 篇我们讲过 LCEL:

python 复制代码
chain = prompt | model | parser
result = chain.invoke({"topic": "Runnable"})

这行代码看起来很轻,但背后压着 LangChain 最核心的一层设计:Runnable

如果只从使用者视角看,它像是一套链式调用 API。

但从源码视角看,Runnable 至少解决了五个问题:

  • 不同组件如何拥有统一调用接口?
  • | 管道语法如何把多个组件组合成一个新组件?
  • 为什么组合后的 chain 仍然可以继续 invoke()stream()batch()
  • 普通 Python 函数为什么可以接进 LCEL?
  • tracing、metadata、并发控制、异常处理这些运行时信息如何贯穿整个链路?

本文会围绕 langchain-core 的 Runnable 源码展开,重点看:

  • Runnable 抽象类的职责。
  • __or__ 运算符重载如何生成 RunnableSequence
  • invoke()stream()batch() 的执行差异。
  • RunnableSequence 如何把多个步骤串起来。
  • RunnableLambda 如何把普通函数包装成 Runnable。
  • RunnableConfig 如何把 callbacks、tags、metadata、max_concurrency 传下去。

LCEL 的本质不是"用竖线少写几行代码",而是用 Runnable 把 LLM 应用中的组件统一成可组合、可批处理、可流式、可追踪的执行单元。

源码入口:先找到 Runnable 在哪里

读这篇源码时,先记住几个路径。

以 LangChain 官方仓库为准,核心代码主要在:

text 复制代码
libs/core/langchain_core/runnables/base.py

这个文件里包含几类关键对象:

对象 作用
Runnable 所有可执行组件的抽象基类
RunnableSerializable 支持序列化和配置化的 Runnable 基类
RunnableSequence 顺序组合多个 Runnable
RunnableParallel 并行执行多个 Runnable
RunnableLambda 把普通函数包装成 Runnable
RunnableGenerator 包装生成器函数,适合流式转换
coerce_to_runnable() 把函数、dict、Runnable-like 对象转换成 Runnable

相关辅助逻辑主要在:

text 复制代码
libs/core/langchain_core/runnables/config.py
libs/core/langchain_core/runnables/utils.py
libs/core/langchain_core/runnables/graph.py

其中:

  • config.py: 负责 RunnableConfig、callback manager、executor、config 合并。
  • utils.py: 负责函数签名判断、schema 推断、并发工具等。
  • graph.py: 负责把 Runnable 链路转换成图结构,供可视化、调试和 tracing 使用。

源码阅读顺序建议是:

text 复制代码
Runnable
  -> __or__ / pipe
  -> coerce_to_runnable
  -> RunnableSequence
  -> RunnableLambda
  -> batch / stream / transform
  -> RunnableConfig

不要一上来就读 RunnableSequence.batch(),它里面有 callback、异常、并发、config 分发,会显得很绕。

先读 Runnable 的公共协议,再读组合类和包装类,最后读 config、callback、streaming 这些横切逻辑。

Runnable 的心智模型:一个可执行、可组合、可观测的工作单元

先看一个最小 Runnable:

python 复制代码
from langchain_core.runnables import RunnableLambda


def add_one(x: int) -> int:
    return x + 1


r = RunnableLambda(add_one)

print(r.invoke(1))        # 2
print(r.batch([1, 2, 3])) # [2, 3, 4]

从源码角度看,Runnable 不是"链"的意思,而是"可执行单元"。

这个可执行单元至少有三层能力:

能力 方法 含义
单次调用 invoke() / ainvoke() 一个输入变一个输出
批量调用 batch() / abatch() 多个输入变多个输出
流式调用 stream() / astream() 一个输入逐步产出多个 chunk
事件流 astream_events() 观察中间步骤和运行事件
日志流 astream_log() 观察运行日志和 patch
组合 __or__() / pipe() 拼成新的 Runnable
配置 with_config() / bind() 注入配置或绑定参数
增强 with_retry() / with_fallbacks() 增加重试和降级
工具化 as_tool() 转成 tool

这就是为什么 promptmodelparser 看起来类型完全不同,却都能被 | 拼起来。

它们都遵守一个最小协议:

text 复制代码
Input -> Runnable -> Output

一旦遵守这个协议,就可以进入 LCEL 组合系统。

Runnable 抽象类:真正必须实现的是 invoke

从源码结构看,Runnable 是一个泛型抽象类:

python 复制代码
Runnable[Input, Output]

你可以把它理解成:

text 复制代码
接收 Input 类型
输出 Output 类型

它的核心抽象方法是 invoke()

简化理解如下:

python 复制代码
class Runnable(Generic[Input, Output]):
    def invoke(self, input: Input, config: RunnableConfig | None = None, **kwargs) -> Output:
        ...

源码里的真实实现会更复杂,因为它要处理:

  • 类型推断。
  • Pydantic schema。
  • config schema。
  • callback manager。
  • tracing。
  • sync / async 适配。
  • streaming。
  • batch 并发。
  • 序列化。

但从阅读入口看,先抓住一个重点:

对一个最小 Runnable 来说,invoke() 是必须回答的问题:给我一个输入,我如何产出一个输出?

其他能力很多都有默认实现。

例如:

  • 默认 ainvoke() 可以把同步 invoke() 放进 executor 里跑。
  • 默认 batch() 可以对多个输入并发调用 invoke()
  • 默认 stream() 可以退化成只产出一次 invoke() 的结果。

这套默认实现非常关键。

它意味着:只要一个组件实现了最基础的 invoke(),就立刻获得了异步、批处理、流式协议的基本形态。

invoke() 是 Runnable 的最小执行语义,ainvoke()batch()stream() 则是在这个语义上的通用运行模式。

__or__:竖线运算符如何生成 RunnableSequence

我们最熟悉的写法是:

python 复制代码
chain = prompt | model | parser

Python 里 | 是运算符。LangChain 让它能用于 Runnable,是因为 Runnable 重载了 __or__()

源码可以简化成这样的逻辑:

python 复制代码
def __or__(self, other):
    return RunnableSequence(
        self,
        coerce_to_runnable(other),
    )

也就是说:

python 复制代码
prompt | model

会变成:

python 复制代码
RunnableSequence(prompt, model)

再继续:

python 复制代码
prompt | model | parser

可以理解成:

python 复制代码
RunnableSequence(
    RunnableSequence(prompt, model),
    parser,
)

实际源码会对嵌套 sequence 做整理,保证最终步骤列表更平整,但心智模型就是这样。

__ror__:为什么 dict 也能放到管道里?

你可能见过这种写法:

python 复制代码
chain = {
    "question": RunnablePassthrough(),
    "context": retriever,
} | prompt | model

这里左边是一个 dict,它本身不是 Runnable。

能工作是因为 Runnable 还实现了反向运算符 __ror__()

当 Python 发现左操作数处理不了 |,就会尝试右操作数的 __ror__()

简化理解:

python 复制代码
def __ror__(self, other):
    return RunnableSequence(
        coerce_to_runnable(other),
        self,
    )

所以:

python 复制代码
some_dict | prompt

会先把 some_dict 转成 Runnable,再和 prompt 组成 RunnableSequence

coerce_to_runnable():Runnable-like 对象的入口

coerce_to_runnable() 是 LCEL 很重要的"入口转换器"。

它负责把这些对象转换成真正的 Runnable:

传入对象 转换结果
已经是 Runnable 原样返回
普通函数 RunnableLambda
生成器函数 RunnableGenerator
dict RunnableParallel
不支持的对象 抛出类型错误

这就是为什么 LCEL 里可以混用:

python 复制代码
from langchain_core.runnables import RunnableLambda

chain = (
    RunnableLambda(lambda x: x + 1)
    | (lambda x: x * 2)
    | {"value": lambda x: x, "text": str}
)

上面写法中,后两个对象不是显式 Runnable,但都会被转换。

| 的核心不是"把两个对象连起来",而是"先把右侧对象规范化成 Runnable,再生成一个新的 RunnableSequence"。

RunnableSequence:链式组合的内部结构

RunnableSequence 是 LangChain 里最重要的组合类。

它代表:

text 复制代码
step1 -> step2 -> step3 -> ... -> stepN

每一步都是 Runnable。

例如:

python 复制代码
chain = prompt | model | parser

可以理解成:

text 复制代码
RunnableSequence
  first: prompt
  middle: [model]
  last: parser

或者更直观地理解成:

text 复制代码
steps = [prompt, model, parser]

invoke():顺序执行每一步

RunnableSequence.invoke() 的核心逻辑非常直接。

伪代码如下:

python 复制代码
def invoke(input, config=None):
    value = input

    for step in steps:
        value = step.invoke(value, child_config)

    return value

所以这条链:

python 复制代码
chain = prompt | model | parser

执行时就是:

text 复制代码
原始输入 dict
  |
  v
prompt.invoke(...)
  |
  v
PromptValue / messages
  |
  v
model.invoke(...)
  |
  v
AIMessage
  |
  v
parser.invoke(...)
  |
  v
字符串 / 结构化对象

你可以把 RunnableSequence 当成一个"for 循环封装器",但它比普通 for 循环多做了很多事情:

  • 给每个 step 分配子 callback。
  • 合并和传递 config。
  • 维护 run name。
  • 捕获错误并上报 tracing。
  • 让整个 sequence 仍然是一个 Runnable。
  • 让 sequence 自动继承 batch()stream()ainvoke() 等能力。

这就是 LCEL 的价值:组合之后仍然符合同一个协议。

子 run:为什么 trace 里有 seq:step:1

读源码时会看到类似这样的概念:

text 复制代码
seq:step:1
seq:step:2
seq:step:3

这不是业务逻辑,而是 tracing 逻辑。

一个 RunnableSequence 本身是一个 root run,它里面的每个 step 都是 child run。

结构类似:

text 复制代码
RunnableSequence run
  |
  |-- seq:step:1 prompt
  |-- seq:step:2 model
  |-- seq:step:3 parser

这也是为什么 LangSmith 或 ConsoleCallbackHandler 可以看到中间步骤。

如果没有这层 callback 分发,chain.invoke() 在外部看起来就只是一个黑盒。

RunnableSequence.invoke() 的本质是顺序调用每个 step,但源码里额外处理了 config、callback、trace、异常和子步骤命名。

stream():流式输出不是所有步骤都天然支持

很多人以为只要调用:

python 复制代码
for chunk in chain.stream(input):
    print(chunk)

就一定会从第一步开始一路流式输出。

源码层面并不是这么简单。

stream() 背后更核心的方法是 transform()

可以这样理解:

text 复制代码
stream(input)
  -> 把单个 input 包装成 iterator
  -> 调用 transform(iterator)
  -> 逐个 yield output chunk

RunnableSequence 来说,流式数据会沿着每个 step 的 transform() 往后传:

text 复制代码
input iterator
  -> step1.transform(...)
  -> step2.transform(...)
  -> step3.transform(...)
  -> output iterator

关键点在这里:

如果中间某个 step 不支持真正的流式 transform,它可能会先把输入缓存起来,等输入完整后再产出结果。

所以流式链路是否真的"边生成边输出",取决于链上每一步是否支持流式。

例如:

python 复制代码
chain = prompt | streaming_model | parser

如果:

  • prompt 很快产出完整 messages。
  • streaming_model 支持 token/chunk 流式输出。
  • parser 支持逐 chunk 解析。

那么整个链路就能比较自然地流式输出。

但如果中间插入一个普通 RunnableLambda

python 复制代码
chain = prompt | model | RunnableLambda(lambda x: expensive_parse(x))

它可能需要等模型输出完整后才执行解析,这会打断前面的流式体验。

RunnableLambda 的流式局限

普通函数包装成 RunnableLambda 时,通常是:

text 复制代码
收完整输入 -> 调函数 -> 产一个输出

它并不天然适合逐 chunk 处理。

如果你需要真正的流式转换,更应该考虑生成器函数或专门实现支持 transform() 的 Runnable。

stream() 是协议层能力,但真实流式效果取决于链上每个步骤是否支持 transform(),普通同步函数经常会让流式链路变成"先缓存、后输出"。

batch():默认并发调用 invoke,而不是神秘批处理

batch() 的使用方式很简单:

python 复制代码
outputs = chain.batch([
    {"topic": "Runnable"},
    {"topic": "RunnableSequence"},
    {"topic": "RunnableLambda"},
])

很多人会误以为 batch() 一定会调用模型供应商的批量 API。

源码默认逻辑不是这样。

官方 reference 和源码都表明:默认 batch() 会并发执行多次 invoke(),适合 IO 密集型 Runnable。

简化伪代码:

python 复制代码
def batch(inputs, config=None, return_exceptions=False):
    configs = get_config_list(config, len(inputs))

    def one(input, config):
        return self.invoke(input, config)

    return executor.map(one, inputs, configs)

它的关键点有三个:

  1. 一个 input 对应一个 config。
  2. 多个 input 默认并发执行。
  3. 如果底层 API 有真正批量接口,子类应该重写 batch()

比如对普通 HTTP 调用来说,默认并发已经有价值。

但如果某个 embeddings provider 提供了原生 batch endpoint,那么最佳实现应该直接覆盖 batch(),一次请求处理多个文本,而不是并发发很多小请求。

max_concurrency:并发不是无限开

RunnableConfig 里有一个关键字段:

python 复制代码
config = {
    "max_concurrency": 5,
}

它用于控制并发数量。

例如:

python 复制代码
results = chain.batch(
    [{"topic": t} for t in topics],
    config={"max_concurrency": 5},
)

这在调用外部模型、搜索 API、数据库、爬虫工具时很重要。

否则你很容易遇到:

  • provider rate limit。
  • 数据库连接池耗尽。
  • 本地 CPU 线程过多。
  • 请求排队导致延迟抖动。

return_exceptions:批处理中如何处理单条失败

batch() 还有一个常见参数:

python 复制代码
return_exceptions=True

如果它是 False,某个输入失败通常会抛出异常。

如果它是 True,失败项会作为异常对象放回结果列表中。

例如:

python 复制代码
outputs = chain.batch(inputs, return_exceptions=True)

for item in outputs:
    if isinstance(item, Exception):
        print("failed:", item)
    else:
        print("ok:", item)

这在批量处理文档、批量抽取结构化信息、批量生成摘要时很实用。

默认 batch() 不是供应商原生批量 API,而是并发执行多个 invoke();真正高效的批量接口需要具体 Runnable 自己重写。

RunnableSequence.batch:批处理链路如何逐步推进

单个 Runnable 的 batch() 是并发执行多个 invoke()

RunnableSequencebatch() 更有意思。

它不是简单地对每个输入完整执行一遍全链路,而是按 step 推进:

text 复制代码
inputs = [a, b, c]

step1.batch(inputs)
  -> [a1, b1, c1]

step2.batch([a1, b1, c1])
  -> [a2, b2, c2]

step3.batch([a2, b2, c2])
  -> [a3, b3, c3]

这样做有两个好处:

  1. 每个 step 都有机会使用自己的批处理优化。
  2. callback 和 tracing 可以按步骤组织。

例如:

python 复制代码
chain = prompt | model | parser

执行 chain.batch(inputs) 时,大致是:

text 复制代码
prompt.batch(inputs)
  -> messages_list

model.batch(messages_list)
  -> ai_messages

parser.batch(ai_messages)
  -> parsed_outputs

如果 model.batch() 有供应商侧优化,它就可以在这一层发挥作用。

如果没有,它也会退回到并发 model.invoke()

批处理中的异常传播

return_exceptions=True 时,RunnableSequence.batch() 还需要处理一个问题:

某个输入在 step1 已经失败了,还要不要进入 step2?

合理做法是:失败项不再进入后续 step,但最终结果列表仍然保持输入顺序。

所以源码里会维护失败输入的位置,再把成功结果和异常结果重新组装回原来的顺序。

这也是为什么 RunnableSequence.batch() 的源码比单个 Runnable 的 batch() 更长。

它不仅要"批量执行",还要保证:

  • 顺序不乱。
  • 异常位置不丢。
  • 成功项继续往后跑。
  • 每个 step 都有自己的 child callback。

RunnableSequence.batch() 是按步骤批量推进,而不是简单地把整条链复制 N 份并发跑。

RunnableLambda:普通函数如何变成 Runnable

RunnableLambda 是 LCEL 非常实用的一层适配器。

它让普通 Python 函数可以进入 Runnable 世界:

python 复制代码
from langchain_core.runnables import RunnableLambda


def normalize_topic(topic: str) -> dict:
    return {"topic": topic.strip()}


chain = RunnableLambda(normalize_topic) | prompt | model | parser

源码上,它主要做几件事:

  • 保存原始函数。
  • 判断函数是同步还是异步。
  • 推断输入输出类型和 schema。
  • 调用时把 inputconfigrun_manager 等按函数签名注入。
  • 如果函数返回的还是 Runnable,则继续执行这个 Runnable。
  • 如果函数是 generator,则支持按 chunk 产出。

函数签名注入:为什么有些函数能接收 config

你可以写:

python 复制代码
from langchain_core.runnables import RunnableConfig, RunnableLambda


def add_trace_prefix(x: str, config: RunnableConfig) -> str:
    run_name = config.get("run_name", "unnamed")
    return f"[{run_name}] {x}"


r = RunnableLambda(add_trace_prefix)

print(r.invoke("hello", config={"run_name": "demo"}))

不是所有函数都必须接收 config

RunnableLambda 会根据函数签名判断是否传入这些额外参数。

常见可注入参数包括:

  • config
  • run_manager

这让普通函数在需要时可以参与 tracing、读取配置、派生 callback。

返回 Runnable:动态路由的基础

RunnableLambda 还有一个很重要的能力:如果函数返回一个 Runnable,它会继续调用返回的 Runnable。

这可以用来做动态路由:

python 复制代码
from langchain_core.runnables import RunnableLambda


def route(question: str):
    if "SQL" in question:
        return sql_chain
    return rag_chain


router = RunnableLambda(route)

result = router.invoke("请用 SQL 查询订单数量")

心智模型是:

text 复制代码
route(input) -> 返回某条 chain -> 执行这条 chain

源码里会用 recursion_limit 防止无限返回 Runnable 导致递归失控。

RunnableLambda 是普通 Python 世界和 Runnable 世界之间的桥,它让函数、动态路由、小型转换逻辑都能进入 LCEL。*

RunnableParallel:dict 为什么代表并行分支

虽然第这篇重点是 RunnableSequence,但理解 dict 自动变成 RunnableParallel 很重要。

比如 RAG 里常见写法:

python 复制代码
from operator import itemgetter
from langchain_core.runnables import RunnablePassthrough

chain = {
    "context": itemgetter("question") | retriever,
    "question": itemgetter("question"),
} | prompt | model | parser

左侧 dict 会被 coerce_to_runnable() 转成 RunnableParallel

它的语义是:

text 复制代码
同一份输入
  |
  +-- context 分支
  |
  +-- question 分支
  |
  v
合并成 dict 输出

也就是:

text 复制代码
input
  -> {
       "context": context_runnable.invoke(input),
       "question": question_runnable.invoke(input),
     }

这解释了为什么 LCEL 里经常用 dict 做字段组装。

它不是普通字典赋值,而是并行执行分支,并把多个分支结果合成一个字典。

在 LCEL 里,dict 通常不是静态数据,而是会被转换成 RunnableParallel 的并行分支声明。

RunnableConfig:为什么 config 能一路传到底

RunnableConfig 是理解生产级 LCEL 的关键。

它不是业务输入,而是运行时配置。

常见字段包括:

字段 作用
tags 给 run 打标签,方便筛选 trace
metadata 附加运行元数据
callbacks 注入 callback handler
run_name 指定当前 run 名称
max_concurrency 控制 batch 并发数量
recursion_limit 控制 Runnable 动态递归深度
configurable 给可配置字段传值
run_id 指定运行 ID

调用时可以这样传:

python 复制代码
result = chain.invoke(
    {"topic": "Runnable"},
    config={
        "run_name": "runnable_article_demo",
        "tags": ["csdn", "source-reading"],
        "metadata": {"article": 22},
    },
)

RunnableSequence 内部,config 不会简单原样塞给每一步。

它会被 patch、merge、派生 child callbacks。

大致结构是:

text 复制代码
root config
  |
  +-- step1 child config
  |
  +-- step2 child config
  |
  +-- step3 child config

这使得 trace 可以形成树:

text 复制代码
chain run
  |
  |-- prompt run
  |-- model run
  |-- parser run

如果你以后读 create_agent()、middleware、ToolNode、LangSmith tracing,都会看到这套 config/callback 机制。

业务数据走 input,运行控制走 config;Runnable 源码大量复杂性都来自 config、callback 和 tracing 的传递。

用一个例子串起 invoke、stream、batch

下面用一个不依赖模型的例子,把三种调用方式串起来。

python 复制代码
from langchain_core.runnables import RunnableLambda


def clean_text(text: str) -> str:
    return text.strip()


def to_words(text: str) -> list[str]:
    return text.split()


def count_words(words: list[str]) -> int:
    return len(words)


chain = (
    RunnableLambda(clean_text)
    | RunnableLambda(to_words)
    | RunnableLambda(count_words)
)

print(chain.invoke("  hello langchain runnable  "))

print(chain.batch([
    "hello langchain",
    "runnable sequence",
    "invoke stream batch",
]))

执行 invoke()

text 复制代码
"  hello langchain runnable  "
  -> clean_text
  -> "hello langchain runnable"
  -> to_words
  -> ["hello", "langchain", "runnable"]
  -> count_words
  -> 3

执行 batch()

text 复制代码
step1.batch(["hello langchain", "runnable sequence", ...])
  -> cleaned_texts

step2.batch(cleaned_texts)
  -> word_lists

step3.batch(word_lists)
  -> counts

执行 stream() 时,由于这里都是普通函数,实际不会有模型 token 那样的细粒度流式效果。通常会等某一步拿到完整输入后再产出。

如果想观察 stream() 更明显的效果,需要使用支持流式的模型或 generator Runnable。

源码阅读时要抓的关键问题

base.py 时,不建议逐行读完。

更高效的方式是带着问题读:

问题一:这个类到底是不是 Runnable?

判断标准:

text 复制代码
能不能 invoke?
能不能 batch?
能不能 stream?
能不能被 | 组合?

问题二:这个对象是原生 Runnable,还是被转换出来的?

例如:

  • ChatPromptTemplate 通常本身就是 Runnable。
  • ChatModel 通常本身就是 Runnable。
  • 普通函数会被转成 RunnableLambda
  • dict 会被转成 RunnableParallel

问题三:这个链路的输入输出类型是什么?

源码中有 InputTypeOutputTypeinput_schemaoutput_schema

这些信息不仅用于文档,也用于校验、可视化、工具化、部署。

问题四:流式输出是在哪一步断掉的?

如果你发现:

python 复制代码
chain.stream(input)

没有逐 token 输出,优先检查:

  • 模型是否开启 streaming。
  • 中间 parser 是否支持流式。
  • 是否插入了普通 RunnableLambda
  • 是否某一步只实现了 invoke(),没有真正实现 transform()

问题五:批处理为什么没有变快?

优先检查:

  • 是否被 max_concurrency 限制。
  • 是否底层 provider 本身限流。
  • 是否某个 step 是 CPU 密集型。
  • 是否具体 Runnable 没有重写真正的原生 batch。
  • 是否 callback/tracing 开销过大。

常见误区

最后整理几个常见误区。

误区一:Runnable 等于 Chain

不准确。

Runnable 是统一执行接口,RunnableSequence 才是顺序链。

也就是说:

text 复制代码
Runnable 是协议
RunnableSequence 是一种组合实现

误区二:| 只是 Python 语法糖

不准确。

| 会触发 __or__(),把右侧对象转换成 Runnable,并返回新的 RunnableSequence

它改变了对象结构,不只是少写几行代码。

误区三:batch() 一定等于供应商批量 API

不准确。

默认 batch() 是并发调用 invoke()

只有具体 Runnable 重写了 batch(),才可能使用供应商原生批量能力。

误区四:stream() 一定逐 token 输出

不准确。

stream() 是协议,真实输出粒度取决于链上每个 step 是否支持流式转换。

误区五:普通函数放进 LCEL 没有成本

不准确。

普通函数很方便,但它可能:

  • 打断流式输出。
  • 隐藏类型信息。
  • 让异常位置不直观。
  • 把复杂业务逻辑塞进匿名 lambda,降低可读性。

如果函数逻辑变复杂,建议显式命名,并用 RunnableLambda 包装。

总结

本文从源码视角拆了 Runnable 的核心设计。

你需要记住这几条主线:

  1. Runnable 是统一执行协议,核心是 invoke(input, config)
  2. __or__() 会把两个 Runnable-like 对象组合成 RunnableSequence
  3. coerce_to_runnable() 负责把函数、dict、generator 等对象转换成 Runnable。
  4. RunnableSequence.invoke() 是顺序执行每个 step。
  5. RunnableSequence.batch() 是按 step 批量推进,并保持结果顺序和异常位置。
  6. stream() 依赖 transform(),真实流式效果取决于每个 step 是否支持流式。
  7. RunnableLambda 让普通 Python 函数进入 LCEL,但普通函数不天然等于高质量流式组件。
  8. RunnableConfig 负责 tags、metadata、callbacks、run_name、max_concurrency 等运行时控制。

最后给一张源码阅读地图:

text 复制代码
Runnable 抽象
  |
  |-- invoke / ainvoke
  |-- batch / abatch
  |-- stream / astream
  |-- transform / atransform
  |
  +-- __or__ / __ror__ / pipe
          |
          v
      RunnableSequence
          |
          +-- step1.invoke
          +-- step2.invoke
          +-- step3.invoke

coerce_to_runnable
  |
  |-- function -> RunnableLambda
  |-- generator -> RunnableGenerator
  |-- dict -> RunnableParallel
  |-- Runnable -> 原样返回

RunnableConfig
  |
  |-- callbacks
  |-- tags / metadata
  |-- run_name
  |-- max_concurrency
  |-- recursion_limit

看懂 Runnable,就看懂了 LangChain 如何把 Prompt、Model、Parser、Retriever、Tool 和普通函数统一成一套可组合的执行系统。

相关推荐
大气的小蜜蜂1 小时前
基于Python+Django的健身房管理系统实现:核心亮点全流程解析
开发语言·python·django
赵民勇2 小时前
Python 协程详解与技巧总结
python
极光代码工作室2 小时前
基于YOLO目标检测的智能监控系统
python·深度学习·yolo·机器学习·计算机视觉
江华森3 小时前
Python 进阶编程实战 — 从多版本环境到百万级登录系统
python
C+-C资深大佬3 小时前
python while循环
服务器·开发语言·python
zh路西法4 小时前
【现代控制理论与卡尔曼滤波】从状态空间到Python仿真实现
开发语言·python
Vodka~5 小时前
WSL2 + RViz GPU渲染机械臂
人工智能·python
8Qi85 小时前
hello-agents学习笔记--Memory让Agent拥有记忆
人工智能·python·llm·agent·ai编程·vibecoding
Esaka_Forever5 小时前
Python 完整内存管理机制详解
开发语言·python·spring