第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 + 多包的结构既便于开发时的跨包调试,又保证了发布时的独立性。
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,运行时依赖极为克制:
# 源码文件: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 之上,提供高级应用模式:
# 源码文件: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 包,每个包封装一个第三方服务的集成:
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 的任何依赖。
graph TB
subgraph "应用代码"
APP["from langchain_openai import ChatOpenAI<br/>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 是我们在本书中花费最多时间的地方。让我们逐一认识它的子模块。
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,每一层都在前一层的基础上增加特定的能力。
classDiagram
class Runnable {
<<ABC>>
+name: str
+invoke(input, config) Output
+batch(inputs, config) list~Output~
+stream(input, config) Iterator~Output~
+ainvoke(input, config) Output
+abatch(inputs, config) list~Output~
+astream(input, config) AsyncIterator~Output~
+__or__(other) RunnableSequence
+__ror__(other) RunnableSequence
+pipe(*others) RunnableSequence
+pick(keys) RunnablePick
+assign(**kwargs) RunnableAssign
+with_config(config) RunnableBinding
+with_retry() RunnableRetry
+with_fallbacks() RunnableWithFallbacks
+get_graph() Graph
+input_schema: BaseModel
+output_schema: BaseModel
}
class Serializable {
<<ABC>>
+is_lc_serializable() bool
+get_lc_namespace() list
+lc_id() list
+to_json() dict
}
class RunnableSerializable {
+name: str
+configurable_fields(**kwargs)
+configurable_alternatives(which, **kwargs)
}
class RunnableSequence {
+first: Runnable
+middle: list~Runnable~
+last: Runnable
+steps: list~Runnable~
}
class RunnableParallel {
+steps__: Mapping~str, Runnable~
}
class RunnableLambda {
+func: Callable
+afunc: Callable
}
class RunnableBranch {
+branches: list~tuple~
+default: Runnable
}
Runnable <|-- RunnableLambda
Runnable <|-- RunnableGenerator
Serializable <|-- RunnableSerializable
Runnable <|-- RunnableSerializable
RunnableSerializable <|-- RunnableSequence
RunnableSerializable <|-- RunnableParallel
RunnableSerializable <|-- RunnableBranch
RunnableSerializable <|-- RunnableBindingBase
RunnableBindingBase <|-- RunnableBinding
这个类层次体现了几个重要的设计决策:
为什么 RunnableLambda 不继承 RunnableSerializable? 因为一个 Python 函数(lambda 或普通函数)在一般情况下是无法序列化的。将 RunnableLambda 直接继承自 Runnable 而非 RunnableSerializable,是对这个现实的诚实表达。如果一个 RunnableLambda 恰好可以序列化(例如使用了 @chain 装饰器的命名函数),框架不会阻止你,但也不会承诺这个能力。
为什么 RunnableSequence 把步骤分成 first、middle、last? 这是为了类型安全。通过这种分拆,RunnableSequence[Input, Output] 可以确保 first 的输入类型是 Input,last 的输出类型是 Output,而 middle 的类型可以是 Any。如果只用一个 list[Runnable],就无法在类型层面表达这个约束。
# 源码文件: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 的架构中流动。假设我们有如下代码:
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__ 方法:
# 源码文件: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:
# 源码文件: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,
)
此时内存中的对象结构:
RunnableSequence
first: RunnableLambda(lambda x: x + 1)
middle: []
last: RunnableLambda(lambda x: x * 2)
2.4.2 第二步:invoke 的入口
调用 chain.invoke(3) 进入 RunnableSequence.invoke:
# 源码文件: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 配置管理的核心。它做三件事:
# 源码文件: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
sequenceDiagram
participant User as 用户代码
participant Seq as RunnableSequence
participant Config as ensure_config
participant CB as CallbackManager
participant Step1 as Step 1 (add_one)
participant Step2 as Step 2 (mul_two)
participant Ctx as ContextVar
User->>Seq: chain.invoke(3)
Seq->>Config: ensure_config(None)
Config->>Ctx: var_child_runnable_config.get()
Config-->>Seq: config{tags:[], metadata:{}, ...}
Seq->>CB: on_chain_start(input=3)
CB-->>Seq: run_manager
Seq->>Seq: patch_config(callbacks=child)
Seq->>Ctx: set_config_context(config)
Seq->>Step1: step.invoke(3, config)
Step1-->>Seq: 4
Seq->>Seq: patch_config(callbacks=child)
Seq->>Ctx: set_config_context(config)
Seq->>Step2: step.invoke(4, config)
Step2-->>Seq: 8
Seq->>CB: on_chain_end(8)
Seq-->>User: 8
2.4.6 第六步:RunnableLambda.invoke 的内部
当执行到 step.invoke(3, config) 时,进入 RunnableLambda.invoke:
# 源码文件: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 的结构
# 源码文件: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 提供了三个核心的配置操作函数:
graph TD
subgraph "ensure_config"
E1["创建默认配置"] --> E2["从 ContextVar 继承"]
E2 --> E3["用显式参数覆盖"]
E3 --> E4["返回完整配置"]
end
subgraph "patch_config"
P1["接收现有配置"] --> P2["替换指定字段<br/>(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 的深意
# 源码文件: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。
# 所有修饰方法都返回新的 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")
)
graph LR
R["原始 Runnable"]
R -->|".with_retry()"| R1["RunnableRetry<br/>包装原始 Runnable"]
R -->|".with_fallbacks()"| R2["RunnableWithFallbacks<br/>包装原始 Runnable"]
R -->|".with_config()"| R3["RunnableBinding<br/>包装原始 Runnable"]
R -->|".configurable_fields()"| R4["RunnableConfigurableFields<br/>包装原始 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:
# 源码文件: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 方法:
# 源码文件: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——理解它们各自的设计动机和实现细节。