# 15 - LCEL 与链式调用 --- **本章课程目标:** - 理解 **Runnable** 的定位,知道 LangChain 为什么要把 Prompt、Model、Parser、Tool、Chain 都抽象成统一的“可执行组件”。 - 理解 **LCEL**(LangChain Expression Language,LangChain 表达式语言)到底解决了什么问题,掌握 `|` 管道符背后的链式组合思想。 - 能读懂并运行本章全部案例:**顺序链、分支链、多步串行链、并行链、函数链**,建立后续学习 [记忆与对话历史](16-记忆与对话历史(含Redis基础).md)、[Tools 工具调用](17-Tools工具调用.md)、[RAG](19-RAG检索增强生成.md)、[Agent](21-Agent智能体.md) 的基础。 **学习建议:** 先亲手跑通 `prompt | model | parser`,再谈分支、并行和自定义函数节点。读代码时盯住两件事:每个 Runnable 收什么、吐什么;组合之后还能不能继续被 `invoke`、`stream`、`batch` 调用。这样看 LCEL,就不会只把它当成一个管道符语法。 --- ## 1、Runnable 与统一调用方式 ### 1.1 前置知识点:抽象基类(ABC) 如果你第一次接触 **抽象基类**,不用把它想得太复杂。对这一章来说,你只需要先建立一个足够实用的理解: **抽象基类,就是先规定“这一类对象应该具备哪些共同能力”,但不急着规定“每个对象内部具体怎么做”。** 这里说的就是一种“统一规则”或“公共协议”。 比如框架设计者先约定: - 只要你属于“可执行组件” - 你就应该支持某些统一方法 - 例如单次调用、批量调用、流式调用、异步调用 这样后面无论这个对象到底是 Prompt、Model、Parser,还是整条 Chain,使用者都可以按同一种方式去理解和调用它。这就是抽象基类最有价值的地方:**先把共同规则定下来,再让不同对象去实现自己的具体行为。** 放到 LangChain 里,你可以把 **Runnable** 理解成这样一种核心抽象接口。它背后的思想就是: - Prompt 是一种可执行组件 - Model 是一种可执行组件 - Parser 是一种可执行组件 - Chain 也是一种可执行组件 既然它们都属于“可执行组件”这一大类,那么就应该尽量遵守同一套调用协议。所以这一节你不用死记 `ABC`、`abstractmethod` 这些 Python 细节,只要先抓住一句话:**抽象基类解决的是“先把共同规则定下来”,而 Runnable 正是 LangChain 用来统一这些规则的关键抽象。** ![`langchain_core.runnables.base` 中 Runnable 的类定义与职责说明:可 invoke、batch、stream 并支持组合](images/15/15-1-1-1.jpeg) > **图意说明**:上图截自 LangChain 参考文档。`Runnable` 声明为 `class Runnable(ABC, Generic[Input, Output])`,描述的是“可被调用、批量处理、流式输出、变换与组合”的工作单元;`invoke`/`ainvoke`、`batch`/`abatch`、`stream`/`astream` 等成对出现,`astream_log` 还可流式透出部分中间结果。各方法均可传入 `config`(如标签、元数据)便于追踪与排障;输入/输出/config 的结构信息可通过 `input_schema`、`output_schema`、`config_schema` 等暴露给工具链与 IDE。 ### 1.2 统一接口的意义 如果没有统一接口,Prompt、模型、解析器、工具、检索器往往会各自一套入口(例如 `format`、`generate`、`parse`、`run`、`stream` 等混用)。结果是:**组件难拼接、方法名难记、替换实现时调用处到处要改**,也很难把整条流程写成清晰的“从左到右”数据流。 LangChain 的解决思路就是:**把这些“能接收输入并产生输出”的对象,尽量抽象成统一接口。**这个统一接口,就是 **Runnable**。 ### 1.3 定义 **Runnable** 是 LangChain 中最核心的抽象之一,本质上就是一个“可执行的数据处理节点”接口。 这里要特别区分一个很容易混淆的点: - **Runnable 是什么**:它表示“这一类对象是可执行组件”,是一种统一抽象标准。 - **统一调用方式是什么**:它是 Runnable 带来的结果,也就是这些组件都可以尽量用同一套方式去调用,比如 `invoke`、`batch`、`stream`。 所以更准确的结论是,**Runnable 不是“统一调用方式”的翻译,而是“统一调用方式”背后的抽象基础。** 只要某个对象实现了 Runnable 接口,它通常就能用统一方式来调用,比如: `invoke`、`batch`、`stream`、`ainvoke`、`abatch`、`astream`。 在 LangChain 里,很多你已经学过的对象,本质上都属于 Runnable: - [Prompt 模板](13-提示词与消息模板.md)、[聊天模型](11-Model-I-O与模型接入.md)、[输出解析器](14-输出解析器.md) - 后面会学到的检索器、工具、甚至整条链 - 更进一步,编译后的 LangGraph 图本身也可以作为 Runnable 被调用 也就是说,后面之所以能把这些组件顺畅地串成一条链,并不是因为“管道符很神奇”,而是:**这些组件本身都被设计成了可组合的 Runnable。** ### 1.4 统一调用方式 这一节先解决一个问题:后面“链式组合”为什么能成立、为什么会好用。 先看你已经学过的三个典型对象: **1. Prompt 模板** - 从业务角度看,Prompt 模板最常见的方法是 `format(...)` - 但从“统一接口、便于后续组合”的角度看,它也可以用 `invoke(...)` ```python # 业务层面常见写法 prompt_str = prompt.format(topic="LangChain") # LCEL 统一接口写法 prompt_value = prompt.invoke({"topic": "LangChain"}) ``` **2. 模型** - 从业务角度看,模型最常见的是 `invoke(...)` - 它接收上一步 Prompt 的输出,再返回 `AIMessage` ```python ai_message = model.invoke(prompt_value) ``` **3. 解析器** - 从业务角度看,解析器常见有 `parse(...)` - 但在 LCEL 里也可以统一通过 `invoke(...)` 接收上一步结果 ```python result = parser.invoke(ai_message) ``` 这样一来,三步就被统一成了同一种风格: ```python prompt_value = prompt.invoke({"question": "什么是 LangChain?"}) ai_message = model.invoke(prompt_value) result = parser.invoke(ai_message) ``` 这背后最大的价值是:**组件之间更容易替换**;**流程更容易串联**;**链本身也可以继续被当作一个组件使用**;**中间结果传递方式更统一,不需要每一步都手写很多适配代码**。 这就是后面 `prompt | model | parser` 能成立的根本原因。 ### 1.5 常用方法 实现了 Runnable 接口的对象,通常都支持下面这些核心方法: | 方法 | 作用 | 适合场景 | | ------------------ | -------------------------- | -------------------------- | | `invoke(input)` | 同步处理单个输入 | 最常用,单次调用 | | `batch(inputs)` | 同步批量处理多个输入 | 批量任务、离线任务 | | `stream(input)` | 同步流式处理 | 打字机输出、长文本生成 | | `ainvoke(input)` | 异步处理单个输入 | 异步服务、高并发 | | `abatch(inputs)` | 异步批量处理 | 异步批量任务 | | `astream(input)` | 异步流式处理 | 异步聊天 UI、流式返回 | | `astream_log(...)` | 流式输出并可选带上中间步骤 | 调试链、观察多节点执行过程 | 可以把它们理解成: - `invoke` 是最基础的“执行一次” - 其他方法是在同步 / 异步、单条 / 批量、一次性 / 流式这几个维度上的扩展 所以当你看到“Prompt、Model、Parser、Chain 都支持 invoke”时,不要把它理解成“它们完全一样”,而要理解成:**它们不同,但都遵守同一套调用协议。** ### 1.6 在实际项目中的价值 这一点在小案例里不一定立刻感受到,但放到实际项目里就很常见。比如一个典型企业问答系统,可能会经历这样的演进: 1. 一开始只是 `prompt + model` 2. 后来为了稳定输出,加上 `parser` 3. 再后来要分中文和英文回答,加上 `branch` 4. 再后来要同时返回摘要与原文观点,加上 `parallel` 5. 再后来要插入自定义日志、埋点、字段映射,又加上 `lambda` 如果没有 Runnable 这层统一抽象,每增加一步,代码都要改很多地方;而在 LCEL 下,这些变化更像“往链上加节点”。这也是为什么 LangChain 的链式调用会让人觉得“像拼积木”。 也正因为这种统一性,后面你会发现检索器、工具、记忆模块甚至 LangGraph 节点,理解起来都会更顺。很多更复杂的高级能力,本质上都是在 Runnable 这层统一抽象之上继续往上搭。 --- ## 2、LCEL 简介 ### 2.1 定义 **LCEL** 是 **LangChain Expression Language** 的缩写,中文通常叫 **LangChain 表达式语言**。它的作用很直接:**用一种声明式、可组合的方式,把多个 Runnable 连接起来。** 最经典的 LCEL 写法就是: ```python chain = prompt | model | parser result = chain.invoke({"question": "什么是 LangChain?"}) ``` 所以你先把 LCEL 理解成:**“用 `|(管道符)` 或其他组合方式,把多个 Runnable 连接起来的一套表达方法。”** 但 LCEL 不只是“把对象连起来”,它还在帮我们做两件事: - 把前一步输出自动传给后一步 - 尽量减少不同节点之间手写适配的样板代码 ### 2.2 LCEL 不只是语法糖 很多人第一次看到 `prompt | model | parser`,会觉得它只是“写起来更短”。但 LCEL 的价值远不止省代码。 它真正带来的好处有三层: **1. 表达更清晰** 传统写法: ```python prompt_value = prompt.invoke({"question": "什么是 LangChain?"}) ai_message = model.invoke(prompt_value) result = parser.invoke(ai_message) ``` LCEL 写法: ```python chain = prompt | model | parser result = chain.invoke({"question": "什么是 LangChain?"}) ``` 第二种写法更像在表达“流程本身”,而不是只是在堆代码。 **2. 更容易组合** - 用 LCEL 连接出来的结果,本身还可以继续参与组合: - 一条顺序链可以放进分支链 - 一条顺序链可以放进并行链 - 并行链的输出还可以继续交给下一步处理 而且在很多常见场景里,LCEL 还会帮我们自动处理“前一步结果如何传给下一步”这类重复性工作。只有当前后节点的输入输出结构不匹配时,我们才需要自己插入 `lambda` 或 `RunnableLambda` 做一次映射。 **3. 统一支持同步、异步、批量、流式** 根据 LangChain 官方参考,`RunnableSequence` 和 `RunnableParallel` 这类组合结构,会自动继承 Runnable 的同步、异步、批量、流式能力。这意味着你不只是得到了一条“可读的链”,还得到了一条“可统一执行的链”。 ### 2.3 LCEL 的核心组合思想 在入门阶段,本章最重要的不是死记类名,而是理解 LCEL 背后的几种核心组合思想: - **顺序组合**:前一步输出作为下一步输入 - **条件路由**:根据输入选择不同链 - **并行组合**:同一输入同时喂给多条链 这里再补充一层组合视角: - **RunnableSequence** 和 **RunnableParallel** 是两种最核心的组合原语 - 很多其他 Runnable 结构,本质上都可以看作在这两种基础组合能力上的扩展或变体 而 `RunnableLambda` 则相当于在组合过程中插入一段你自己的 Python 逻辑,让你可以做: - 中间结果调试 - 输入结构映射 - 输出结果整理 - 简单业务判断 也就是说,LCEL 的本质并不是“多了几个类”,而是:**LangChain 开始让你用“数据流编排”的方式思考 LLM 应用。** ### 2.4 LCEL 和 Chain 的关系 这一点特别容易混淆,所以单独强调一下: - **LCEL 是什么**:它是构建流程的一种表达方式,重点在“怎么写”。 - **Chain 是什么**:它是通过 LCEL 或其他组合方式构建出来的可执行流程,重点在“最后得到了什么”。 所以可以先用一句最容易记的话来理解:**LCEL 是构建链的方法,Chain 是构建出来的流程。** --- ## 3、Chain 结构 ### 3.1 定义 在 LangChain 语境里,**Chain(链)** 可以简单理解成:**把多个 Runnable 按某种规则组合起来后,形成的一段可执行流程。** 这段流程可以很简单,也可以很复杂: - 简单时,就是 `prompt | model` - 常见时,是 `prompt | model | parser` - 复杂时,可能是分支 + 并行 + 自定义函数节点混合组成的流程 最关键的一点是:**链本身也是 Runnable。** 这意味着链不会成为“终点对象”,它依然可以继续被组合。比如: - 一条顺序链可以作为 `RunnableBranch` 的分支 - 一条顺序链可以放进 `RunnableParallel` - 并行结果还可以继续交给后面的节点处理 ### 3.2 典型结构 这一节开始,我们不再重点讨论“怎么写链”,而是看“链通常长什么样”。这一章和前面三章的关系非常紧密。 如果把 [第 13 章](13-提示词与消息模板.md)、[第 11 章](11-Model-I-O与模型接入.md)、[第 14 章](14-输出解析器.md) 放在一起看,你会发现最典型的一条链就是: 1. **Prompt**:组织输入 2. **Model**:调用模型生成结果 3. **Parser**:把结果解析成业务更容易使用的形式 也就是: ```python chain = prompt | model | parser ``` 这条链不是偶然写法,而是 LangChain 里最经典、最基础的组合方式。 --- ## 4、链式调用基础用法与案例 ### 4.1 几种链的选择与对比 | 类型 | 典型写法 / 类名 | 执行方式 | 输入 → 输出 | 典型场景 | | -------------- | ------------------------------------------------ | ---------------------- | --------------- | ---------------------------- | | **顺序链** | `prompt -> model -> parser` / `RunnableSequence` | 一步接一步执行 | 单输入 → 单输出 | 最基础的问答、抽取、摘要 | | **分支链** | `RunnableBranch(...)` | 按条件只走其中一条子链 | 单输入 → 单输出 | 意图路由、多语言路由 | | **多步串行链** | 多条子链继续串联 | 前一步结果给后一步 | 单输入 → 单输出 | 先总结再翻译、先整理再生成 | | **并行链** | `RunnableParallel({...})` | 多条子链同时执行 | 单输入 → 多输出 | 中英文同时生成、多模型并跑 | | **函数链** | `RunnableLambda(func)` | 在链中插入 Python 函数 | 取决于函数 | 字段映射、调试、轻量业务逻辑 | 如果你现在只想快速建立直觉,可以这样选: - **只有一条直线流程**:先用顺序链 - **要按条件切换不同流程**:用分支链 - **要把多个模型调用前后串起来**:用多步串行链 - **要同一输入同时跑多条链**:用并行链 - **要插入自定义 Python 逻辑**:用函数链 --- ### 4.2 RunnableSequence(顺序链) `RunnableSequence` 是最常见、也最重要的链类型之一。LangChain 官方参考里明确说明,它几乎出现在所有基础链式流程中。 它的核心规则很简单:**前一个 Runnable 的输出,直接作为后一个 Runnable 的输入。** 所以: ```python chain = prompt | model | parser ``` 本质上就是一个 `RunnableSequence`。 你也可以显式写成: ```python from langchain_core.runnables import RunnableSequence chain = RunnableSequence(first=prompt, middle=[model], last=parser) ``` 但在实际开发中,更常见、更推荐的仍然是管道符写法,因为可读性更好。 这类链最典型的场景,就是把前面几章的核心内容一次性串起来: - Prompt 组织输入 - Model 调用模型 - Parser 把输出转成字符串或结构化结果 【案例源码】`案例与源码-2-LangChain框架/06-lcel/LCEL_RunnableSequenceDemo.py` [LCEL_RunnableSequenceDemo.py](案例与源码-2-LangChain框架/06-lcel/LCEL_RunnableSequenceDemo.py ":include :type=code") 这个案例建议重点看一下,因为它不仅演示了“分步执行”,也演示了“直接把三步写成一条链再一次 invoke”。这正是 LCEL 的核心体验。 ### 4.3 RunnableBranch(分支链) 如果顺序链解决的是“按顺序做”,那么 `RunnableBranch` 解决的就是:**根据输入内容,决定走哪条链。** 它很像编程里的 `if / elif / else`,只是现在被放进了 Runnable 体系中。典型结构是: ```python RunnableBranch( (条件1, 链1), (条件2, 链2), 默认链, ) ``` 执行时会依次判断条件: - 第一个命中的条件对应的链会被执行 - 如果都不命中,就走默认链 这在项目里非常常见,比如: - 根据语言选择不同翻译链 - 根据用户意图选择不同客服流程 - 根据问题类型选择不同 Prompt 或模型 本章配套案例就是一个非常典型的“语言路由”例子: - 输入里有“日语”关键词,走日语翻译链 - 输入里有“韩语”关键词,走韩语翻译链 - 否则默认走英语翻译链 【案例源码】`案例与源码-2-LangChain框架/06-lcel/LCEL_RunnableBranchDemo.py` [LCEL_RunnableBranchDemo.py](案例与源码-2-LangChain框架/06-lcel/LCEL_RunnableBranchDemo.py ":include :type=code") 这个案例和实际项目非常贴近,因为很多真实业务并不是“所有请求走同一条链”,而是“先判断,再分流”。 ### 4.4 Multi-Step Chain(多步串行链) 这一节要学的重点是:**如何把多条子链首尾串起来,形成多步串行流程。** 比如这个案例做的事是: 1. 先让模型用中文介绍某个主题 2. 再把第一步结果转换成第二步所需的输入结构 3. 最后再交给另一条链翻译成英文 也就是一种非常典型的“多步加工”: - 第一步不是最终结果 - 第二步依赖第一步结果 - 中间还可能需要做一次结构映射 这类串行链在项目里特别常见,比如: - 先摘要,再翻译 - 先提取要点,再生成报告 - 先检索,再重写,再结构化 【案例源码】`案例与源码-2-LangChain框架/06-lcel/LCEL_MultiStepChainDemo.py` [LCEL_MultiStepChainDemo.py](案例与源码-2-LangChain框架/06-lcel/LCEL_MultiStepChainDemo.py ":include :type=code") 这个案例里最值得你注意的点,不是文件名,而是中间这个映射动作: ```python (lambda content: {"input": content}) ``` 它说明一个很常见的现实问题:**前后两步的输入输出结构,不一定天然匹配。**这也是为什么函数节点和 RunnableLambda 在 LCEL 中很实用。 ### 4.5 RunnableParallel(并行链) `RunnableParallel` 解决的是另一个常见问题:**同一份输入,我想同时交给多条链处理。** 它的核心特点是:多条子链共享同一输入、同时执行,最后再把结果按键汇总成一个 `dict`。 ![RunnableParallel 并行链:同一输入进入多条 Runnable,并将结果汇总成 dict 后交给下游链](images/15/15-4-5-1.svg) 从执行机制上理解也很有帮助: - 同步执行时,LangChain 通常会并发调度这些子 Runnable - 异步执行时,思路上就是把多个异步任务一起等待后再汇总结果 你不一定需要一开始就记住底层细节,但要知道:**并行链不是“写在一起”,而是真的在“同一输入下多路执行”。** 例如: ```python parallel_chain = RunnableParallel({ "chinese": chain1, "english": chain2, }) ``` 除了显式使用 `RunnableParallel(...)`,LCEL 里还有一个非常实用的写法:**直接用字典表达并行结构**。例如: ```python parallel_then_summary = { "paragraph_1": chain1, "paragraph_2": chain2, } | summary_chain ``` 这段写法的含义是: 1. 先并行运行 `chain1` 和 `chain2` 2. 把结果汇总成一个字典 3. 再把这个字典交给后面的 `summary_chain` 这类写法在实际项目里很常见,因为很多业务并不是“并行完就结束”,而是“并行生成多个结果后,再统一分析或总结”。 调用后返回结果可能像这样: ```python { "chinese": "...", "english": "..." } ``` 根据 LangChain 官方参考,`RunnableParallel` 是与 `RunnableSequence` 并列的另一个核心组合原语。 这种链在实际项目里也非常常见: - 同一问题同时生成中英文答案 - 同一问题同时走多个模型做对比 - 同一份输入同时做多个维度分析 下面这个案例就是一个很清晰的例子: - 一条子链用中文介绍 `LangChain` - 另一条子链用英文介绍 `LangChain` - 然后一次性返回两个结果 【案例源码】`案例与源码-2-LangChain框架/06-lcel/LCEL_RunnableParallelDemo.py` [LCEL_RunnableParallelDemo.py](案例与源码-2-LangChain框架/06-lcel/LCEL_RunnableParallelDemo.py ":include :type=code") 这个案例还有一个很有价值的补充点:它调用了 `get_graph().print_ascii()`。这有助于你从“代码链”过渡到“图结构”的理解,为后续学习 LangGraph 做铺垫。 ### 4.6 RunnableLambda(函数链) `RunnableLambda` 的价值在于:**把普通 Python 函数也变成 Runnable。** 这听起来像个小功能,但在实际开发里非常实用,因为链式流程经常会遇到这种情况: - 上一步输出结构不符合下一步输入要求 - 想打印中间结果做调试 - 想做一次字段重命名 - 想插入一点简单业务逻辑 如果没有函数节点,你就得把链拆开,在外面手写很多中间处理代码;有了 `RunnableLambda`,这些逻辑就能被放回链内部。 例如: ```python from langchain_core.runnables import RunnableLambda def debug_print(x): print(x) return {"input": x} chain = chain1 | RunnableLambda(debug_print) | chain2 ``` LangChain 还支持一种更省事的写法:直接把函数放在 `|` 中间,框架会自动包装成 Runnable。 本章配套案例也同时演示了两种写法: - `chain1 | debug_print | chain2` - `chain1 | RunnableLambda(debug_print) | chain2` 【案例源码】`案例与源码-2-LangChain框架/06-lcel/LCEL_RunnableLambdaDemo.py` [LCEL_RunnableLambdaDemo.py](案例与源码-2-LangChain框架/06-lcel/LCEL_RunnableLambdaDemo.py ":include :type=code") 从项目角度看,RunnableLambda 很像“链里的胶水层”: - 不负责模型能力 - 负责把前后节点粘起来 如果前面的“多步串行链”让你感受到“为什么需要中间映射”,这一节就是在回答“中间映射应该怎么优雅地写进链里”。 ### 4.7 补充:其他常见 Runnable 结构 除了本章重点展开的几种结构,LangChain 里还有一些在实际项目里很常见、但初学阶段不必深入的 Runnable 组件。 | 名称 | 作用 | 什么时候会用到 | | ----------------------- | ------------------------------------------------------- | -------------------------------------------- | | `RunnablePassthrough` | 接收输入后原样透传,也可顺手往输出中补充字段 | 想保留原始输入、给结果加键、做轻量上下文拼装 | | `RunnableWithFallbacks` | 为某个 Runnable 配置兜底逻辑,失败后回退到备用 Runnable | 模型调用失败、主链失败后自动切备用方案 | | `RunnableBinding` | 为 Runnable 预绑定 `config`、默认参数等 | 同一链在不同环境复用、可配置温度与模型等 | 你现在不需要立刻掌握它们,但可以先建立印象:**Runnable 世界不只有“顺序、分支、并行、lambda”这几种,LangChain 提供了一整套可执行节点家族。** ### 4.8 重试与兜底 真实项目里,链路不是每次都能顺利跑完:模型接口会超时,结构化输出可能偶尔不合规,外部服务也可能短暂失败。LCEL 的好处之一,是可以把这类稳定性处理挂在 Runnable 上,而不是散落在业务代码各处。 最常见的两个动作是: | 写法 | 作用 | 适合场景 | | ----------------------- | ----------------------------------- | ------------------------------ | | `with_retry(...)` | 当前 Runnable 失败后自动重试 | 临时网络抖动、模型偶发格式错误 | | `with_fallbacks([...])` | 主 Runnable 失败后切到备用 Runnable | 主模型不可用、主链策略失败 | 简化示例: ```python safe_chain = chain.with_retry(stop_after_attempt=3) fallback_chain = primary_chain.with_fallbacks([backup_chain]) ``` 这类写法不改变链的主流程,却能让项目更接近生产环境。入门阶段不需要把参数都背下来,先记住:**LCEL 不只负责“怎么串”,也能帮助你把重试、兜底、配置这些工程能力放到链上。** --- **章节思考题:** 1. LCEL 帮你解决的不是“少写几行代码”,而是什么问题? **参考思路:** 它让输入格式化、模型调用、解析、分支、并行和后处理都变成可组合的 Runnable。重点是流程结构清楚、可复用、可流式、可批处理,而不是管道符看起来简洁。 2. 读一条 LCEL 链时,你会如何判断每一段是否设计合理? **参考思路:** 看每个 Runnable 的输入和输出是否清楚,是否只做一类事情,是否方便单独测试。链路问题很多不是模型错,而是前后节点的数据结构没对上。 3. `RunnableLambda` 什么时候是好用的胶水,什么时候会变成坏味道? **参考思路:** 少量字段转换、清洗、路由判断适合用它;如果里面塞了大量业务逻辑、外部副作用和复杂异常处理,就应该拆成明确函数、工具或服务,而不是藏在链里。 4. 遇到“先分类、再分流、部分并行、最后汇总”的任务,你会如何拆链? **参考思路:** 先用顺序链做分类,再用分支链选择路线,可并行的检索或分析用并行链,最后用汇总节点统一输出。先画数据流,再写 LCEL,会比直接拼管道稳。 **本章小结:** - **Runnable** 是 LangChain 中统一的“可执行组件”抽象。Prompt、Model、Parser、Tool、Chain 之所以能被统一调用,是因为它们在 Runnable 这层被约束成了同一种接口风格。 - **LCEL** 是把多个 Runnable 组合成链的表达式语言。它最核心的价值,不只是 `|` 写起来简洁,而是让流程变得声明式、可组合、可扩展。 - **链的核心主线** 是前面几章内容的自然延伸:把 [提示词模板](13-提示词与消息模板.md)、[模型调用](11-Model-I-O与模型接入.md)、[输出解析器](14-输出解析器.md) 组织成一条可执行流程。 - **常见链结构** 中,顺序链是基础,分支链解决路由问题,多步串行链解决前后步骤依赖问题,并行链解决多路同时处理问题,RunnableLambda 解决自定义逻辑如何插入链;重试与兜底则解决链路稳定性问题。 - 从掌握结果看,学完本章后,你至少应该:明白 `Runnable` 为什么是 LangChain 里最重要的统一抽象,知道 Prompt、Model、Parser、Chain 都能被统一调用;能熟练读懂并写出 `prompt | model | parser` 这种最基础 LCEL 链;能区分顺序链、分支链、多步串行链、并行链、函数链的使用场景。 **建议下一步:** 先把本章 5 个案例都跑一遍,重点观察“每一步输入输出是什么、为什么能接上下一步”;然后继续学习 [第 16 章 记忆与对话历史](16-记忆与对话历史(含Redis基础).md) 和 [第 17 章 Tools 工具调用](17-Tools工具调用.md),你会更清楚链式调用如何进一步扩展成带状态、带工具、带决策能力的 LLM 应用。