LangChain 深入分析第二篇:Runnable 和 LCEL 是如何实现的

一、前言:从 "路由" 转向 "表达式"

在传统编程范式中,我们往往通过命令式结构(如 if/elsefortry/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(),你可以实时接收部分结果,实现更流畅的交互体验。
  • .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!

相当于:

  1. 先生成一个字典 {"name": "LangChain"}
  2. 传递给下一个 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="")
  • 提示:
    • 在调试多链条并行执行时非常有用
    • tagsrun_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 流式链条。

相关推荐
缘友一世1 天前
LangGraph智能体(天气和新闻助手)开发与部署
语言模型·langchain·大模型·llm·langgraph
巴厘猫2 天前
Java开发者新机遇:LangChain4j——在Java中构建LLM应用的利器
java·后端·langchain
大志说编程2 天前
LangChain框架入门02:开发环境配置
langchain
AI大模型2 天前
LangChain框架入门01:LangChain是什么?
langchain·llm·agent
王国强20092 天前
LangChain 架构总览:现代 AI 应用的基石
langchain
sky丶Mamba2 天前
LangChain和LangGraph 里面的 `create_react_agent`有什么不同
langchain·agent·langgraph
老周聊大模型3 天前
LangChain替代框架深度横评:轻量化、企业级、垂直专精的技术博弈
langchain·llm·agent
Python测试之道3 天前
用LangGraph实现聊天机器人记忆功能的深度解析
人工智能·langchain·prompt
都叫我大帅哥3 天前
我给大模型装上“记忆黄金券”:LangChain的ConversationSummaryBufferMemory全解析
python·langchain·ai编程