三星云服务
27.14M · 2026-04-12
经过前面十七章的源码深潜,我们已经逐层拆解了 LangChain 的每一个核心组件。现在是时候抬起头来,从全局视角审视这个框架的设计哲学了。
LangChain 不仅仅是一个工具库,它是一个关于"如何构建 AI 应用框架"的设计范本。在它的源码中,蕴含着一系列精心选择的设计模式和架构决策。这些模式不是 LangChain 独创的 -- 它们来自分布式系统、编译器设计、Web 框架等多个领域 -- 但 LangChain 将它们巧妙地组合在一起,应用到 AI 应用这个全新的领域。
本章将从 LangChain 的具体实现中提炼出五个核心设计模式:Runnable 协议、回调洋葱模型、Partner 解耦架构、LCEL 组合优于继承、以及安全纵深防御。每个模式我们都将分析其动机、实现、权衡和可迁移性。最后,我们会讨论如何将这些模式应用到你自己的 AI 应用框架中。
:::tip 本章要点
在 LangChain 早期,每种组件都有自己的接口。LLM 用 predict,Chain 用 run,Tool 用 _run。开发者需要记住每种组件的 API,组合时还需要手动编写胶水代码。
Runnable 协议的引入解决了这个问题:所有可执行组件共享同一套接口。
class Runnable(Generic[Input, Output], ABC):
def invoke(self, input: Input, config: RunnableConfig = None) -> Output: ...
async def ainvoke(self, input: Input, config: RunnableConfig = None) -> Output: ...
def stream(self, input: Input, config: RunnableConfig = None) -> Iterator[Output]: ...
async def astream(self, input: Input, config: RunnableConfig = None) -> AsyncIterator[Output]: ...
def batch(self, inputs: list[Input], config: RunnableConfig = None) -> list[Output]: ...
async def abatch(self, inputs: list[Input], config: RunnableConfig = None) -> list[Output]: ...
# 组合操作
def pipe(self, *others) -> RunnableSequence: ...
def __or__(self, other) -> RunnableSequence: ... # 支持 | 操作符
# 配置操作
def with_config(self, config) -> RunnableBinding: ...
def with_retry(self, **kwargs) -> RunnableRetry: ...
def with_fallbacks(self, fallbacks) -> RunnableWithFallbacks: ...
def configurable_fields(self, **kwargs) -> DynamicRunnable: ...
统一的代价:所有组件必须接受 Input 返回 Output,这意味着类型信息在组合时可能被弱化。LangChain 通过泛型和 Pydantic 的 get_input_schema / get_output_schema 来缓解这个问题,但运行时的类型安全仍然有限。
"方法爆炸":每种组件需要实现 invoke、ainvoke、stream、astream、batch、abatch 六个基本方法。LangChain 通过默认实现减轻了这个负担:ainvoke 默认调用线程池中的 invoke,stream 默认 yield 单个 invoke 结果,batch 默认对每个输入调用 invoke。子类只需覆盖性能关键的方法。
flowchart TB
subgraph "Runnable 协议统一的世界"
A["ChatModel<br/>Runnable[list~Message~, AIMessage]"]
B["PromptTemplate<br/>Runnable[dict, PromptValue]"]
C["OutputParser<br/>Runnable[AIMessage, dict]"]
D["Retriever<br/>Runnable[str, list~Document~]"]
E["Tool<br/>Runnable[str|dict, str]"]
end
F["统一操作"] --> A
F --> B
F --> C
F --> D
F --> E
G["invoke / ainvoke"] --> F
H["stream / astream"] --> F
I["batch / abatch"] --> F
J["pipe / | 操作符"] --> F
K["with_retry / with_fallbacks"] --> F
Runnable 协议模式适用于任何需要将异构组件统一起来的场景。它的核心价值在于将"做什么"的多样性与"怎么调用"的统一性分开。
关键原则是:定义最小但完备的公共接口,所有组件必须支持的操作集合不能太多(否则实现负担太重)也不能太少(否则功能受限)。提供合理的默认实现来降低门槛,让开发者可以只覆盖关键方法就获得完整功能。支持组合操作让组件可以无缝连接,这是框架价值的倍增器。通过泛型保留类型信息,尽管有统一接口,输入输出的类型仍然可以在静态分析中追踪。
这个模式在函数式编程社区中有着深厚的理论基础。Runnable 本质上是一个范畴论中的态射(morphism),而 | 操作符就是态射的组合。RunnableParallel 对应积(product),RunnableBranch 对应余积(coproduct)。虽然 LangChain 的实现不需要开发者了解范畴论,但底层的数学结构保证了组合操作的一致性和可预测性。
AI 应用的调试和监控比传统应用更加困难。一次 Agent 执行可能涉及多轮 LLM 调用、多次工具执行、数十个 token 的流式输出。开发者需要看到整个过程的详细信息,但又不希望在业务代码中到处插入日志语句。
LangChain 的回调系统借鉴了 Web 框架的中间件模式,形成一个"洋葱模型":每个 Runnable 的执行被包裹在 on_xxx_start 和 on_xxx_end 回调对中。
class BaseCallbackHandler:
def on_llm_start(self, serialized, prompts, **kwargs): ...
def on_llm_new_token(self, token, **kwargs): ...
def on_llm_end(self, response, **kwargs): ...
def on_llm_error(self, error, **kwargs): ...
def on_chain_start(self, serialized, inputs, **kwargs): ...
def on_chain_end(self, outputs, **kwargs): ...
def on_chain_error(self, error, **kwargs): ...
def on_tool_start(self, serialized, input_str, **kwargs): ...
def on_tool_end(self, output, **kwargs): ...
def on_tool_error(self, error, **kwargs): ...
def on_agent_action(self, action, **kwargs): ...
def on_agent_finish(self, finish, **kwargs): ...
flowchart TB
subgraph "AgentExecutor._call"
direction TB
A["on_chain_start(AgentExecutor)"] --> B
subgraph "Agent.plan"
B["on_chain_start(Agent)"] --> C
subgraph "LLM._generate"
C["on_llm_start(ChatOpenAI)"]
C --> D["on_llm_new_token (逐 token)"]
D --> E["on_llm_end"]
end
E --> F["on_chain_end(Agent)"]
end
F --> G["on_agent_action"]
G --> H
subgraph "Tool.run"
H["on_tool_start(search)"]
H --> I["on_tool_end"]
end
I --> J["on_agent_finish"]
J --> K["on_chain_end(AgentExecutor)"]
end
关键的设计决策是 get_child() 方法。当 AgentExecutor 调用 Agent 的 plan 方法时,它创建一个子 CallbackManager:
output = self._action_agent.plan(
intermediate_steps,
callbacks=run_manager.get_child() if run_manager else None,
**inputs,
)
子 CallbackManager 继承父级的所有 handler,但有独立的 run_id。这确保了:
性能开销:每次 Runnable 调用都会触发回调,即使没有注册任何 handler。LangChain 通过 verbose 标志和懒加载来缓解这个问题。
handler 接口膨胀:随着组件类型增加,回调方法也在增加(on_llm_xxx、on_chain_xxx、on_tool_xxx、on_agent_xxx)。这是"统一 handler 接口"与"类型安全"之间的权衡。一种可能的改进方向是使用事件系统取代固定方法名 -- handler 注册关心的事件类型,而非实现特定的方法。LangChain 的 astream_events 已经在这个方向上迈出了一步。
回调传播的隐式性:回调通过 RunnableConfig 在调用链中隐式传播,这使得追踪回调的来源变得困难。当一个 handler 被意外触发或未被触发时,调试的难度较高。开发者需要理解 get_child 的父子关系创建机制,才能正确理解回调的传播路径。
洋葱回调模式适用于任何需要非侵入式观测的系统。三个关键要素:
get_child() 建立调用链层次AI 生态系统的服务提供商数量以指数级增长。LangChain 需要支持数十个 LLM 提供商、数十个向量数据库、数十个工具服务。如果全部放在一个包中,依赖冲突和安装体积将变得不可控。
langchain-core (稳定锚点)
|
+-- langchain-openai (独立发布)
+-- langchain-anthropic (独立发布)
+-- langchain-groq (独立发布)
+-- langchain-chroma (独立发布)
+-- ...
|
langchain-tests (行为契约)
flowchart TB
subgraph "抽象层 (langchain-core)"
A["BaseChatModel"]
B["BaseEmbeddings"]
C["BaseTool"]
D["BaseRetriever"]
E["Serializable"]
F["Runnable 协议"]
end
subgraph "实现层 (Partner 包)"
G["langchain-openai"]
H["langchain-anthropic"]
I["langchain-chroma"]
end
subgraph "验证层 (langchain-tests)"
J["ChatModelUnitTests"]
K["ChatModelIntegrationTests"]
L["EmbeddingsUnitTests"]
end
A --> G
A --> H
B --> I
J --> G
J --> H
L --> I
抽象层(langchain-core):定义接口,极少变动。所有 Partner 包的稳定依赖。
实现层(Partner 包):独立开发、独立发版。每个包只依赖 langchain-core 和自己的 SDK。
验证层(langchain-tests):定义行为契约。通过继承测试基类,Partner 包自动获得完整的测试覆盖。
发现性:用户需要知道要安装哪个 Partner 包。LangChain 通过文档和错误消息来引导("你需要安装 langchain-openai")。
版本协调:当 langchain-core 更新接口时,所有 Partner 包都需要适配。通过 semver 和 >=x.y.z,<(x+1).0.0 的版本约束来管理。
重复代码:每个 Partner 包都需要实现类似的消息转换、错误处理、重试逻辑。LangChain 通过在 langchain-core 中提供工具函数(如 convert_to_openai_tool)来减少重复。
Partner 解耦架构适用于任何需要管理大量第三方集成的框架:
LangChain 的历史清楚地展示了从继承到组合的演进路径。
早期(继承模式):
# 旧方式:通过继承创建自定义 Chain
class MyCustomChain(Chain):
llm: BaseLLM
prompt: PromptTemplate
output_parser: OutputParser
def _call(self, inputs: dict) -> dict:
prompt_text = self.prompt.format(**inputs)
llm_output = self.llm.predict(prompt_text)
parsed = self.output_parser.parse(llm_output)
return {"output": parsed}
现在(组合模式):
# 新方式:通过 LCEL 组合
chain = prompt | llm | output_parser
flowchart LR
subgraph "继承模式"
A["MyChain(Chain)"] -->|"包含"| B[LLM]
A -->|"包含"| C[Prompt]
A -->|"包含"| D[Parser]
A -->|"手动编写 _call 方法"| E["胶水代码"]
end
subgraph "组合模式 (LCEL)"
F[Prompt] -->|"|"| G[LLM]
G -->|"|"| H[Parser]
I["自动获得"] --> J["stream / batch / async"]
I --> K["fallbacks / retry"]
I --> L["可视化图"]
end
| 操作符自动处理输入输出的对接Agent 构建函数完美体现了这个模式:
def create_tool_calling_agent(llm, tools, prompt, *, message_formatter=...):
llm_with_tools = llm.bind_tools(tools)
return (
RunnablePassthrough.assign(
agent_scratchpad=lambda x: message_formatter(x["intermediate_steps"]),
)
| prompt
| llm_with_tools
| ToolsAgentOutputParser()
)
四个阶段通过 | 连接,每个阶段都是独立的 Runnable。想换一个输出解析器?替换最后一段。想加一个缓存层?在 llm 前面插入。这种灵活性是继承模式无法提供的。
可读性:对于简单的链,LCEL 非常直观。但对于复杂的分支逻辑(RunnableBranch、RunnableParallel 嵌套),可读性可能下降。
调试难度:管道中的错误堆栈可能很深,不如在单个 _call 方法中设断点直观。LangChain 通过回调系统和 LangSmith 追踪来缓解这个问题。
类型推导:Python 的类型系统难以完美追踪 | 操作符链中的类型变换。IDE 的自动补全在管道末端可能失效。
组合优于继承的模式在 AI 应用框架中特别有价值,因为 AI 管道的组合方式千变万化,难以通过有限的类层次来覆盖。三个实践建议:
invoke/stream/batch序列化/反序列化是安全敏感的操作。Python 的 pickle 因为允许任意代码执行而臭名昭著。LangChain 的序列化系统需要在便利性和安全性之间找到平衡。
flowchart TB
A["不可信数据"] --> B["第一层: 转义防护<br/>包含 'lc' 键的普通字典被标记"]
B --> C["第二层: 白名单控制<br/>allowed_objects 限制可实例化的类"]
C --> D["第三层: 命名空间验证<br/>只允许可信包的类路径"]
D --> E["第四层: init_validator<br/>检查构造参数<br/>(如阻止 jinja2 模板)"]
E --> F["第五层: Serializable 子类检查<br/>最终确认是合法的 LC 类"]
F --> G["安全实例化"]
B -.->|"失败"| H["还原为普通字典"]
C -.->|"失败"| I["ValueError"]
D -.->|"失败"| I
E -.->|"失败"| I
F -.->|"失败"| I
每一层都是独立的安全关卡,任何一层的失败都会阻止对象实例化。这种纵深防御确保了即使某一层被绕过,后面的层仍然能够拦截攻击。
is_lc_serializable() 返回 False。开发者必须显式启用。allowed_objects="core" 只允许 langchain_core 中的类。secrets_from_env=False 防止通过构造密钥字段来泄露环境变量。default_init_validator 阻止 template_format="jinja2"。纵深防御模式适用于任何需要反序列化不可信数据的场景:
如果你要构建一个 AI 应用框架(无论是公司内部的还是开源的),以下是从 LangChain 中可以借鉴的核心架构决策。
确定你的框架中所有组件必须遵循的最小接口。不要试图一开始就定义完整的接口,从最简单的 invoke 开始:
class Component(Generic[Input, Output], ABC):
@abstractmethod
def invoke(self, input: Input) -> Output: ...
def __or__(self, other: Component) -> Pipeline:
return Pipeline([self, other])
在框架的最早期就设计好回调/追踪系统。后期加入的可观测性总是不够深入。关键原则:
class Observable:
def invoke(self, input, callbacks=None):
callbacks = callbacks or []
for cb in callbacks:
cb.on_start(input)
try:
result = self._invoke(input)
for cb in callbacks:
cb.on_end(result)
return result
except Exception as e:
for cb in callbacks:
cb.on_error(e)
raise
将核心抽象(接口定义、基本数据类型、配置系统)放在一个独立的包中。这个包的 API 变更要极其谨慎。所有集成都只依赖这个核心包。
my-ai-core (稳定)
- Component 协议
- Message 类型
- Config 系统
- 回调基类
my-ai-openai (频繁更新)
依赖: my-ai-core + openai SDK
my-ai-anthropic (频繁更新)
依赖: my-ai-core + anthropic SDK
不要让用户通过继承来扩展框架。提供组合原语:
# 不要这样
class MyPipeline(Pipeline):
def _run(self, input):
...
# 鼓励这样
pipeline = format_step | llm_step | parse_step
如果你的框架涉及序列化,从第一天就设计安全模型。后期补救总是不够的。关键原则:默认安全、白名单控制、纵深防御。
flowchart LR
subgraph "框架构建路线图"
A["1. 核心协议<br/>invoke / stream / batch"] --> B["2. 可观测性<br/>回调/追踪系统"]
B --> C["3. 核心包稳定<br/>独立发布"]
C --> D["4. 集成解耦<br/>Partner 包模式"]
D --> E["5. 组合原语<br/>管道操作符"]
E --> F["6. 安全模型<br/>序列化白名单"]
end
除了前面明确列举的五个模式之外,LangChain 中还有一个隐含但无处不在的设计原则值得特别提出:约定优于配置。
这个原则体现在多个层面。首先是方法命名的约定。所有 Runnable 都通过 invoke 调用,同步版本不加前缀,异步版本加 a 前缀(ainvoke),流式版本改为 stream 和 astream,批量版本改为 batch 和 abatch。这套命名约定一旦被开发者记住,就可以在不查文档的情况下猜测任何 Runnable 的方法名。
其次是配置传播的约定。RunnableConfig 通过函数参数自动向下传播,开发者不需要手动在每个子调用中传递配置。run_manager.get_child() 约定了父子回调的创建方式。这些约定减少了样板代码,也减少了遗漏配置传播的风险。
然后是 Partner 包的结构约定。包名遵循 langchain-xxx 格式,模块结构遵循 langchain_xxx/chat_models/base.py 的路径约定,密钥使用 XXX_API_KEY 的环境变量名约定,序列化使用 is_lc_serializable 和 get_lc_namespace 的方法约定。新的 Partner 开发者只需要参照现有包的结构,就能快速上手。
最后是测试的约定。标准测试通过继承基类和声明属性的方式工作,不需要额外的配置文件或注解。能力检测通过检查方法是否被覆盖来自动完成,不需要手动声明。这种"检测而非声明"的约定减少了开发者需要维护的配置项。
约定优于配置的核心价值在于降低认知负担。在一个拥有数百个类和数千个方法的框架中,如果每个行为都需要显式配置,开发者会被淹没在配置选项中。通过建立一致的约定,开发者可以将注意力集中在真正需要定制的地方,而非框架的机制性细节。
当然,约定也有局限性。约定是隐式的,新开发者如果不了解这些约定,可能会感到困惑。LangChain 通过详细的文档、丰富的示例和有意义的错误消息来缓解这个问题。例如,当提示模板缺少 agent_scratchpad 变量时,Agent 构建函数会给出明确的错误消息,引导开发者理解这个约定。
公正地审视 LangChain 的设计,也需要承认其局限性。
Runnable 协议试图统一所有组件,但不同组件的本质差异有时会泄漏出来。例如,ChatModel 的 invoke 输入是 list[BaseMessage],而 PromptTemplate 的输入是 dict。在管道中组合它们需要理解每个组件的实际输入输出类型,协议的"统一性"在此处打了折扣。
独立 Partner 包带来了版本碎片化问题。langchain-core 1.2 和 1.3 之间的接口变更可能导致部分 Partner 包暂时不兼容。社区需要持续的版本协调工作。
尽管 LCEL 简化了简单场景,复杂场景下的调试(嵌套的 RunnableParallel、条件分支、动态配置)仍然有不低的学习曲线。框架的抽象层次越多,出错时的定位就越困难。
并非所有 AI 应用都需要 Runnable 协议的全部功能。对于简单的"调用模型-解析输出"场景,直接使用 SDK 可能更直观。框架的价值在复杂场景中才充分体现。
五个设计模式不是孤立存在的,它们之间存在深层的相互依赖和协同关系。理解这些关系,才能完整地把握 LangChain 的架构哲学。
Runnable 协议定义了统一的组件接口,是其他所有模式的根基。LCEL 的管道组合依赖 Runnable 的 __or__ 操作符。回调系统需要 Runnable 提供统一的生命周期钩子(invoke 开始和结束)。配置系统通过 DynamicRunnable 扩展 Runnable 接口。Partner 包通过实现 Runnable 子类(如 BaseChatModel)接入生态。序列化系统依赖 Serializable,而 RunnableSerializable 同时继承了 Runnable 和 Serializable。
可以说,如果 Runnable 协议的设计出了问题,整个框架都会受到影响。这也是为什么 langchain-core 对 Runnable 接口的修改极其谨慎。
回调系统为 Partner 包提供了标准化的可观测性接口。ChatOpenAI 在调用 OpenAI API 前触发 on_llm_start,在每个 token 返回时触发 on_llm_new_token,在调用完成时触发 on_llm_end。Partner 包的实现者不需要了解回调系统的内部机制,只需在正确的时机调用 run_manager 的方法即可。
这种"约定优于配置"的设计让所有 Partner 包自动获得了与 LangSmith、标准日志等追踪系统的集成能力,无需 Partner 开发者进行任何额外工作。
组合模式让用户可以自由地将组件串联起来,但这也增加了安全性的复杂度。一个 LCEL 管道中可能包含数十个 Runnable,每个都有自己的序列化表示。当整个管道被序列化时,需要递归地序列化每个子组件,并确保每个子组件的密钥都被正确替换。反序列化时,需要逐层重建整个管道,每个节点都经过白名单验证。
这种复杂度是"便利性"与"安全性"之间不可避免的张力。LangChain 的选择是在安全性上不妥协(默认最严格的白名单),同时提供足够的配置灵活性(allowed_objects、additional_import_mappings)让开发者可以根据信任等级调整策略。
配置系统通过 RunnableConfig 贯穿整个调用链。在 Agent 场景下,这意味着顶层传入的配置(tags、metadata、callbacks、configurable)会自动传播到 Agent 内部的每次 LLM 调用和工具执行。AgentExecutor 通过 run_manager.get_child() 创建子回调管理器时,配置信息也随之传递。
这种自动传播在多租户场景下尤为重要:不同租户的请求可以携带不同的 configurable 参数(如模型名称、温度),通过 DynamicRunnable 在 Agent 执行循环的每次 LLM 调用时动态解析为对应的模型实例。整个过程对 Agent 的 plan 逻辑完全透明。
flowchart TB
subgraph "模式相互作用"
A["Runnable 协议"] -->|"统一接口"| B["LCEL 组合"]
A -->|"生命周期钩子"| C["回调洋葱"]
A -->|"Serializable 子类"| D["安全序列化"]
A -->|"标准实现接口"| E["Partner 解耦"]
B -->|"管道节点序列化"| D
C -->|"自动传播到"| E
D -->|"白名单注册"| E
F["配置系统"] -->|"RunnableConfig 传播"| A
F -->|"DynamicRunnable"| B
end
将 LangChain 的设计模式与其他 AI 应用框架进行对比,有助于更好地理解每种设计选择的优劣。
LlamaIndex 专注于数据索引和检索增强生成(RAG),与 LangChain 的通用框架定位不同。在接口设计上,LlamaIndex 使用 QueryEngine 和 ChatEngine 等更具领域语义的抽象,而非 LangChain 的通用 Runnable。这种设计在 RAG 场景下更加直观,但通用性和组合性不如 Runnable 协议。
在集成管理上,LlamaIndex 同样采用了独立包模式(llama-index-llms-openai 等),但包名约定和目录结构与 LangChain 不同。两个框架都认识到了独立包模式在依赖管理上的优势。
微软的 Semantic Kernel 采用了更加面向对象的设计风格。它使用 Kernel 作为中心注册表,所有插件(Plugin)和函数(Function)在 Kernel 中注册后才能使用。这种设计与 LangChain 的去中心化组合形成了鲜明对比。
Semantic Kernel 的优势在于类型安全更强、IDE 支持更好(尤其是在 C# 和 Java 中)。LangChain 的优势在于组合更灵活、管道更声明式。两者代表了 AI 框架设计的两种哲学:集中注册与去中心化组合。
Haystack 使用管道(Pipeline)作为核心抽象,组件(Component)通过输入输出端口连接。每个组件声明自己的输入和输出的类型和名称,管道在组装时验证连接的兼容性。
这种显式端口声明的设计比 LangChain 的 Runnable 泛型参数更加严格,能够在编译时(管道组装时)捕获更多类型错误。但它也更加冗长 -- 每个组件需要显式声明端口,而不是像 LangChain 那样通过泛型自动推导。
所有这些框架都认同几个核心设计原则:统一的组件接口、可组合的管道抽象、独立的集成包管理、以及某种形式的可观测性支持。差异主要在于抽象层次的选择:更通用还是更领域特定?更灵活还是更类型安全?更声明式还是更命令式?
没有绝对正确的答案。最佳选择取决于你的目标用户、技术栈和应用场景。LangChain 的设计选择 -- 通用、灵活、声明式 -- 适合快速原型和多样化的应用场景,特别是当你需要频繁实验不同的模型、工具和管道配置时。在类型安全和编译时检查更重要的场景下,Semantic Kernel 或 Haystack 的方案可能更合适,因为它们在编译阶段就能捕获更多的配置错误。
从这些对比中可以提炼出一个通用的框架设计原则:抽象层次的选择应该与目标用户的需求匹配。如果你的用户主要是应用开发者(希望快速构建产品),更高级别、更自动化的抽象会更受欢迎。如果你的用户主要是平台工程师(需要精确控制每个环节),更低级别、更显式的抽象会更合适。LangChain 试图通过"层次化的抽象"来兼顾两者 -- LCEL 提供高级声明式组合,Runnable 提供中级可编程接口,底层的 BaseChatModel 和 BaseTool 提供低级可覆盖的钩子。
| 模式 | 核心思想 | LangChain 实现 | 可迁移场景 |
|---|---|---|---|
| Runnable 协议 | 统一接口,一套 API 驱动所有组件 | invoke/stream/batch + pipe | 任何需要统一异构组件的框架 |
| 回调洋葱 | 无侵入式可观测性 | BaseCallbackHandler + get_child | 需要追踪调用链的系统 |
| Partner 解耦 | 核心稳定,集成独立 | langchain-core + Partner 包 | 大量第三方集成的框架 |
| 组合优于继承 | 管道声明优于子类重写 | LCEL | 操作符 | 流水线组合场景 |
| 纵深防御 | 多层独立安全检查 | 转义+白名单+验证器 | 反序列化不可信数据 |
mindmap
root((LangChain<br/>设计模式))
Runnable 协议
统一接口
12 种操作
类型参数化
默认实现减负
回调洋葱
start/end 成对
父子关系传播
handler 分发
无侵入式
Partner 解耦
核心稳定
独立打包
标准测试
映射迁移
LCEL 组合
管道操作符
声明式
自动获得能力
可视化
安全纵深
默认关闭
白名单
多层校验
转义防护
本章从 LangChain 的具体实现中提炼出五个核心设计模式。Runnable 协议模式解决了异构组件统一调用的问题。回调洋葱模型提供了无侵入式的可观测性。Partner 解耦架构管理了爆炸式增长的集成生态。LCEL 组合优于继承,让复杂管道的构建变得声明式和可组合。安全纵深防御为序列化系统提供了多层保护。
这些模式不是孤立的 -- 它们相互支撑。Runnable 协议是 LCEL 组合的基础,回调系统需要 Runnable 的统一生命周期钩子,Partner 解耦依赖 langchain-core 中稳定的 Runnable 抽象,安全系统保护了 Serializable(Runnable 的父类)的持久化。
如果说前十七章是"LangChain 是怎么做的",那本章要回答的是"为什么这样做,以及你可以怎样借鉴"。希望这些设计模式能为你构建自己的 AI 应用框架提供有价值的参考。
至此,我们对 LangChain 的源码之旅就告一段落了。从最底层的 Runnable 协议到最顶层的 Agent 执行循环,从消息系统到序列化安全,从单个工具到整个 Partner 生态 -- 这些源码中蕴含的工程智慧,远比 API 文档能告诉你的要丰富得多。理解了"为什么这样设计",你才能在框架之上,而非框架之内,构建真正优秀的 AI 应用。