引言:为什么 Agent 需要"有状态"的执行引擎?
当你用 LangChain 写第一个 Agent 时,代码可能长这样:
agent = initialize_agent(tools, llm, agent="zero-shot-react-description")
result = agent.run("查询北京天气并发送到钉钉")
简单、直观——直到你需要:
- 在工具调用失败后断点续跑,而非从头开始
- 让人类在关键节点介入审批,而非被动等待最终结果
- 调试时回溯任意步骤的状态,而非在黑盒中猜错
传统 DAG(有向无环图)执行引擎假设任务是无状态、幂等的,但 Agent 的本质是有状态的对话流:每一步都依赖历史上下文,每个工具调用都可能改变世界状态。LangGraph 的核心洞察是:把 Agent 建模为状态机,用图计算引擎保证执行的确定性与可恢复性。
本文基于 LangGraph 0.4.x 源码,深入其 Pregel 执行引擎的实现原理,揭示它如何用 BSP(Bulk Synchronous Parallel)模型解决 Agent 工程化的核心痛点。
第一章:架构全景——三层模型与数据流向
LangGraph 的架构可划分为清晰的三个层次:
┌─────────────────────────────────────────────────────────────────┐
│ SDK Layer (开发者接口) │
│ StateGraph.add_node() / add_edge() / compile() │
├─────────────────────────────────────────────────────────────────┤
│ Runtime Layer (Pregel 引擎) │
│ PregelLoop → Channel Manager → Node Executor → Checkpointing │
├─────────────────────────────────────────────────────────────────┤
│ Persistence Layer (状态持久) │
│ Checkpoint Saver (Memory/Postgres/Redis/SQLite) │
└─────────────────────────────────────────────────────────────────┘
1.1 核心抽象:Channels、Nodes 与 Edges
LangGraph 的图模型借鉴了 Google Pregel 和 Apache Beam 的设计,核心抽象极其精简:
# libs/langgraph/langgraph/graph/state.py
class StateGraph(Graph):
def add_node(self, node: str, action: Runnable) -> "StateGraph":
# 节点是订阅特定 channels 的函数
self.nodes[node] = action
return self
def add_edge(self, start: str, end: str) -> "StateGraph":
# 边定义执行顺序,但真正的数据流通过 channels
self.edges[start].add(end)
return self
Why this design? Channels 作为命名数据容器解耦了节点间的直接依赖。节点只订阅自己关心的 channels,不感知上游是谁——这让图的重组和并行执行变得简单。
What if alternative? 如果采用直接函数调用链(如 LangChain 的 LLMChain),节点间强耦合,难以实现部分重跑和并行分支。Channels 的引入让数据流与执行流分离,是支持断点续跑的关键。
1.2 系统架构图
graph TB
subgraph SDK["SDK Layer"]
SG[StateGraph<br/>图定义]
SG -->|compile| PG[Pregel<br/>执行图]
end
subgraph Runtime["Runtime Layer"]
PL[PregelLoop<br/>主执行循环]
CM[ChannelManager<br/>通道管理]
NE[NodeExecutor<br/>节点执行器]
CK[CheckpointSaver<br/>状态快照]
PL -->|read/write| CM
PL -->|submit| NE
PL -->|save/load| CK
end
subgraph Persistence["Persistence Layer"]
PS[(Postgres/Redis<br/>SQLite/Memory)]
end
PG --> PL
CK --> PS
style SDK fill:#e1f5fe
style Runtime fill:#fff3e0
style Persistence fill:#e8f5e9
第二章:执行引擎深潜——PregelLoop 的 BSP 实现
Pregel 是 Google 提出的图计算模型,核心思想是批量同步并行(BSP):每一轮超步(superstep)中,所有节点并行执行,然后通过屏障(barrier)同步状态。LangGraph 将其适配为 Agent 执行引擎。
2.1 核心执行循环
# libs/langgraph/langgraph/pregel/__init__.py
class PregelLoop:
async def ainvoke(self, input: dict, config: RunnableConfig):
# 1. 加载最新 checkpoint(O(1) 时间复杂度)
checkpoint = await self.checkpointer.aget(config)
# 2. BSP 执行循环
while True:
# 2.1 选择可执行节点(基于 channel 版本号)
tasks = self._select_tasks(checkpoint)
if not tasks:
break # 无待执行节点,结束
# 2.2 并行执行(每个节点获得状态副本)
results = await asyncio.gather(*[
self._execute_node(task, checkpoint.copy())
for task in tasks
])
# 2.3 确定性合并更新(按节点名排序)
checkpoint = self._merge_updates(checkpoint, results)
# 2.4 持久化 checkpoint
await self.checkpointer.aput(config, checkpoint)
return checkpoint
Why this design?
每个节点获得独立的状态副本(checkpoint.copy()),并行执行后通过确定性合并(按节点名排序)整合结果。这消除了数据竞争,让执行结果与并行度无关——同一图结构无论串行还是并行执行,最终状态一致。
What if alternative? 如果采用共享内存模型,节点间可能产生竞态条件,需要复杂的锁机制。LangGraph 的"副本+合并"策略用空间换确定性,更适合 Agent 这种需要可重现执行轨迹的场景。
2.2 调用栈级 Data Flow 时序图
以一次典型的 ReAct Agent 执行(用户输入 → LLM 推理 → 工具调用 → 结果返回)为例:
sequenceDiagram
participant User as 用户代码
participant SDK as StateGraph
participant PL as PregelLoop
participant CM as ChannelManager
participant NE as NodeExecutor
participant LLM as LLM 客户端
participant Tool as 工具函数
participant CK as CheckpointSaver
User->>SDK: invoke({"messages": [user_input]})
SDK->>PL: 编译后的 Pregel 图
%% Superstep 1: LLM 推理
PL->>CK: aget(config) 加载 checkpoint
PL->>PL: _select_tasks() 选择 agent 节点
PL->>NE: submit(agent_node, state_copy)
NE->>CM: 读取 messages channel
NE->>LLM: async chat.completions.create()
LLM-->>NE: AIMessage(content="需要查询天气")
NE->>CM: 写入 messages channel
NE-->>PL: 返回更新
PL->>PL: _merge_updates() 合并
PL->>CK: aput(config, checkpoint_v1)
%% Superstep 2: 工具执行
PL->>PL: _select_tasks() 选择 tools 节点
PL->>NE: submit(tools_node, state_copy)
NE->>CM: 读取 messages channel
NE->>Tool: 调用 get_weather("北京")
Tool-->>NE: {"temperature": 25}
NE->>CM: 写入 messages channel
NE-->>PL: 返回更新
PL->>PL: _merge_updates() 合并
PL->>CK: aput(config, checkpoint_v2)
%% Superstep 3: 最终回复
PL->>PL: _select_tasks() 选择 agent 节点
PL->>NE: submit(agent_node, state_copy)
NE->>LLM: async chat.completions.create()
LLM-->>NE: AIMessage(content="北京今天25度")
NE-->>PL: 返回更新
PL->>PL: _merge_updates() 合并
PL->>CK: aput(config, checkpoint_v3)
PL->>PL: _select_tasks() 无待执行节点
PL-->>User: 返回最终状态
2.3 状态版本与增量更新
Channels 通过版本号机制实现增量更新:
# libs/langgraph/langgraph/channels/base.py
class Channel(Generic[V]:
def update(self, values: Sequence[V]) -> None:
"""应用增量更新,版本号递增"""
for value in values:
self.value = self._apply(self.value, value)
self.version += 1
def get(self) -> V:
"""读取当前值"""
return self.value
每个 checkpoint 只保存状态增量而非全量历史,这让历史查询保持 O(1) 时间复杂度——无论执行多少步,加载最新 checkpoint 的耗时恒定。
第三章:关键机制源码剖析
3.1 人机协作:Interrupt 与 Resume
LangGraph 的 interrupt() 函数让 Agent 能在任意节点暂停,等待人类输入:
# libs/langgraph/langgraph/types.py
def interrupt(value: Any) -> Any:
"""暂停执行,等待外部恢复"""
raise GraphInterrupt(value)
# libs/langgraph/langgraph/pregel/loop.py
class PregelLoop:
async def ainvoke(self, input, config):
# ... 执行循环
try:
result = await self._execute_node(task, state)
except GraphInterrupt as e:
# 保存中断状态,等待 resume
await self._save_interrupt(config, task, e.value)
raise
async def aresume(self, input, config):
"""从断点恢复执行"""
interrupt_state = await self._load_interrupt(config)
# 继续执行被中断的节点
return await self._resume_from(interrupt_state)
Why this design?
异常驱动的中断机制让业务代码保持纯净——开发者只需在需要暂停的地方调用 interrupt(),无需处理复杂的回调或状态机转换。框架层负责捕获异常、保存上下文、提供恢复入口。
What if alternative? 如果采用显式的状态机模式(如定义 INTERRUPTED 状态),每个节点都需要检查状态并决定行为,代码侵入性强。LangGraph 的"异常即暂停"设计让正常逻辑与中断逻辑解耦。
3.2 流式输出:六模式 Streaming
LangGraph 支持六种流式模式,满足不同调试和监控需求:
# libs/langgraph/langgraph/pregel/__init__.py
class Pregel:
async def astream(self, input, config, stream_mode: StreamMode):
if stream_mode == "values":
# 输出完整状态快照
yield checkpoint
elif stream_mode == "updates":
# 仅输出增量更新
yield delta
elif stream_mode == "messages":
# 输出 LLM 消息流(token 级)
async for token in llm.astream():
yield token
elif stream_mode == "debug":
# 输出执行元数据
yield {"node": node_name, "duration_ms": 123}
流式实现基于异步生成器,与执行引擎共享事件循环,无需额外的消息队列或 WebSocket 层。
3.3 容错与重试机制
# libs/langgraph/langgraph/pregel/retry.py
class RetryPolicy:
def __init__(self, max_attempts: int = 3, backoff: float = 1.0):
self.max_attempts = max_attempts
self.backoff = backoff
async def execute_with_retry(node: Callable, state: dict, policy: RetryPolicy):
for attempt in range(policy.max_attempts):
try:
return await node(state)
except RetryableError as e:
if attempt == policy.max_attempts - 1:
raise
await asyncio.sleep(policy.backoff * (2 ** attempt))
节点级别的重试策略与 checkpoint 机制配合:重试从当前 superstep 开始,而非整个图重新执行,大幅减少 LLM API 调用成本。
第四章:工程批判与生产实践
4.1 架构优势
| 维度 | LangGraph 方案 | 传统方案 | 优势 |
|---|---|---|---|
| 确定性 | 副本+合并,版本号排序 | 共享内存+锁 | 执行结果可重现,便于调试 |
| 可恢复性 | Checkpoint 持久化 | 内存状态 | 进程崩溃后可断点续跑 |
| 并发控制 | BSP 屏障同步 | 回调地狱 | 代码直观,避免竞态 |
| 扩展性 | Channels 解耦 | 直接函数调用 | 节点可独立测试、复用 |
4.2 已知瓶颈与优化建议
瓶颈 1:长上下文状态膨胀
当 messages channel 积累多轮对话后,每次 checkpoint 序列化/反序列化开销增大。
# 优化:使用截断策略或摘要节点
class State(TypedDict):
messages: Annotated[list, add_messages]
summary: str # 历史摘要
def summarize_node(state: State):
if len(state["messages"]) > 10:
summary = llm.invoke(f"总结: {state['messages'][:-5]}")
return {"summary": summary, "messages": state["messages"][-5:]}
瓶颈 2:工具调用延迟累积
每个 superstep 都要等待最慢的节点完成,串行工具调用会拉长整体延迟。
# 优化:并行工具调用
class State(TypedDict):
results: Annotated[list, operator.add]
# 多个工具节点订阅同一触发条件,自动并行执行
graph.add_edge("agent", ["tool_a", "tool_b", "tool_c"])
瓶颈 3:调试黑盒
复杂图的执行路径难以直观理解。
# 优化:使用 LangSmith 或自定义回调
from langchain.callbacks import LangChainTracer
config = {
"callbacks": [LangChainTracer(project_name="agent-debug")],
"run_name": "weather-agent"
}
result = await graph.ainvoke(input, config)
4.3 二次开发扩展点
- 自定义 Channel 类型:实现
Channel接口,支持自定义合并逻辑 - 自定义 CheckpointSaver:对接企业级存储(如 DynamoDB、Cassandra)
- 节点装饰器:封装通用的重试、超时、指标采集逻辑
第五章:延伸阅读与源码导航
核心源码路径
| 模块 | 路径 | 职责 |
|---|---|---|
| StateGraph | libs/langgraph/langgraph/graph/state.py | 图定义与编译 |
| PregelLoop | libs/langgraph/langgraph/pregel/loop.py | 执行引擎主循环 |
| Channels | libs/langgraph/langgraph/channels/ | 通道类型实现 |
| Checkpointing | libs/langgraph/langgraph/checkpoint/ | 状态持久化 |
| Streaming | libs/langgraph/langgraph/pregel/__init__.py | 流式输出实现 |
设计文档与参考
- Building LangGraph: Designing an Agent Runtime
- Pregel: A System for Large-Scale Graph Processing (Google 论文)
- Apache Beam Programming Model
版本信息
本文分析基于 LangGraph 0.4.x 主线(commit 范围:2025-Q4 至 2026-Q1)。关键 API 在 0.3→0.4 迁移中保持稳定,主要变更集中在 CLI 工具和 LangSmith 集成。
总结:可迁移的设计模式
LangGraph 的架构为构建可靠 Agent 系统提供了三个可复用的工程模式:
-
状态副本+确定性合并:在需要可重现执行的场景(如工作流引擎、事务处理),用空间换确定性,消除并发不确定性。
-
版本化增量更新:在需要历史回溯的场景(如协作编辑、审计日志),用版本号代替全量快照,保持 O(1) 查询复杂度。
-
异常驱动中断:在需要人机协作的场景(如审批流、交互式 Agent),用异常机制解耦正常逻辑与中断逻辑,保持业务代码纯净。