如果说 Channel 是 LangGraph 的状态存储层,那么 Pregel 执行引擎就是驱动整张图"活起来"的心脏。理解 Pregel,你就理解了 LangGraph 最核心的问题:一次 graph.invoke() 调用究竟发生了什么,节点是怎么被触发的,并发执行的正确性从何而来,以及 checkpoint 为什么总是一致的。

这一切都源自一个 2010 年的学术模型。

1. 为什么选择 BSP 模型

2010 年,Google 发表了论文《Pregel: A System for Large-Scale Graph Processing》,提出用 BSP(Bulk Synchronous Parallel,批同步并行)模型处理大规模图计算——PageRank、最短路径、连通分量,这类问题都在它上面跑得很好。

BSP 的工作方式分三个阶段,循环执行:

  1. 本地计算(Local Computation):每个顶点独立读取自己的状态并执行计算,互不干扰。
  2. 消息传递(Message Passing):顶点将计算结果作为消息发往邻居顶点。
  3. 全局同步屏障(Barrier Synchronization):等所有顶点都完成计算、消息都发出后,进入下一轮。没有顶点能越过屏障提前进入下一轮。

这三个阶段构成一个"超步"(superstep)。整个计算就是一系列超步的迭代,直到没有顶点产生新消息为止。

LangGraph 将这套模型直接适配到了 Agent 执行语义上:

graph LR
    subgraph superstep["⟳ 超步循环(Superstep)"]
        direction LR
        A["🧠 本地计算<br/>节点并发执行"] --> B["📨 消息传递<br/>写入 Channel"]
        B --> C["⏸ 同步屏障<br/>等待全部节点完成"]
    end
    C -->|"有节点被触发"| A
    C -->|"无节点可触发"| D(["执行结束"])

为什么这套模型特别适合 Agent?因为 Agent 的推理-行动循环天然就是"计算-观察-再计算"的迭代结构:调用 LLM 得到结果,执行工具,观察输出,再次推理。每个超步对应一轮"思考和行动",超步边界是天然的快照点——此时所有节点都已完成执行,所有状态都已更新,checkpoint 记录的是一个完全一致的系统状态,而不是某个节点执行到一半的中间状态。这对调试和恢复来说至关重要。

2. 图编译(.compile() 做了什么)

当你写完 StateGraph 然后调用 .compile() 时,LangGraph 做的远不止"把图存下来"。编译阶段是一次性的昂贵分析,目的是把所有能在静态时确定的事情提前做完,让运行期的每次 invoke 只做最小的动态决策。

拓扑验证是第一步。LangGraph 检查是否有孤立节点(定义了但没有边连接),边引用的节点是否真实存在,以及有没有非法的循环引用。有向循环本身是允许的(Agent 需要循环),但某些特殊情况会被提前拒绝。

Channel 实例化在这里完成。LangGraph 根据你的 State schema 创建 channel 对象。除了用户定义的 channel,框架还会创建几个内部 channel:__start__(接收初始输入)、__end__(收集最终输出)、__interrupt__(中断信号),以及 TASKS(存储 Send 对象,用于动态 fan-out)。这些 channel 对用户透明,但在执行引擎内部起关键作用。

节点包装将用户提供的普通 callable 包装成 PregelNode。PregelNode 携带了节点的 triggers(哪些 channel 的变化会触发这个节点)、retry_policy(重试策略)、timeout_policy(超时策略)等元数据。包装完成后,运行期的调度逻辑只需要操作统一的 PregelNode 接口。