第2章 架构总览
本书章节导航
- 前言
- 第1章 为什么需要理解 LangChain
- 第2章 架构总览 (当前)
- 第3章 Runnable 与 LCEL 表达式语言
- 第4章 消息系统与多模态
- 第5章 语言模型抽象层
- 第6章 提示词模板引擎
- 第7章 输出解析与结构化输出
- 第8章 工具系统
- 第9章 文档加载与文本分割
- 第10章 向量存储与检索器
- 第11章 Chain 组合模式
- 第12章 回调与可观测性
- 第13章 记忆与会话管理
- 第14章 Agent 架构与执行循环
- 第15章 工具调用与 Agent 模式
- 第16章 序列化与配置系统
- 第17章 Partner 集成架构
- 第18章 设计模式与架构决策
本章基于 LangChain 1.0.3 / langchain-core 1.2.26 源码分析。源码路径:
libs/目录。
理解一个框架的架构,最好的方式不是从概念图开始,而是从一次真实的调用开始。当你写下 chain.invoke("Hello") 并按下回车键时,数据在 LangChain 的哪些层之间流动?哪些对象被创建和销毁?回调事件在什么时机被触发?
本章将回答这些问题。我们首先俯瞰 LangChain 的三层包架构(langchain-core、langchain、Partners),然后逐目录解析 langchain-core 的内部结构,最后通过跟踪一次完整的 chain.invoke() 调用,将抽象的架构图变成具体的执行流。
:::tip 本章要点
- 三层包架构:langchain-core 是基石,langchain 是组合层,Partners 是集成层,三者的依赖关系严格单向
- langchain-core 目录结构:17 个子模块的职责划分,核心抽象的层级关系
- Runnable 类层次 :从
RunnableABC 到RunnableSerializable再到具体组件的继承链 - chain.invoke() 的完整旅程:从用户调用到 CallbackManager、ContextVar、线程池的底层执行
- 配置传播机制 :
RunnableConfig如何通过ensure_config、patch_config、set_config_context在组件间流转 :::
2.1 三层包架构
LangChain 的代码分布在一个 monorepo 中,libs/ 目录下包含多个独立的 Python 包。这种 monorepo + 多包的结构既便于开发时的跨包调试,又保证了发布时的独立性。
bash
libs/
core/ # langchain-core 1.2.26
langchain_core/
runnables/ # Runnable 协议与 LCEL
messages/ # 消息类型系统
language_models/ # LLM/ChatModel 抽象
prompts/ # Prompt 模板
output_parsers/ # 输出解析器
callbacks/ # 回调与追踪
tools/ # 工具接口
documents/ # 文档类型
...
langchain/ # langchain-classic 1.0.3
langchain_classic/
chains/ # 经典 Chain 实现
agents/ # Agent 实现
memory/ # Memory 实现
retrievers/ # 高级 Retriever
...
partners/ # Partner 集成包
openai/ # langchain-openai
anthropic/ # langchain-anthropic
chroma/ # langchain-chroma
ollama/ # langchain-ollama
...
text-splitters/ # langchain-text-splitters
standard-tests/ # 标准测试套件
2.1.1 langchain-core:基石层
langchain-core 是整个生态系统的地基。它的设计目标是:用最少的外部依赖,定义最完整的抽象接口。
查看其 pyproject.toml,运行时依赖极为克制:
toml
# 源码文件:libs/core/pyproject.toml
[project]
name = "langchain-core"
version = "1.2.26"
requires-python = ">=3.10.0,<4.0.0"
dependencies = [
"langsmith>=0.3.45,<1.0.0",
"tenacity!=8.4.0,>=8.1.0,<10.0.0",
"jsonpatch>=1.33.0,<2.0.0",
"PyYAML>=5.3.0,<7.0.0",
"pydantic>=2.7.4,<3.0.0",
"typing-extensions>=4.7",
"packaging>=23.2,<25.0",
]
只有 7 个运行时依赖,且每一个都有明确的理由:pydantic 用于数据建模和校验,langsmith 用于追踪,tenacity 用于重试,jsonpatch 用于流式日志,PyYAML 用于配置,typing-extensions 用于向后兼容的类型注解,packaging 用于版本比较。
2.1.2 langchain:组合层
langchain(在最新版本中命名为 langchain-classic)构建在 langchain-core 之上,提供高级应用模式:
toml
# 源码文件:libs/langchain/pyproject.toml
[project]
name = "langchain-classic"
version = "1.0.3"
dependencies = [
"langchain-core>=1.2.19,<2.0.0",
"langchain-text-splitters>=1.1.1,<2.0.0",
"langsmith>=0.1.17,<1.0.0",
"pydantic>=2.7.4,<3.0.0",
"SQLAlchemy>=1.4.0,<3.0.0",
...
]
关键观察:langchain-classic 依赖 langchain-core,但 langchain-core 绝不依赖 langchain-classic。这种严格的单向依赖是架构健康的核心保证。
2.1.3 Partner 包:集成层
libs/partners/ 目录下有 15+ 个独立的 Partner 包,每个包封装一个第三方服务的集成:
bash
partners/
openai/ # ChatOpenAI, OpenAIEmbeddings
anthropic/ # ChatAnthropic
chroma/ # Chroma vector store
ollama/ # ChatOllama
fireworks/ # ChatFireworks
groq/ # ChatGroq
mistralai/ # ChatMistralAI
huggingface/ # HuggingFace 模型和嵌入
...
每个 Partner 包只依赖 langchain-core,不依赖 langchain-classic 或其他 Partner 包。这意味着你可以只安装 langchain-openai 而不拉取 langchain-anthropic 的任何依赖。
from langchain_core.prompts import ChatPromptTemplate"] end subgraph "Partner 包" direction LR P1["langchain-openai"] P2["langchain-anthropic"] P3["langchain-chroma"] P4["langchain-ollama"] end subgraph "langchain-classic 1.0.3" LC["chains / agents / memory / retrievers"] end subgraph "langchain-core 1.2.26" CORE["Runnable / Messages / Prompts / LMs / Callbacks / Tools / Documents"] end APP --> P1 APP --> LC APP --> CORE P1 --> CORE P2 --> CORE P3 --> CORE P4 --> CORE LC --> CORE P1 -.->|不依赖| P2 P1 -.->|不依赖| LC style CORE fill:#4CAF50,color:#fff style LC fill:#FF9800,color:#fff style P1 fill:#2196F3,color:#fff style P2 fill:#2196F3,color:#fff style P3 fill:#2196F3,color:#fff style P4 fill:#2196F3,color:#fff
2.1.4 设计决策:为什么分成三层
这个分层不是一开始就有的。早期的 LangChain 是一个单体包,所有代码都在 langchain 中。当生态系统快速增长后,问题开始显现:
- 依赖爆炸 :安装
langchain就意味着安装 OpenAI SDK、Anthropic SDK、ChromaDB 等全部依赖 - 版本耦合 :OpenAI SDK 的一次 breaking change 会导致整个
langchain发版 - 贡献瓶颈:所有 PR 都汇聚到一个仓库,审核压力巨大
分层架构解决了这些问题:
langchain-core极少变动,提供稳定的接口契约- Partner 包可以独立发版,跟随各自上游的节奏
- 开发者只安装需要的包,依赖树干净清晰
2.2 langchain-core 目录结构导航
langchain-core 是我们在本书中花费最多时间的地方。让我们逐一认识它的子模块。
csharp
langchain_core/
runnables/ # [核心] Runnable 协议、LCEL 所有组合原语
base.py # ~6200 行,Runnable/RunnableSequence/RunnableParallel/RunnableLambda
config.py # RunnableConfig、ensure_config、patch_config
branch.py # RunnableBranch 条件分支
passthrough.py # RunnablePassthrough/RunnableAssign/RunnablePick
configurable.py # 运行时可配置的 Runnable
fallbacks.py # RunnableWithFallbacks 降级策略
history.py # RunnableWithMessageHistory 对话历史
retry.py # 重试逻辑
router.py # 路由器
graph.py # 计算图表示
graph_mermaid.py # Mermaid 图生成
utils.py # 工具函数、类型定义
schema.py # StreamEvent 等 schema
messages/ # 消息类型系统
language_models/ # BaseLLM、BaseChatModel 等抽象接口
prompts/ # PromptTemplate、ChatPromptTemplate 等
output_parsers/ # StrOutputParser、JsonOutputParser 等
callbacks/ # CallbackManager、BaseCallbackHandler
tracers/ # LangSmith 追踪器、ConsoleCallbackHandler
tools/ # BaseTool 接口
documents/ # Document 数据类型
load/ # 序列化/反序列化(Serializable 基类)
embeddings/ # Embeddings 抽象接口
vectorstores/ # VectorStore 抽象接口
indexing/ # 索引 API
utils/ # 通用工具函数
其中 runnables/ 目录是最核心的,它包含了 LangChain 的"操作系统内核"------所有其他模块中的组件最终都要实现 Runnable 协议。
2.3 核心抽象层级
LangChain 的类层次设计遵循"层层添加能力"的原则。从最抽象的 Runnable 到最具体的 ChatOpenAI,每一层都在前一层的基础上增加特定的能力。
这个类层次体现了几个重要的设计决策:
为什么 RunnableLambda 不继承 RunnableSerializable? 因为一个 Python 函数(lambda 或普通函数)在一般情况下是无法序列化的。将 RunnableLambda 直接继承自 Runnable 而非 RunnableSerializable,是对这个现实的诚实表达。如果一个 RunnableLambda 恰好可以序列化(例如使用了 @chain 装饰器的命名函数),框架不会阻止你,但也不会承诺这个能力。
为什么 RunnableSequence 把步骤分成 first、middle、last? 这是为了类型安全。通过这种分拆,RunnableSequence[Input, Output] 可以确保 first 的输入类型是 Input,last 的输出类型是 Output,而 middle 的类型可以是 Any。如果只用一个 list[Runnable],就无法在类型层面表达这个约束。
python
# 源码文件:libs/core/langchain_core/runnables/base.py
class RunnableSequence(RunnableSerializable[Input, Output]):
first: Runnable[Input, Any] # 输入类型由此决定
middle: list[Runnable[Any, Any]] = Field(default_factory=list)
last: Runnable[Any, Output] # 输出类型由此决定
@property
def steps(self) -> list[Runnable[Any, Any]]:
return [self.first, *self.middle, self.last]
2.4 跟踪一次完整的 chain.invoke()
现在让我们跟踪一次完整的调用,观察数据如何在 LangChain 的架构中流动。假设我们有如下代码:
python
from langchain_core.runnables import RunnableLambda
add_one = RunnableLambda(lambda x: x + 1)
mul_two = RunnableLambda(lambda x: x * 2)
chain = add_one | mul_two # 创建 RunnableSequence
result = chain.invoke(3) # 期望结果: (3 + 1) * 2 = 8
2.4.1 第一步:构建 RunnableSequence
当 Python 执行 add_one | mul_two 时,调用的是 Runnable.__or__ 方法:
python
# 源码文件:libs/core/langchain_core/runnables/base.py (第618行)
def __or__(self, other):
return RunnableSequence(self, coerce_to_runnable(other))
coerce_to_runnable 会将非 Runnable 对象转换为 Runnable。在这里 mul_two 已经是 RunnableLambda,所以直接返回。
RunnableSequence.__init__ 接收可变参数 *steps,将它们展平(如果某个 step 本身是 RunnableSequence,会被解包),然后分配到 first、middle、last:
python
# 源码文件:libs/core/langchain_core/runnables/base.py (第2911行)
def __init__(self, *steps: RunnableLike, name: str | None = None, ...) -> None:
steps_flat: list[Runnable] = []
for step in steps:
if isinstance(step, RunnableSequence):
steps_flat.extend(step.steps) # 展平嵌套的 Sequence
else:
steps_flat.append(coerce_to_runnable(step))
super().__init__(
first=steps_flat[0],
middle=list(steps_flat[1:-1]),
last=steps_flat[-1],
name=name,
)
此时内存中的对象结构:
yaml
RunnableSequence
first: RunnableLambda(lambda x: x + 1)
middle: []
last: RunnableLambda(lambda x: x * 2)
2.4.2 第二步:invoke 的入口
调用 chain.invoke(3) 进入 RunnableSequence.invoke:
python
# 源码文件:libs/core/langchain_core/runnables/base.py (第3131行)
def invoke(self, input: Input, config: RunnableConfig | None = None, **kwargs) -> Output:
# 1. 初始化配置
config = ensure_config(config)
# 2. 配置回调管理器
callback_manager = get_callback_manager_for_config(config)
# 3. 启动根级追踪
run_manager = callback_manager.on_chain_start(
None, input,
name=config.get("run_name") or self.get_name(),
run_id=config.pop("run_id", None),
)
input_ = input
# 4. 依次执行每个步骤
try:
for i, step in enumerate(self.steps):
config = patch_config(
config,
callbacks=run_manager.get_child(f"seq:step:{i + 1}")
)
with set_config_context(config) as context:
if i == 0:
input_ = context.run(step.invoke, input_, config, **kwargs)
else:
input_ = context.run(step.invoke, input_, config)
except BaseException as e:
run_manager.on_chain_error(e) # 5a. 错误上报
raise
else:
run_manager.on_chain_end(input_) # 5b. 成功上报
return cast("Output", input_)
2.4.3 第三步:ensure_config 的配置初始化
ensure_config 是 LangChain 配置管理的核心。它做三件事:
python
# 源码文件:libs/core/langchain_core/runnables/config.py (第225行)
def ensure_config(config: RunnableConfig | None = None) -> RunnableConfig:
# 1. 创建默认配置
empty = RunnableConfig(
tags=[], metadata={}, callbacks=None,
recursion_limit=DEFAULT_RECURSION_LIMIT, # 25
configurable={},
)
# 2. 从 ContextVar 继承父级配置
if var_config := var_child_runnable_config.get():
empty.update({k: v.copy() if k in COPIABLE_KEYS else v ...})
# 3. 用显式传入的配置覆盖
if config is not None:
empty.update({k: v ...})
return empty
这里的 var_child_runnable_config 是一个 ContextVar,它使得嵌套调用中的子 Runnable 能自动继承父 Runnable 的配置(如 tags、metadata、callbacks),而无需开发者手动传递。
2.4.4 第四步:CallbackManager 与追踪
get_callback_manager_for_config 从配置中提取回调信息,创建一个 CallbackManager。on_chain_start 通知所有注册的回调处理器"一个新的 chain 执行开始了",并返回一个 RunManager,用于后续的子步骤追踪。
2.4.5 第五步:逐步执行
对于每个步骤,框架做了三件关键的事:
patch_config:用run_manager.get_child()创建一个子回调管理器,确保子步骤的追踪事件能正确嵌套在父步骤之下set_config_context:将当前配置写入ContextVar,然后拷贝当前上下文(copy_context()),在新的上下文中执行步骤context.run(step.invoke, ...):在隔离的上下文中执行步骤的invoke
2.4.6 第六步:RunnableLambda.invoke 的内部
当执行到 step.invoke(3, config) 时,进入 RunnableLambda.invoke:
python
# 源码文件:libs/core/langchain_core/runnables/base.py (第4997行)
def invoke(self, input, config=None, **kwargs):
if hasattr(self, "func"):
return self._call_with_config(
self._invoke,
input,
ensure_config(config),
**kwargs,
)
raise TypeError("Cannot invoke a coroutine function synchronously.")
_call_with_config 是所有 Runnable 共享的模板方法,它负责:
- 启动子级的回调追踪(
on_chain_start) - 在正确的上下文中调用实际的函数
- 处理错误并上报(
on_chain_error) - 上报成功结果(
on_chain_end)
最终,self.func(3) 被调用,返回 4。
2.5 配置传播机制深入
RunnableConfig 是 LangChain 的"中枢神经系统"。理解它的传播机制对于掌握整个框架至关重要。
2.5.1 RunnableConfig 的结构
python
# 源码文件:libs/core/langchain_core/runnables/config.py (第49行)
class RunnableConfig(TypedDict, total=False):
tags: list[str] # 标签,用于过滤和追踪
metadata: dict[str, Any] # 元数据,传递给回调处理器
callbacks: Callbacks # 回调处理器链
run_name: str # 当前运行的名称
max_concurrency: int | None # 最大并发数
recursion_limit: int # 递归深度限制(默认25)
configurable: dict[str, Any] # 运行时可配置字段
run_id: uuid.UUID | None # 唯一运行标识
total=False 意味着所有字段都是可选的。这使得配置可以被"部分创建、逐步合并"------一个组件可以只设置 tags,另一个组件可以只设置 metadata,merge_configs 会将它们正确合并。
2.5.2 三种配置操作
LangChain 提供了三个核心的配置操作函数:
(callbacks, recursion_limit等)"] P2 --> P3["返回新配置"] end subgraph "set_config_context" S1["将配置写入 ContextVar"] --> S2["拷贝当前上下文"] S2 --> S3["yield 上下文"] S3 --> S4["清理 ContextVar"] end
ensure_config:确保配置完整。如果传入None,返回默认配置;同时从ContextVar继承父级配置patch_config:修补配置。在现有配置基础上替换特定字段(如替换callbacks为子级回调管理器)set_config_context:设置上下文。将配置写入ContextVar,创建隔离的执行上下文
2.5.3 COPIABLE_KEYS 的深意
python
# 源码文件:libs/core/langchain_core/runnables/config.py
COPIABLE_KEYS = ["tags", "metadata", "callbacks", "configurable"]
当配置从 ContextVar 或显式参数中继承时,COPIABLE_KEYS 中的字段会被 copy() 而非直接引用。这是为了防止"共享引用"问题------如果子 Runnable 向 tags 列表中添加元素,不应该影响父 Runnable 的 tags。这是一个容易被忽视但极其重要的细节。
2.6 Runnable 的通用方法体系
Runnable 基类不仅定义了核心的 invoke/batch/stream 协议,还提供了一整套用于修饰和增强的"方法修饰器"。这些方法遵循一个统一的模式:它们不修改原 Runnable,而是返回一个新的包装 Runnable。
python
# 所有修饰方法都返回新的 Runnable,不修改原始对象
chain = prompt | model | parser
# with_retry: 返回 RunnableRetry 包装
chain_with_retry = chain.with_retry(stop_after_attempt=3)
# with_fallbacks: 返回 RunnableWithFallbacks 包装
chain_with_fallback = chain.with_fallbacks([fallback_chain])
# with_config: 返回 RunnableBinding 包装
chain_with_config = chain.with_config({"tags": ["production"]})
# configurable_fields: 返回 RunnableConfigurableFields 包装
chain_configurable = model.configurable_fields(
temperature=ConfigurableField(id="temp")
)
包装原始 Runnable"] R -->|".with_fallbacks()"| R2["RunnableWithFallbacks
包装原始 Runnable"] R -->|".with_config()"| R3["RunnableBinding
包装原始 Runnable"] R -->|".configurable_fields()"| R4["RunnableConfigurableFields
包装原始 Runnable"] R -->|".pick(keys)"| R5["原始 Runnable | RunnablePick"] R -->|".assign(**kw)"| R6["原始 Runnable | RunnableAssign"] style R fill:#e8f5e9 style R1 fill:#fff3e0 style R2 fill:#fff3e0 style R3 fill:#fff3e0 style R4 fill:#fff3e0 style R5 fill:#e3f2fd style R6 fill:#e3f2fd
这种"不可变包装"的设计模式(装饰器模式)使得:
- 原始 Runnable 不受影响,可以被多次修饰生成不同变体
- 每个包装层都可以独立测试
- 包装是可组合的:
chain.with_retry().with_fallbacks([...])是合法的
2.7 batch 与并行执行
Runnable 基类提供了 batch 的默认实现,它使用线程池并行执行多个 invoke:
python
# 源码文件:libs/core/langchain_core/runnables/base.py (第867行)
def batch(self, inputs, config=None, *, return_exceptions=False, **kwargs):
if not inputs:
return []
configs = get_config_list(config, len(inputs))
def invoke(input_, config):
if return_exceptions:
try:
return self.invoke(input_, config, **kwargs)
except Exception as e:
return e
else:
return self.invoke(input_, config, **kwargs)
# 单个输入时不使用线程池
if len(inputs) == 1:
return [invoke(inputs[0], configs[0])]
with get_executor_for_config(configs[0]) as executor:
return list(executor.map(invoke, inputs, configs))
get_executor_for_config 会根据 config["max_concurrency"] 创建一个 ThreadPoolExecutor。如果 max_concurrency 未指定,使用 Python 默认的线程数(通常是 CPU 核数 + 4)。
而 RunnableSequence 覆写了 batch,它的实现更加精妙------它对序列中的每个步骤调用 batch,而不是对整个序列调用多次 invoke。这意味着如果某个步骤(如 LLM 调用)有原生的批量 API,它可以利用这个 API 来提高效率。
2.8 stream 与流式执行
流式执行是 LangChain 最复杂也最精妙的部分之一。RunnableSequence 的流式执行依赖于 transform 方法:
python
# 源码文件:libs/core/langchain_core/runnables/base.py (第3465行)
def _transform(self, inputs, run_manager, config, **kwargs):
steps = [self.first, *self.middle, self.last]
# 将每个步骤的 transform 串联成管道
final_pipeline = cast("Iterator[Output]", inputs)
for idx, step in enumerate(steps):
config = patch_config(
config, callbacks=run_manager.get_child(f"seq:step:{idx + 1}")
)
if idx == 0:
final_pipeline = step.transform(final_pipeline, config, **kwargs)
else:
final_pipeline = step.transform(final_pipeline, config)
yield from final_pipeline
核心思想是:不是等前一步完全执行完再开始下一步,而是将前一步的输出流直接接入下一步的输入流。 这使得支持 transform 的步骤(如 LLM 的流式输出)可以一边产生 token 一边被下一步处理,实现真正的端到端流式。
对于不支持原生 transform 的步骤(如 RunnableLambda),默认实现会先累积全部输入再产出输出------这就是为什么 LangChain 文档建议在需要流式的场景中使用 RunnableGenerator 而非 RunnableLambda。
2.9 设计决策总结
TypedDict vs Pydantic Model for Config
LangChain 选择用 TypedDict 而非 Pydantic Model 定义 RunnableConfig。这是因为 TypedDict 就是一个普通的 dict,可以用 dict.update 来合并,性能开销极小。而 Pydantic Model 的实例化和验证在高频调用路径上会带来可感知的性能损失------RunnableConfig 在每次 invoke 中都会被创建和传递多次。
ContextVar 的选择
使用 ContextVar 而非线程本地存储(threading.local)是一个面向 async 友好的决策。ContextVar 在 asyncio.Task 之间正确传播,而 threading.local 不会。这使得 LangChain 在异步场景下的配置传播无缝工作。
递归限制
默认递归限制 DEFAULT_RECURSION_LIMIT = 25 是为了防止无限递归的 Agent 循环。当一个 Agent 反复调用工具而不收敛时,这个限制会自动终止执行。这是一个务实的安全措施。
2.10 小结
本章从三个层面建立了对 LangChain 架构的全面理解。
首先,我们认识了三层包架构:langchain-core 是最小化依赖的基石层,定义了所有核心抽象;langchain(langchain-classic)是组合层,提供 Chains、Agents 等高级模式;Partner 包是集成层,每个包独立封装一个第三方服务。三者之间的依赖严格单向。
然后,我们深入了 langchain-core 的目录结构和类层次,理解了 Runnable -> RunnableSerializable -> 具体组件的继承链,以及为什么 RunnableLambda 和 RunnableGenerator 直接继承自 Runnable 而非 RunnableSerializable。
最后,我们跟踪了一次完整的 chain.invoke() 调用,从 ensure_config 的配置初始化,到 CallbackManager 的追踪启动,到 patch_config + set_config_context 的上下文隔离,再到每个步骤的实际执行。这个过程揭示了 LangChain 如何将简洁的用户 API(一行 invoke 调用)转化为复杂的内部编排。
下一章,我们将深入 Runnable 协议和 LCEL 的每一个组合原语------RunnableSequence、RunnableParallel、RunnableLambda、RunnableBranch、RunnablePassthrough------理解它们各自的设计动机和实现细节。