一、问题的起点:StateGraph 是开发者语言,Pregel 才是机器语言
理解 LangGraph 最重要的一个认知跳跃是:StateGraph 是给人看的声明式 DSL,Pregel 才是真正运行图的引擎 。当你调用 .compile() 时,StateGraph 内部完成了一次完整的翻译:State 的每个键变成 Channel,Node 变成 PregelNode,Edge 变成 Channel 路由。没有 compile,就没有 super-step、没有 checkpoint、没有事务性保证。
这套分层设计并非偶然。Pregel 类是 LangGraph 的核心运行时引擎,实现了一套受 Google Pregel 系统启发的消息传递图计算模型。CompiledStateGraph 继承自 Pregel,因此直接使用 StateGraph.compile() 得到的就是一个完整的 Pregel 实例。
二、编译过程:三个核心元素的变换
2.1 State 字段 → Channel 实例
StateGraph 会自动为每个 state key 创建对应的 Channel,Channel 的类型由你的类型注解决定。规则如下:
- 无
Annotated注解的字段 →LastValuechannel(后写覆盖前写) - 带 reducer 函数的
Annotated字段 →BinaryOperatorAggregatechannel(如operator.add) add_messagesreducer → 专属Topicchannel(带 ID 去重的累积语义)- 不需要持久化的中间值 →
EphemeralValuechannel### 2.2 Node → PregelNode(订阅关系的建立)
在 Pregel 中,节点可以订阅任意数量的 channel,也可以向任意数量的 channel 写入。而在当前 LangGraph 实现中,每个节点对应两个 channel:一个是它订阅的(依赖的)channel,另一个是它写入的 channel。
对于给定的节点 N,只要它订阅的 channel M 的值发生变化,节点 N 就必须被执行。直觉上,这代表了当前节点的数据依赖关系。
编译时,每个 add_node("name", func) 调用会生成一个 PregelNode,该对象内部持有:
channels:输入 channel 列表(它读取哪些 channel 的值)triggers:触发执行的 channel 集合(哪些 channel 更新会激活它)writers:输出ChannelWrite列表(执行完后向哪些 channel 写入)
2.3 Edge → 隐式 Channel 路由
这是最容易被忽略的设计。Edge 在底层并不是一个独立对象,而是通过一个专属的 trigger channel 来表达路由关系的。
所有的 edge 都被折叠成每个节点的单一 trigger channel,因此执行复杂度对 edge 数量是常数级的。
具体机制是:
add_edge("A", "B")→ 节点 A 的输出写入一个名为branch:A→B的 trigger channel,节点 B 订阅该 channeladd_conditional_edges("A", condition, {...})→ 节点 A 执行完毕后调用condition函数,动态决定向哪个 trigger channel 写入,进而激活对应的下游节点START→ 特殊的__start__input channel,图启动时第一个被写入END→ 特殊的__end__output channel,被写入后 Pregel 输出结果并终止
三、完整的编译结果透视
用一个具体例子,看清 compile() 前后的全貌:
python
from typing import Annotated
import operator
from typing_extensions import TypedDict
from langchain_core.messages import BaseMessage
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
class AgentState(TypedDict):
messages: Annotated[list[BaseMessage], add_messages] # → Topic channel
step: int # → LastValue channel
total: Annotated[int, operator.add] # → BinaryOperatorAggregate channel
def llm_node(state: AgentState):
return {"messages": [AIMessage(...)], "step": state["step"] + 1}
def tool_node(state: AgentState):
return {"messages": [ToolMessage(...)], "total": 1}
def router(state: AgentState):
if state["messages"][-1].tool_calls:
return "tools"
return END
builder = StateGraph(AgentState)
builder.add_node("llm", llm_node)
builder.add_node("tools", tool_node)
builder.add_edge(START, "llm")
builder.add_conditional_edges("llm", router, {"tools": "tools", END: END})
builder.add_edge("tools", "llm")
graph = builder.compile() # → CompiledStateGraph extends Pregel
编译后的 Pregel 实例内部结构:---
四、Channel 的核心数据模型
每个 channel 实例都实现了以下接口:
python
class BaseChannel(ABC):
def update(self, values: Sequence[UpdateT]) -> bool:
# 接收本 super-step 中所有写入此 channel 的值
# 按 reducer 规则合并,返回值是否有变化(bool 决定是否触发下游)
...
def get(self) -> ValueT:
# 返回当前存储的值,供订阅节点读取
...
def checkpoint(self) -> CheckpointT:
# 返回可序列化的快照,用于 checkpoint 持久化
...
def from_checkpoint(self, checkpoint: CheckpointT) -> None:
# 从快照恢复状态
...
Channel 的核心三要素:值类型(value type)、更新类型(update type)以及更新函数(update function)------该函数接收一组更新序列并修改存储的值。
update() 方法的返回值(是否发生变化)是整个执行引擎停机判断的关键依据。
五、Pregel 执行引擎:Channel 如何驱动 Graph
5.1 三阶段 super-step 模型
Pregel 将应用的执行组织为多个步骤,遵循 Pregel 算法/块同步并行(Bulk Synchronous Parallel)模型。每个步骤包含三个阶段:Plan (规划)、Execution (执行)、Update(更新)。
具体来说:
Plan 阶段:在第一步中,选择订阅了特殊 input channel 的 actor;在后续步骤中,选择订阅了在上一步中被更新了的 channel 的 actor。
Execution 阶段 :并行执行所有被选中的 actor,直到全部完成、某一个失败或超时。在此阶段,channel 的更新对其他 actor 是不可见的。节点持有的是 channel 值的独立副本,彼此隔离。
Update 阶段 :更新 channel,将 actor 在这一步骤中写入的值合并进去。此时 channel.update(values) 被调用,reducer 逻辑在这里执行。### 5.2 Channel 版本号机制:激活节点的判定依据
每个 channel 都持有一个名称和当前版本号(单调递增的字符串),节点(函数)订阅一个或多个 channel,一旦这些 channel 发生变化便运行。执行循环在"没有更多节点需要运行"时停止------即比较所有 channel 的版本与每个节点最后一次见到的版本后,发现所有节点都已经看到了其订阅 channel 的最新版本。
具体的比较逻辑:
python
# Pregel 内部的节点激活判定逻辑(伪代码)
def plan_step(nodes, channels):
active_nodes = []
for node in nodes.values():
for trigger_channel in node.triggers:
ch = channels[trigger_channel]
# 关键:channel 当前版本 > 节点上次见到的版本
if ch.version > node.last_seen_version[trigger_channel]:
active_nodes.append(node)
break # 任意一个 trigger channel 更新即激活
return active_nodes
5.3 并行写入的确定性保证
一旦所有节点完成,每个状态副本的更新会以确定性顺序被应用到各自对应的 channel 上(而不是依据节点的启动或完成先后顺序,因为这在不同执行间会变化)。这确保了节点的执行顺序和延迟永远不会影响 agent 的最终输出。
对于 BinaryOperatorAggregate channel,同一 super-step 中多个节点写入同一 channel 时,update() 会接收到一个有序列表,按 reducer 函数累积合并:
python
class BinaryOperatorAggregate(BaseChannel):
def update(self, values: Sequence[UpdateT]) -> bool:
if not values:
return False
for v in values:
self.value = self.operator(self.value, v) # 二元累加
return True
六、Edge 类型与 Channel 对应的完整解析
6.1 固定边(add_edge)
add_edge("A", "B") 生成:
- 一个名为
branch:A→B的EphemeralValuetrigger channel - 节点 A 的
writers中追加:在执行完后无条件写入该 trigger channel - 节点 B 的
triggers中追加:branch:A→B
6.2 条件边(add_conditional_edges)
python
builder.add_conditional_edges("llm", router, {"tools": "tools", END: END})
编译后:
- 创建若干个 trigger channel:
branch:llm→tools、branch:llm→__end__ - 节点
llm的 writers 追加一个特殊ConditionalWrite:执行完后调用router(state),根据返回值选择性地写入对应 trigger channel - 节点
tools的 triggers 包含branch:llm→tools __end__channel 订阅branch:llm→__end__
6.3 Send API(动态并行边)
Send 类支持动态的、数据驱动的 map-reduce 并行模式。它允许在运行时根据 state 内容动态创建边。目标节点会为每个 Send 对象运行一次,全部并行执行,每个节点实例接收各自独立的 state 切片。
Send 的底层实现是在运行时动态创建 trigger channel 并注入写操作,而不是编译时静态生成,这使得 fan-out 数量可以在运行时确定:
python
def map_node(state):
return [Send("worker", {"item": x}) for x in state["items"]]
# 每个 Send 在当前 super-step 内动态写入独立的 trigger channel
# 所有 worker 节点实例在下一个 super-step 并行激活
```---
## 七、循环图的执行:Channel 版本如何支撑 cycle
LangGraph 区别于 DAG 框架的核心在于支持循环。循环的实现完全依赖 channel 的版本机制:
super-step 1: start channel 写入 → llm 激活
super-step 2: llm 写入 messages、branch:llm→tools → tools 激活
super-step 3: tools 写入 messages、branch:tools→llm → llm 再次激活
super-step 4: llm 写入 messages、branch:llm→end → 无更多节点入队 → 停机
关键点:执行会持续重复,直到没有 actor 被选中执行,或达到最大步骤数为止。每一次 llm 被激活,对应的 trigger channel 版本号都已递增,因此 Plan 阶段能够正确识别并将其再次入队,而不会因"已执行过"被跳过。
**循环终止由以下任一条件触发:**
1. 条件边路由到 `END`:向 `__end__` channel 写入,而非任何 trigger channel
2. 写入 `None` 值到自身订阅的 channel(`ChannelWriteEntry(skip_none=True)` 阻止写入,channel 版本不变,下游不再激活)
3. 达到 `recursion_limit`(默认 25 个 super-step)
---
## 八、事务性保证与并发安全
### 8.1 隔离副本执行
Execute 阶段节点以独立的 channel 值副本执行,即每个节点看到的是它开始执行时 channel 的快照,彼此不互相影响。一旦所有节点完成,更新以确定性顺序被应用到各 channel 中。这保证了无论并行节点的网络延迟如何变化,最终结果始终一致。
### 8.2 super-step 的原子性
节点在同一 super-step 内并发执行,来自并行执行的更新不保证有特定的一致顺序------这是设计时的关键考量。如果存在并行节点向同一 channel 写入,需要使用带 reducer 的 `BinaryOperatorAggregate`(如累加),而非依赖覆盖顺序。
若任一节点抛出异常,该 super-step 的所有 channel 更新均不会被应用(类似数据库事务回滚),checkpoint 保留在上一个成功的 super-step 状态。
---
## 九、Private State:节点声明额外 Channel 的机制
节点可以声明额外的 state channel,只要 state schema 定义存在。这意味着,即使某个 schema(如 `PrivateState`)未在 `StateGraph` 初始化时传入,只要该 schema 被定义,节点就可以将其字段作为新的 state channel 加入图中并写入。
这使得节点之间可以通过私有 channel 传递数据,而这些数据对不声明该 channel 的节点不可见------实现了一种节点级别的信息封装。
---
## 十、完整数据流:一次 Agent Loop 的 Channel 视角
以 ReAct Agent 为例,完整追踪每个 super-step 的 channel 状态变化:
invoke({"messages": [HumanMessage("查天气")]})
── super-step 1 ──
Plan: start .version=1 > llm.last_seen → llm 入队
Execute: llm_node(state) → 调用 LLM → 返回 AIMessage(tool_calls=[...])
Update: messages.update([AIMessage(...)]) → messages.version=1
branch:llm→tools.update([True]) → trigger.version=1
Checkpoint: 保存 {messages: [...], step: 1}
── super-step 2 ──
Plan: branch:llm→tools.version=1 > tools.last_seen → tools 入队
Execute: tool_node(state) → 执行工具调用 → 返回 ToolMessage("晴,25°C")
Update: messages.update([ToolMessage(...)]) → messages.version=2
branch:tools→llm.update([True]) → trigger.version=1
total.update([1]) → total.version=1
Checkpoint: 保存 {messages: [...], step: 1, total: 1}
── super-step 3 ──
Plan: branch:tools→llm.version=1 > llm.last_seen → llm 入队
Execute: llm_node(state) → 调用 LLM → 返回 AIMessage("天气晴,25°C") (无 tool_calls)
Update: messages.update([AIMessage(...)]) → messages.version=3
branch:llm→end.update([True]) → end .version=1
Checkpoint: 保存最终状态
── super-step 4 ──
Plan: 所有 trigger channel 版本均已被各节点见过 → 无节点入队 → 停机
Output: 读取 output_channels [messages, step, total] → 返回给调用者
---
## 十一、小结:Channel 是 LangGraph 的"神经系统"
| 高层概念 | 底层 Pregel 实体 | 职责 |
|---|---|---|
| State 字段(无注解) | `LastValue` channel | 单值覆盖存储,跨步骤传递 |
| State 字段(reducer) | `BinaryOperatorAggregate` | 并行写入自动合并 |
| `add_messages` 字段 | `Topic` channel | 带 ID 去重的消息累积 |
| `add_edge(A, B)` | `EphemeralValue` trigger channel | 静态路由,激活下游 |
| `add_conditional_edges` | 多个 `EphemeralValue` + 条件写 | 动态路由,激活被选中的下游 |
| `Send(node, data)` | 运行时动态 trigger channel | fan-out 并行,map-reduce |
| Node 函数 | `PregelNode`(订阅 + 写入) | 计算逻辑的封装单元 |
| Graph 入口 | `__start__` channel | invoke 时第一个被写入 |
| Graph 出口 | `__end__` / `output_channels` | 停机后读取返回值 |
| Checkpoint | `channel.checkpoint()` | 每 super-step 序列化所有 channel 快照 |
Channel 驱动执行的本质是:一个或多个 channel 被映射到 input,即 agent 的起始输入被写入这些 channel,从而触发订阅这些 channel 的节点;一个或多个 channel 被映射到 output,即当执行停止时,这些 channel 的值即为 agent 的返回值。Channel 的版本号变更是节点激活的唯一信号,reducer 语义决定了并发写入的合并方式,而 `channel.update()` 的返回值(是否发生变化)则是图停机的判定依据------整个 LangGraph 的执行驱动,都浓缩在这三个机制之中。