一、前言:从 "路由" 转向 "表达式"
在传统编程范式中,我们往往通过命令式结构(如 if/else
、for
、try/except
)明确编排程序的执行路径,就像手动设计一张流程图。
LangChain 通过 LCEL(LangChain 表达式语言)将这类流程控制抽象为"可组合的执行表达式 "。每个模块都被封装为 Runnable
,多个模块之间通过链式语法自然组合,系统根据数据流自动推导执行顺序。
这不仅仅是语法层面的变化,更代表着一种编程思维的演进:从"命令式控制"转向"表达式组合",从"显式逻辑"转向"模块化构建"。
二、核心概念:Runnable 是什么?
Runnable
是 LangChain 架构中的核心抽象单元,是 LCEL 表达式语言的最小执行模块和构建基石。
任何可执行的单元都是 Runnable
核心基类
python
class Runnable(ABC, Generic[Input, Output]):
"""A unit of work that can be invoked, batched, streamed, transformed and composed.
...
"""
@abstractmethod
def invoke(
self,
input: Input,
config: Optional[RunnableConfig] = None,
**kwargs: Any,
) -> Output:
"""Transform a single input into an output.
...
"""
.invoke(input)
是 LangChain 中 Runnable 接口的核心同步调用方法,用于将输入传入模块,执行并返回结果。适用于单次执行、测试链路或组合多个模块后的最终触发。
特点:
- 同步执行:立即返回处理结果
- 通用入口:适用于任何 Runnable 对象(LLM、链、工具等)
- 可组合:支持与
.with_retry()
、.transform()
等方法链式使用
LangChain 将你所知道的:
- LLM
- PromptTemplate
- ChatHistory
- Tool
- AgentExecutor
通通都抽象为 Runnable
,不同类型只是指定了不同的输入/输出数据类型。
其他方法
.stream()
将普通的执行过程变成流式执行,允许逐步产出结果,而不是等待全部完成后一次性返回。- 在调用大型语言模型(LLM)或其它需要长时间运行的任务时,输出结果往往是增量产生的。使用
.stream()
,你可以实时接收部分结果,实现更流畅的交互体验。
- 在调用大型语言模型(LLM)或其它需要长时间运行的任务时,输出结果往往是增量产生的。使用
.batch()
支持一次性处理一批输入,返回一批对应输出,提高整体吞吐量。.transform()
为输出添加一个转换器(函数),对结果进行进一步加工或格式化。.with_fallbacks()
设置备用执行路径,当主任务失败时自动切换执行备用任务。.with_retry()
为当前任务添加自动重试功能,遇到异常自动按重试策略重试。
方法 | 作用说明 | 典型应用场景 |
---|---|---|
.stream() |
实时流式输出,边生成边返回 | 聊天机器人、文本生成 |
.batch() |
批量处理输入,提高吞吐量 | 批量分类、批量生成 |
.transform() |
对输出结果进行后续转换处理 | 格式化、过滤、标注 |
.with_fallbacks() |
多路径备选执行,容错降级 | 多模型容灾、工具降级 |
.with_retry() |
失败自动重试,保证稳定性 | 网络异常、API 失败重试 |
三、LCEL 组合式表达式:从几个 Runnable 到一条链
LCEL 是 "LangChain Expression Language" 的缩写,目标是用最少的输入以最光明的方式构造最处理的连接流程。
示例:
python
from langchain_core.runnables import RunnableMap, RunnableLambda
chain = RunnableMap({"name": lambda _: "LangChain"}) \
| RunnableLambda(lambda d: f"Hello, {d['name']}!")
print(chain.invoke({}))
# Hello, LangChain!
相当于:
- 先生成一个字典
{"name": "LangChain"}
- 传递给下一个
lambda
,输出 "Hello, LangChain"
后续可以这样扩展:
python
| some_chat_model \
| some_output_parser \
| final_formatter
四、实现解析:执行链条与数据流通道
LangChain 的 Runnable 原理根基于一套"输入 - 运行 - 输出"的基本通道模型:
通过 invoke()
启动
python
def invoke(self, input: Input) -> Output:
return self._call_with_config(input, config)
配合 Configurable 和 RunnableBinding
它们会为 chain 中的每个 Runnable 自动分配唯一的 ID,并附加配置(config),以便于后续的日志记录、异常追踪以及 fallback 分支的排查与切换。
支持 RunnableSequence 系列化
- 用
|
连接 - 内部编排
_invoke
,_batch
,_stream
总结核心意图
LangChain 在执行层的设计并不仅仅是为了"串起来能用",而是希望通过 Runnable + LCEL 表达式系统,构建一个可观测、可控制、易扩展、可组合、可替换的 AI 应用执行基座。
下图是典型执行链条的简化视图,展示了 LangChain 中各个模块的基本作用和周边能力增强点:
css
[Prompt] -> [LLM] -> [Parser] -> [PostProcess]
\ | | |
Config Retry OutputMap LangSmith Trace
- 每个模块之间通过 Runnable 接口无缝衔接
- 任意节点都可以挂接 Retry / Config / Debug / Tracing 能力
- 整条链条既是执行流程,也是组合表达式(LCEL)
这种结构让 LangChain 能像搭积木一样构建复杂任务,又能像监控系统一样追踪每个模块的执行情况。
五、扩展方案:如何实现自定义 Runnable
所有 Runnable 只需要继承 Runnable
,重写 _invoke
:
示例:实现一个简单 Tool
python
from langchain_core.runnables import Runnable
class AddOne(Runnable[int, int]):
def invoke(self, input: int, config=None) -> int:
return input + 1
AddOne
继承了Runnable[int, int]
,表示这个类输入是int
,输出也是int
。
这个简单的
AddOne
类展示了 LangChain 架构的核心理念:将所有可执行逻辑抽象为统一接口Runnable
,使其天然具备组合性、控制性和可观测性。通过继承
Runnable
,即便是最简单的 Python 函数,也能被纳入 LCEL 表达式系统,与 LLM、Prompt、Parser 等模块无缝集成,构建出结构清晰、逻辑强大、易于维护的 AI 应用流程。
与其他 Runnable 组合
python
from langchain_core.runnables import RunnableLambda
add = AddOne()
square = RunnableLambda(lambda x: x * x)
workflow = add | square
workflow.invoke(2) # 输出:9
六、相关扩展功能
功能说明和代码示例
with_retry(RetryConfig)
:自动重试机制,增强鲁棒性- 缓解 LLM 调用时的 timeout、RateLimitError 或网络故障问题。
- 就像给函数加上
try...except...retry
,但更优雅、自动、声明式。
python
from langchain_core.runnables.retry import RetryHandler
retryable = llm.with_retry(RetryHandler(max_attempts=3))
response = retryable.invoke("你好")
with_fallbacks([B, C])
:备用路径,失败时自动切换- 当主要模块(如 LLM A)不可用时,自动尝试备用方案(如 B 和 C)。
- 像
try A except → try B → try C
的自动化策略,非常适合生产场景。
python
robust_chain = llm.with_fallbacks([openai_llm, local_llm])
with_config(tags=[...], run_name=...)
:执行元信息,便于可观测性- 为每次执行添加追踪信息,方便在 LangSmith 等调试平台中记录来源与上下文。
- 相当于"打标签 + 命名",你在 LangSmith 中可以看到漂亮的执行轨迹。
python
configured = chain.with_config(tags=["query", "qa"], run_name="qa_main_chain")
RunnableParallel
:并行执行多个任务,提高吞吐率- 一次性运行多个
Runnable
,并收集所有结果,适合处理多个输入源或分任务处理。 - 输出是一个字典:
{"qa": ..., "summary": ...}
,并行执行效率更高。
- 一次性运行多个
python
from langchain_core.runnables import RunnableParallel
parallel_chain = RunnableParallel({
"qa": qa_chain,
"summary": summary_chain,
})
result = parallel_chain.invoke({"input": "今天的新闻..."})
RunnableBranch
:条件分支逻辑,像if-else
一样灵活- 根据输入内容的特征动态选择执行路径。
- 把控制流
if/elif/else
模块化、声明式地挂接到链路中。
python
from langchain_core.runnables import RunnableBranch
branch = RunnableBranch(
(lambda x: "help" in x.lower(), help_chain),
(lambda x: "buy" in x.lower(), shopping_chain),
default_chain
)
扩展功能与 .stream()
/ .batch()
联合使用
LangChain 中的 Runnable
拥有统一的三种执行入口:
.invoke()
:同步执行单次输入.batch()
:同步执行一批输入.stream()
:逐步返回输出(生成式任务常用)
下面我们按扩展能力逐一说明如何结合使用:
✅ 1. with_retry()
+ .batch()
/ .stream()
-
作用:为一批请求或流式数据执行增加自动重试机制,增强稳健性。
-
示例:
python
retry_llm = llm.with_retry()
results = retry_llm.batch(["你好", "请翻译", "帮我写一封邮件"])
python
for chunk in llm.with_retry().stream("请写一段诗"):
print(chunk, end="")
- 提示:
RetryHandler
会针对每个失败样本重试,不会重跑整个 batch。- 如果用于
.stream()
,只会在流开始失败时重试,流中间错误不会回滚。
✅ 2. with_fallbacks()
+ .batch()
/ .stream()
-
作用:当主模块失败时,自动切换到备用模块处理批量请求或流式任务。
-
示例:
python
robust = llm.with_fallbacks([backup_llm])
results = robust.batch(["你是谁", "请总结这段文字"])
python
for chunk in robust.stream("用 fallback 模式生成一段回答"):
print(chunk, end="")
- 提示:
- fallback 是按整个输入粒度判断是否失败,不是逐个 token fallback
- 不适合频繁局部失败但整体还有效的情况(建议配合
RetryHandler
)
✅ 3. with_config()
+ .batch()
/ .stream()
-
作用:批处理或流式调用时,添加执行标识,用于日志标注、链路追踪(LangSmith 等)。
-
示例:
python
configured = chain.with_config(tags=["批处理任务"], run_name="qa_batch")
results = configured.batch(list_of_questions)
python
for token in chain.with_config(run_name="stream_chat").stream("你好啊"):
print(token, end="")
- 提示:
- 在调试多链条并行执行时非常有用
tags
和run_name
可配合 LangSmith 实现全链路调用树分析
✅ 4. RunnableParallel
+ .batch()
/ .stream()
-
作用:并行执行多个子链,支持同时流式或批量处理多个子任务。
-
示例:
python
parallel = RunnableParallel({
"translate": translator,
"summary": summarizer
})
results = parallel.batch([{"text": "A"}, {"text": "B"}])
输出结果结构:
python
[
{"translate": "...", "summary": "..."},
{"translate": "...", "summary": "..."}
]
目前 stream + 并行使用受限,不建议用于多路 stream 合并,需自定义包装
✅ 5. RunnableBranch
+ .batch()
/ .stream()
-
作用:批量输入中可根据条件路由到不同模块,或对每条输入动态选择处理逻辑。
-
示例:
python
branch = RunnableBranch(
(lambda x: "翻译" in x, translator),
(lambda x: "摘要" in x, summarizer),
default_chain
)
results = branch.batch(["请翻译", "请摘要", "默认处理"])
每条输入匹配一个路径,独立处理
七、总结思维
Runnable 和 LCEL 不是一套 API,而是一套 执行逻辑架构模型。
它为 LLM 应用提供了一套通用型执行单元,并能够被分层、并行、切换、监控和应急处理,是 LangChain 搭建模块化调度器的基础。
接下来我们将手工实现一个完整的 Runnable 模块,并构建属于自己的 LangChain 流式链条。