LangChain:从设计模式层面来理解链(Chain)的设计
LangChain 中的“链”(Chain)并非单一对应某一种设计模式,而是多种设计模式的巧妙融合。
LangChain 的 Chain 本质上责任链的现代化实现 —— 通过 Runnable 协议统一接口,用组合模式支持嵌套,借建造者模式简化构造,最终实现了声明式、可组合、延迟执行的 LLM 应用流水线。
LangChain:Chain
在 LangChain 中,链(Chain)的核心思想是将多个处理单元(如 LLM 调用、提示模板、解析器等)连接成一个可执行的工作流,并支持线性顺序、条件路由等控制流。如果用设计模式来类比,最贴切的是 责任链模式(Chain of Responsibility Pattern),同时在某些场景下也体现了 管道模式(Pipeline Pattern) 和 模板方法模式(Template Method Pattern)。
在LangChain 中,链(Chain) 本质上责任链的现代化实现 —— 通过 Runnable 协议统一接口,用组合模式支持嵌套,借建造者模式简化构造,最终实现了声明式、可组合、延迟执行的 LLM 应用流水线。
这种设计让开发者可以用 a | b | c 这样直观的语法,构建出堪比 DAG (有向无环图) 的复杂工作流,而底层依然保持清晰的职责分离。
匹配的设计模式
责任链模式
责任链模式(Chain of Responsibility):使多个对象都有机会处理请求,将它们连成一条链,并沿着链传递请求,直到有一个对象处理它为止。
- LangChain 中的体现:
基础链/顺序链:每个组件(如
Runnable)依次执行,并将输出传递给下一个。这类似于责任链中每个处理器处理请求后必须传递给下一个(除非是终结点)。当创建一个
RunnableSequence时,数据会像流水线一样,依次通过链上的每一个组件。前一个组件的输出自动成为后一个组件的输入,每个组件都负责处理流程中的一个特定环节。这种线性的、有序的数据传递和处理过程,正是责任链模式的精髓。路由链:根据输入条件动态选择下一个链,这是责任链的典型特征——每个节点判断自己是否能处理请求,不能则转发。LangChain 的
RunnableBranch本质上就是一个条件责任链。链的可组合性:你可以将整个链视为一个节点,嵌入到更大的链中,形成嵌套的责任链。
- 为什么最合适:
- 解耦:链中的每个组件只关心自己的输入/输出,无需知道整个工作流的全貌。
- 灵活性:可以在运行时动态改变链的结构(如添加、删除中间步骤)。
- 单一职责:每个链节点只完成一个明确的任务,符合责任链中“处理器仅处理自己关心的部分”的原则。
代码示例映射:
1
2
3
4
5
6
7
8# 责任链风格:每个函数判断是否处理
def chain_step_1(x):
# 1. 自己处理
return self.process(x)
# 2. 决定是否传递给下一个
if self.next_chain:
return self.next_chain(output)
return defaultLangChain 的
RunnableSequence和RunnableBranch正是这种思想的封装。责任链特性 LangChain 实现 解耦请求发送者和接收者 Runnable接口统一输入输出多个对象有机会处理请求 LLMChain→TransformChain→RouterChain链式传递 `chain1
管道模式
管道模式(Pipeline Pattern):也称管道与过滤器模式,将数据处理看作一系列“过滤器”,数据在管道中流动,每个过滤器对数据进行转换。
LangChain 中的体现:
- LCEL(LangChain Expression Language)使用管道符
|连接组件,如prompt | model | parser,这几乎是管道模式的教科书式实现。 - 数据单向流动,每个阶段是独立的过滤器。
- LCEL(LangChain Expression Language)使用管道符
区别:管道模式通常所有阶段都会执行,没有条件跳过或路由。而 LangChain 的路由链允许分支,超出了经典管道模式的范围。因此管道模式更适合描述顺序链,但不足以覆盖路由链。
1 | # 类似管道-过滤器的实现逻辑:chain = prompt | model | parser |
组合模式
组合模式 (Composite Pattern):通过树形结构组织对象,使客户端能够以统一方式处理单个对象和对象组合,适用于整体与部分具有层次关系的场景。
LangChain 中的体现:
链允许将简单的组件(如提示词模板、大语言模型、输出解析器)组合成更复杂的、功能统一的对象。
可以像搭积木一样,通过管道符
|或.pipe()方法将多个Runnable对象串联起来,形成一个RunnableSequence。这个序列本身也是一个Runnable对象,可以被继续组合或调用。这完美体现了组合模式“部分-整体”的层次结构思想,让客户端可以统一地对待单个对象和组合对象。链可以嵌套链,统一了接口调用。
Runnable接口让原子组件和复杂链条对外暴露相同的调用方式。SimpleSequentialChain 和 SequentialChain 形成树形结构,
1 | # 组合模式:统一对待单个组件和组合组件 |
模板方法模式
模板方法模式(Template Method Pattern):父类定义算法的骨架(模板方法),子类实现某些具体步骤。
LangChain 中的体现:
- 基类
Chain(老版本)或Runnable定义了invoke、batch、stream等公共方法,子类需要实现_call或_invoke等钩子方法。BaseChain._call()定义骨架,子类定制具体步骤,但保持执行流程的一致性。 - 例如,LLMChain固定了调用流程:格式化提示词→调用LLM→解析输出。自定义链时只需要实现
_call逻辑,框架会处理错误重试、回调、异步等通用行为。
- 基类
为什么不是最贴切:模板方法模式侧重于继承和算法步骤的固定顺序,而 LangChain 的链更强调组合(将一个链作为另一个链的组件),且路由链的步骤选择是动态的,并非编译时确定。
装饰模式
装饰模式(Decorator Pattern),又称装饰者模式,属于结构型设计模式。该模式通过组合而非继承的方式动态扩展对象功能,在不修改原类结构的前提下,由装饰类包裹真实对象并叠加附加职责,其核心设计遵循开闭原则。装饰对象与原始对象共享接口,通过持有引用在请求转发前后实现功能增强,常用于需运行时灵活扩展功能的场景。
LangChain 中的体现:
LangChain 提供了一系列便捷的方法来“包装”一个现有的链,为其增加新的行为。例如:
with_retry(): 为主链增加失败重试的能力。with_fallbacks(): 为主链添加降级方案,当主链执行失败时尝试备用链。with_types(): 为链附加类型信息,用于类型检查。
结论
综上所述,LangChain 的“链”是一个复合设计模式的典范:
- 它使用责任链模式和组合模式来构建灵活、线性的工作流。
- 它使用模板方法模式来规范具体链的实现框架。
- 它使用装饰器模式来优雅地扩展链的功能。
| 设计模式 | 在 LangChain 中的体现 | 适用场景 |
|---|---|---|
| 责任链模式 | 完美匹配路由链的条件转发,也能解释顺序链的线性传递。 | 动态选择执行路径 |
| 管道模式 | 完美匹配LCEL的管道语法,但无法涵盖条件分支。 | 大多数顺序处理链 |
| 组合模式 | 完美匹配了链嵌套链,链可以灵活组合实现完整的功能 | SequentialChain, 复杂工作流 |
| 模板方法模式 | 匹配 LangChain 的基类骨架,但不是链组合的核心思想。 | 标准化链的执行流程 |
| 装饰模式 | 匹配对现有的链进行包含,增加新的行为 | 失败重试、回退 |
设计模式的表现差异
设计模式在不同软件开发语言的表现差异。
本人直观的感觉是设计模式在Java语言的程序设计中表现很明显,但在 Python,Golang开发语言就表现不明显。
语言的特性差异
| 语言 | 特性 | 设计哲学 |
|---|---|---|
| Java | 静态强类型语言,语法严谨,但表达方式单一 需要显式声明类型和接口,结构化的代码是显式的, 导致必须通过设计模式来解耦类型依赖 |
企业级、 规范优先, 严谨结构 |
| Python | 动态类型,关注对象行为而非类型, 类型运行时决定,减少抽象层(接口,实现) 函数是一等公民,函数作为参数传递,替代策略模式 语言特性内化了许多设计模式 |
实用主义 简洁至上 |
| Golang | 简单设计,没有类和继承, 只有组合,用组合替代复杂模式 隐匿接口,不需要显示声明实现 追求 简单、可读、务实,避免抽象过度 |
极简主义、 组合优先 |
对OOP依赖程度
- Java推崇纯OOP:一切皆对象,设计模式大多基于“继承”和“多态”构建。必须显式地画出类图,写出层次结构,这放大了模式的存在感。
- Python是“多范式”语言:可以用OOP,也可以用函数式、过程式。很多模式可以用更轻量的闭包或高阶函数代替。
- Go是“实用主义”:它没有类和继承,只有
struct和接口。它选择用goroutine和channel解决并发问题,而不是用Observer模式或Future模式。在并发领域,Go的模式表现非常“明显”,只是换了个赛道。
观点引用
Java 的语法约束迫使开发者显式使用设计模式,而 Python 和 Go 的语言特性让这些模式”隐形”或”简化”了。
在静态类型、缺乏高阶抽象的语言(如 Java 早期)中,开发者需手动实现如工厂、策略、观察者等模式来弥补语言表达力的不足。
在支持高阶函数、鸭子类型、多重分发、宏系统等特性的语言(如 Lisp、Python、Julia、Go)中,这些模式要么被语言原生支持,要么可通过更简单的方式实现,从而“消失”或“简化”。
在某些编程语言中,设计模式是对语言局限性的权宜之计。而在更强大的语言(如 Lisp 或 Python)中,很多模式要么变得简单,要么根本不需要。 —— Peter Norvig《动态语言设计模式》
因此,设计模式并非绝对必要,而是特定语言生态下对缺陷的适应性回应。
“设计模式是语言缺陷的补丁” —— Peter Norvig(Google 研发总监、知名人工智能专家)
”设计模式是经验的结晶,不是代码的枷锁“——GoF成员Erich Gamma
Java 需要显式模式来弥补语言的静态约束,Python/Go 通过语言演进吸收了模式的本质,抛弃了模式的仪式。
并不是在Python/Go中看不到设计模式,是因为它们有更直接、更轻量的语言特性来实现同一目的。设计模式被语言特性重新表达了,模式本身就溶解在了语言特性中。以至于不再称之为”模式”,而称之为”写法”。
这反映了编程语言的演进趋势:从”模式化编程”到”语言特性化”,从”显式结构”到”隐式简化”。
许多模式本质上是弥补语言特性不足的解决方案,当语言本身提供更优雅的替代方案时,模式就变得不必要。
设计模式的思想是永恒的(如“组合优于继承”、“针对接口编程”),但具体实现形式是依赖于语言的。
LangChain:从设计模式层面来理解链(Chain)的设计
http://blog.gxitsky.com/2026/04/12/AI-LangChain-037-Chain-Design-Mode/

