⚙️ Pregel 引擎概念 ★
这是 LangGraph 的"黑盒揭秘"章。前三章你学会了怎么用 StateGraph,本章看它底层是什么——StateGraph 编译后变成 Pregel 引擎,用 BSP 三阶段循环执行。我们用图解讲清概念(按你的选择,讲到入门级,不逐行精读算法源码)。
本章目标
- 理解 StateGraph 编译后变成什么(CompiledStateGraph 继承 Pregel)
- 掌握 Pregel 的 BSP 三阶段模型(Plan → Execute → Update)
- 理解 Actor(PregelNode)和 Channel(通道)的角色
- 明白"声明式图"如何降级为"通道读写",打通 LG2 的 Channel 伏笔
- 知道想深入引擎该读哪些源码文件(进阶指引)
按学习计划,引擎部分讲到"概念到入门级"。我们会用图解讲清BSP 模型和 Channel 机制,但不逐行精读 _algo.py/_loop.py。章末给出进阶阅读指引,供日后深入。
StateGraph 编译后变成什么
LG1 我们说"compile 后才能执行"。那 compile 到底做了什么?看源码:
class CompiledStateGraph(Pregel):
"""StateGraph 编译后的产物。它继承自 Pregel!
也就是说:你写的 StateGraph(声明式图)
编译后变成了一个 Pregel 实例(执行引擎)。
attach_node / attach_edge 方法把"图的节点和边"
翻译成"Pregel 的 actor + channel 订阅"。
"""
...
CompiledStateGraph 继承自 Pregel!这意味着你写的每一个 StateGraph,本质都是一个 Pregel 实例。compile() 做的事,就是把"人能看懂的节点和边"翻译成"Pregel 能执行的 actor 和 channel"。这一层翻译就是 LG1-LG4 所有便利的代价/来源。
Actor 与 Channel:Pregel 的两大原语
Pregel 源自 Google 的图计算论文,基于 BSP(Bulk Synchronous Parallel,批同步并行)模型。它的世界由两类东西组成:
class Pregel(PregelProtocol):
"""Pregel 管理 LangGraph 应用的运行时行为。
## 概览
Pregel 把【actor】和【channel】组合成一个应用。
- Actor 从 channel 读数据,向 channel 写数据
- 执行被组织成多个 step,遵循【Pregel 算法 / BSP 模型】
每个 step 包含三个阶段:
- Plan :决定本步执行哪些 actor
- Execute:并行执行选中的 actor(本步写入对其他 actor 不可见!)
- Update:用 actor 写入的值更新 channel
重复,直到没有 actor 要执行,或达到最大步数。
## Actor
一个 actor 就是一个 PregelNode。
它订阅 channel、读 channel、写 channel。
## Channel
Channel 用于 actor 间通信。每个 channel 有:
- 值类型 / 更新类型 / 更新函数(update)
- update 接收一系列更新,修改存储的值
"""
对应回你熟悉的 StateGraph 概念
| StateGraph(高层) | Pregel(底层) | 说明 |
|---|---|---|
| 节点(node 函数) | Actor(PregelNode) | 每个 add_node 的函数 → 一个 actor |
| 状态字段(State 的 key) | Channel(通道) | 每个状态字段 → 一个 channel(LG2 伏笔回收) |
| reducer | Channel 的 update 函数 | add_messages → BinaryOperatorAggregate 通道 |
| 边 / 条件边 | channel 订阅关系 | 边变成"actor 订阅哪些 channel" |
| invoke 一次 | 多个 BSP step | 一次调用 = 引擎跑若干步 |
BSP 三阶段模型 ★
这是本章的核心。Pregel 的每个 step("super-step")严格经历三个阶段。注意它和 F3 章讲的三阶段是同一回事——因为 LangGraph 的循环就是 Pregel 的循环:
Execute 阶段所有 actor 并行执行,但本步的写入对其他 actor 不可见——只有 Update 阶段把值写进 channel 后,下一步的 actor 才能看到。这叫批同步(Bulk Synchronous)。这就是为什么多个节点并行写同一字段时,reducer 能正确聚合——它们看到的是同一步开始时的快照,不会互相干扰。
用例子理解 BSP
假设图是 START → A → B → END(A、B 顺序执行):
invoke({"x": 1})
【super-step 1】
Plan : START 更新了 input channel → 选中订阅它的 actor A
Execute: 执行 A(读 x=1,算出新值) ← A 写的值此刻 B 看不到
Update : 把 A 的写入应用到 channel(如 x=2)
【super-step 2】
Plan : x channel 更新了 → 选中订阅它的 actor B
Execute: 执行 B(读 x=2,算出新值) ← 现在 B 能看到 A 的结果了
Update : 把 B 的写入应用
【super-step 3】
Plan : 没有订阅者要执行了 → 循环结束
返回最终 channel 值
如果是并行图(A、B 同时跑),它们会在同一个 super-step 的 Execute 阶段并发,但都基于同一步开始时的快照读写,互不干扰,Update 阶段用 reducer 合并。这就是 LangGraph 并行安全的基础。
Channel 类型
LG2 我们说"reducer 底层是 Channel"。现在看具体对应(源码在 channels/):
| Channel 类型 | 源码 | 用途 | 对应 StateGraph |
|---|---|---|---|
LastValue | last_value.py:20 | 存最后一个值(每步最多一个写) | 无 reducer 的字段(默认覆盖) |
BinaryOperatorAggregate | binop.py | 用二元运算符累积 | Annotated[list, operator.add] |
Topic | topic.py | PubSub 主题,多值累积 | Send 扇出的目标 |
EphemeralValue | ephemeral_value.py | 临时值(每步后重置) | 边/分支信号通道 |
NamedBarrierValue | named_barrier_value.py | 屏障,等所有上游完成 | 多入边的 join 语义 |
LG2 说"Annotated[list, add_messages] 编译成 BinaryOperatorAggregate 通道"。现在你明白了吗:compile() 时,每个状态字段的 Annotated[type, reducer] 被翻译成对应的 Channel;add_messages 这种 reducer 就是 Channel 的 update() 函数。你写的 reducer = Channel 的更新逻辑。
与 Checkpoint 的关系
Pregel 的 Update 阶段还会持久化 channel 快照——这就是 Checkpoint(LG6 详解)。每个 super-step 结束,所有 channel 的值被存盘。这让 Agent 能中断后恢复(从某个 checkpoint 续跑)。LG6 会讲透。
进阶阅读指引
如果你日后想彻底搞懂 Pregel 内部,按这个顺序读源码:
- pregel/main.py:449 Pregel 类文档(三阶段模型,最权威)
- pregel/_algo.py:232
apply_writes(Update 阶段如何写 channel) - pregel/_algo.py:371
prepare_next_tasks(Plan 阶段如何选 actor) - pregel/_loop.py:592
tick(一个 super-step 的循环控制) - pregel/_runner.py:135
PregelRunner(Execute 阶段并发执行) - channels/base.py:19
BaseChannel(通道抽象)
这个顺序从"是什么"到"怎么跑",循序渐进。对日常使用 Agent 而言,理解到本章的概念级就够了,不必深入。
与生产实践对照
| Pregel 概念 | OpenCode 对应 | |
|---|---|---|
| BSP 三阶段循环 | → | runLoop 的 while(true)(OC2 精读) |
| Actor(PregelNode) | → | runLoop 内每步的处理逻辑(调LLM/执行工具) |
| Channel 通道状态 | → | Session DB 持久化的消息/Part |
| Execute 并行不可见性 | → | 无(OpenCode 单步串行,靠 status 状态机) |
| Update + Checkpoint | → | 每步事件落库(事件溯源 EventV2Bridge) |
| super-step 概念 | → | runLoop 的一次 while 迭代 |
OpenCode 是单 Agent 串行循环(一个会话一个 runLoop),不需要 Pregel 的"多 actor 并行 + 批同步"。它的状态管理更简单(直接存 DB),循环也更直接(while + status)。Pregel 的复杂度是为"图并行执行"服务的,而 OpenCode 这类编码 Agent 不需要。理解这点,你就知道何时该用 LangGraph、何时该用更简单的循环。
小结
compile()把 StateGraph 翻译成 Pregel 实例——CompiledStateGraph继承Pregel。- Pregel 基于 BSP 模型:每个 super-step 经历 Plan → Execute → Update 三阶段,循环直到无 actor。
- Execute 阶段并行但写入互不可见,Update 才统一提交——这是并行安全的基础。
- 节点 → Actor,状态字段 → Channel,reducer → Channel 的 update。LG2 的伏笔在此回收。
- 日常用 Agent 理解到概念级即可;深入引擎按章末顺序读源码。
引擎机制搞清楚了,回到实用功能。下一章 LG6 · 检查点与人在回路——Pregel 的 Update 阶段会存 Checkpoint,这让 Agent 能中断后恢复。我们学习 interrupt(中断)+ Command(恢复),实现"执行前需人工批准"的 Agent。