Pregel 为什么会成为LangGraph编排的心脏

摘要: 拆解 LangGraph.js 最核心的 Pregel 执行引擎------看它如何用 Superstep、Channel 和 Checkpoint 三板斧,解决 AI Agent 编排中状态管理、并行执行、断点续行三大难题。


一、为什么你需要了解 Pregel

如果你用过 LangGraph 构建 Agent,你大概写过这样的代码:

typescript 复制代码
const graph = new StateGraph(State)
  .addNode("agent", agentNode)
  .addNode("tools", toolNode)
  .addEdge("__start__", "agent")
  .addConditionalEdges("agent", route, { tools: "tools", end: "__end__" })
  .addEdge("tools", "agent")
  .compile({ checkpointer: new MemorySaver() });

const result = await graph.invoke(input, config);

调用 invoke 的那一刻,背后运转的正是 Pregel 执行引擎。它负责:

  • 决定每一步哪些节点该执行
  • 让互不依赖的节点并行跑起来
  • 在每个步骤结束后自动保存快照(Checkpoint)
  • 在需要人类介入时暂停,之后从断点精确恢复

理解 Pregel,就是理解 LangGraph 的「物理定律」。


二、一段跨越 16 年的技术演化

2.1 Google Pregel:为万亿级图而生

2010 年,Google 发表了论文 Pregel: A System for Large-Scale Graph Processing ,提出了一种基于 BSP(Bulk Synchronous Parallel) 的大规模图计算模型。

BSP 的核心思想极其简洁------计算以离散的 Superstep(超级步) 推进,每个 Superstep 包含三个阶段:

scss 复制代码
┌──────────────────────────────────────────────┐
│            一个 BSP Superstep                  │
├──────────────────────────────────────────────┤
│ ① 本地计算    每个节点独立运行 compute()       │
│       ↓                                      │
│ ② 消息传递    节点之间通过边交换消息            │
│       ↓                                      │
│ ③ 屏障同步    全局等待,所有消息到达后才进入下一轮│
└──────────────────────────────────────────────┘

关键约束:步骤②发送的消息,在步骤③同步完成之前,接收方看不到。这消除了数据竞争。

2.2 LangGraph 的关键改造

LangGraph 保留了 BSP 的屏障同步语义,但把「图计算」改造成了「Agent 编排」:

维度 Google Pregel LangGraph Pregel
目标 大规模图数据处理 AI Agent 工作流编排
节点含义 图中的顶点 Agent 的执行步骤(函数)
消息通道 图的边 Channel(通道)系统
状态存储 节点本地值 全局 Channel Map + Checkpoint
持久化 无内置 每轮 Superstep 自动 Checkpoint
人机交互 interrupt 机制(HITL)

最核心的设计差异 :Google Pregel 中节点通过 sendMessage(targetId, value) 直接定向发送;LangGraph 中节点写入 Channel,由 Channel 的语义决定值如何聚合------这是一种发布-订阅模型


三、Superstep:Pregel 引擎的心跳

Pregel 的一切魔法都围绕 Superstep 展开。每一次 Superstep 经历五个精密的阶段:

graph TB A[Phase 1: _prepareNextTasks<br/>确定本轮要执行的任务] --> B[Phase 2: PregelRunner.tick<br/>并发执行所有任务] B --> C[Phase 3: _applyWrites<br/>将写入应用到 Channel] C --> D[Phase 4: _putCheckpoint<br/>持久化快照] D --> E[Phase 5: 中断检查 / 准备下一轮] E --> A

Phase 1:任务准备------Pull 与 Push 双轨调度

每个 Superstep 开始时,引擎需要确定本轮要执行哪些任务。任务来源有两条轨道:

Pull(拉取):Channel 版本变化触发的常规节点。当一个 Channel 被上游写入新版本,订阅该 Channel 的节点会被自动触发。触发条件是一个简洁的数学不等式:

css 复制代码
channel_versions[triggerChan] > versions_seen[nodeName][triggerChan]

左边是 Channel 的全局当前版本,右边是该节点上次看到的版本。前者大于后者,说明有新数据需要处理。

Push(推送) :上游节点通过 Send() API 主动分发的动态任务。每个 Send 包携带自定义输入数据,写入一个名为 TASKS 的特殊通道,在下一个 Superstep 被消费为并行任务。

graph LR subgraph Pull 拉取调度 A1[Channel 被写入新版本] --> A2[triggerToNodes 反向索引] A2 --> A3[版本检查通过] A3 --> A4[_procInput 读取输入] end subgraph Push 推送调度 B1[节点调用 Send] --> B2[写入 TASKS 通道] B2 --> B3[按 index 取出 Send 包] B3 --> B4[packet.args 作为输入] end

Pull 是静态可预测的(编译时就能确定触发关系),Push 是运行时动态决定的。两者取并集构成完整的任务集合。

Pull 调度还有一个精巧的优化------candidateNodes 函数利用 triggerToNodes 反向索引,只检查上一轮被修改的 Channel 关联的节点,避免了全量遍历:

优化级别 条件 策略 复杂度
L1 精准触发 有 updatedChannels 只检查被修改 Channel 关联的节点 O(updatedChannels × avgDegree)
L2 空值短路 所有 channel_versions 为 null 直接返回空集合 O(channelCount)
L3 全量遍历 以上均不满足 遍历所有节点 O(nodeCount)

Phase 2:并发执行------Promise.race + Barrier

所有准备好的任务被交给 PregelRunner 并发执行。核心机制是 Promise.race

typescript 复制代码
while (/* 还有任务 */) {
  // 启动尽可能多的任务(受 maxConcurrency 限制)
  for (; startedCount < tasks.length && activeCount < maxConcurrency; startedCount++) {
    executingTasksMap[task.id] = _runWithRetry(task, retryPolicy);
  }
  // 等待任意一个任务完成
  const settled = await Promise.race([
    ...Object.values(executingTasksMap),
    barrier.wait,  // 支持运行时动态注入新任务
  ]);
  yield settled;
}

这里有一个巧妙的设计------createPromiseBarrier。当某个任务在执行过程中通过 call()Send() 动态创建子任务时,barrier 会被唤醒(barrier.next()),让 Promise.race 立即感知到新任务的加入,将其纳入并发执行池。

这意味着 Send() 产生的子任务可以在同一个 Superstep 内立即执行 (通过 acceptPush 机制),而非等到下一个 Superstep,显著降低端到端延迟。

Phase 3:写入应用------Channel 是唯一的真相来源

这是整个 Superstep 最关键的步骤。所有并行任务的输出不会直接传给下一个节点,而是写入 Channel,由 Channel 的语义决定状态如何聚合:

typescript 复制代码
// 按 path 排序保证确定性
tasks.sort((a, b) => comparePaths(a.path, b.path));

// 更新 versions_seen(标记节点已看到的 channel 版本)
for (const task of tasks) {
  for (const chan of task.triggers) {
    checkpoint.versions_seen[task.name][chan] = checkpoint.channel_versions[chan];
  }
}

// 按 Channel 分组写入,调用 channel.update(vals)
for (const [chan, vals] of Object.entries(pendingWritesByChannel)) {
  const updated = channels[chan].update(vals);  // Channel 自行决定聚合策略
  if (updated) {
    checkpoint.channel_versions[chan] = getNextVersion(maxVersion);
    updatedChannels.add(chan);
  }
}

核心洞察_applyWrites 是 Channel-Driven 架构的枢纽------实现了生产者-消费者解耦。节点不知道谁会消费自己的输出,消费者也不知道输入来自哪个节点。

Phase 4 & 5:持久化与循环控制

每个 Superstep 结束后自动保存 Checkpoint,包含 Channel 值快照、版本号、已见版本和挂起写入。然后检查是否需要中断(interruptBefore / interruptAfter),或是否已无新任务可执行。

关键时序_putCheckpointshouldInterrupt 之前执行------保证中断时断点已持久化到存储层。


四、Channel:并发冲突的终极解法

当多个并行节点同时试图修改共享状态时,传统方案是加锁。Pregel 采用了一条完全不同的路径------通过 五层防御体系 从根本上消除并发写入冲突。

第 1 层:写入缓冲

每个并行任务在执行期间不会直接修改 Channel 。所有写入暂存在任务本地的 writes 数组中:

scss 复制代码
   NodeA 执行中                NodeB 执行中               NodeC 执行中
   ┌──────────────┐           ┌──────────────┐           ┌──────────────┐
   │ taskA.writes │           │ taskB.writes │           │ taskC.writes │
   │  [x, 5]      │           │  [x, 10]     │           │  [y, "hello"]│
   └──────────────┘           └──────────────┘           └──────────────┘
         │                          │                          │
         └──────────┬───────────────┘──────────────────────────┘
                    │ 全部完成后统一提交
                    ▼
            _applyWrites()

并行执行期间不存在共享可变状态,无需任何锁。

第 2 层:Superstep 屏障

只有当所有 并行任务都完成后,_applyWrites 才会被调用,统一将写入应用到 Channel。这是对 BSP 屏障同步的忠实继承。

第 3 层:Channel 语义隔离(核心)

这是最精妙的设计。每种 Channel 类型定义了明确的冲突解决语义

Channel 类型 冲突策略 并行写入行为
LastValue 拒绝冲突 多个写入 → 抛出 InvalidUpdateError
BinaryOperatorAggregate Reducer 合并 按 reducer 函数顺序合并
Topic 收集所有 所有写入收集到列表
DeltaChannel 批量 Reducer 批量 reducer + Overwrite 语义

LastValue 的设计哲学 特别值得一提:如果多个并行节点写入同一个 LastValue Channel,系统直接报错。这不是 bug,而是设计信号------告诉图设计者"你不应该让多个并行节点写同一个字段"。尽早暴露设计缺陷,而非静默产生不确定行为。

typescript 复制代码
// LastValue 的 update 方法
update(values: Value[]): boolean {
  if (values.length !== 1) {
    throw new InvalidUpdateError(
      "LastValue can only receive one value per step.",
      { lc_error_code: "INVALID_CONCURRENT_GRAPH_UPDATE" }
    );
  }
  this.value = [values[values.length - 1]];
  return true;
}

Reducer 的优雅 :图设计者通过选择 Reducer 函数来显式定义合并策略。例如,多个并行节点写入 messages 字段时,Reducer 自动拼接所有消息;写入 totalTokens 时,Reducer 自动求和:

typescript 复制代码
const State = Annotation.Root({
  // 消息列表:并行写入自动拼接
  messages: Annotation<BaseMessage[]>({ reducer: messagesReducer }),
  // Token 计数:并行写入自动累加
  totalTokens: Annotation<number>({ reducer: (a, b) => a + b }),
  // 当前步骤:只允许单写者(并行写入会报错)
  currentStep: Annotation<string>,
});

第 4 层:确定性排序

即使有了 Channel 语义,写入的应用顺序也必须确定性。_applyWrites 按任务的 path(任务在图中的唯一地址标识)排序,保证 Reducer 合并顺序可重现:

ini 复制代码
排序前(Promise.race 完成顺序随机):
  NodeB(writes=[counter,10]), NodeA(writes=[counter,5]), NodeC(writes=[counter,3])

排序后(按 path 确定性排列):
  NodeA < NodeB < NodeC

→ Reducer 始终按 A→B→C 顺序合并:counter = 0 + 5 + 10 + 3 = 18

第 5 层:版本追踪

channel_versionsversions_seen 两套版本号确保节点触发的确定性------不会重复触发、不会遗漏触发、触发判定与执行顺序无关。

与传统并发模型的对比

复制代码
┌─────────────────┬────────────────┬────────────────┬──────────────────┐
│                 │ 锁(Mutex)     │ Actor 模型      │ Pregel Channel   │
├─────────────────┼────────────────┼────────────────┼──────────────────┤
│ 冲突检测         │ 运行时死锁      │ 消息顺序不定     │ 编译/运行时显式   │
│ 冲突解决         │ 程序员手动处理   │ Actor 内部逻辑   │ Channel 语义定义 │
│ 死锁风险         │ 高              │ 无              │ 无               │
│ 与持久化的集成    │ 需自行实现       │ 需自行实现       │ 原生 Checkpoint │
└─────────────────┴────────────────┴────────────────┴──────────────────┘

五、Checkpoint + 断点续行:Agent 的"存档读档"

Agent 编排中有一个传统框架几乎无法优雅解决的问题:Agent 执行到一半需要人类审批,怎么办?

Pregel + Checkpoint 的组合提供了完整的断点续行能力,支撑四大场景:

5.1 三种暂停方式

interruptBefore / interruptAfter:编译期声明的中断点。

typescript 复制代码
const graph = new StateGraph(State)
  .addNode("agent", agent)
  .addNode("human_review", humanReview)
  .addNode("execute", execute)
  .addEdge("__start__", "agent")
  .addEdge("agent", "human_review")
  .addEdge("human_review", "execute")
  .compile({
    checkpointer: new MemorySaver(),
    interruptBefore: ["human_review"],  // 执行 human_review 前暂停
  });

interrupt():节点内部主动中断。

typescript 复制代码
const humanReview = (state) => {
  const humanDecision = interrupt({
    question: `是否批准?最后消息: "${state.messages.at(-1)}"`,
    options: ["approved", "rejected"],
  });
  return { decision: humanDecision };
};

interrupt() 底层有三路分支:

  1. 已有 resume 值(恢复执行时)→ 直接返回
  2. 有 nullResume 值(来自 Command 的全局恢复)→ 消费并返回
  3. 无 resume 值 → 抛出 GraphInterrupt 中断执行

5.2 Command + resume:恢复执行

typescript 复制代码
// 第一次调用:agent 执行 → human_review 中 interrupt() 触发中断
const r1 = await graph.invoke(
  { messages: ["用户: 请帮我处理这个任务"] },
  { configurable: { thread_id: "approval-001" } }
);
// → { messages: [...], __interrupt__: [{ value: { question: "是否批准?..." } }] }

// 人类审批后,恢复执行
const r2 = await graph.invoke(
  new Command({ resume: "approved" }),
  { configurable: { thread_id: "approval-001" } }
);
// → { messages: [..., "方案 A 已执行!"], decision: "approved" }

5.3 shouldInterrupt 的防重复中断

恢复执行时有一个微妙的风险:shouldInterrupt 会不会再次触发中断?

答案在 versions_seen[INTERRUPT]------一个特殊的"中断游标"。每次 resume 时,它会被同步到最新的 channel_versions,使得 anyChannelUpdated 判断为 false,从而避免重复中断:

css 复制代码
阶段 1:首次触发中断
  channel_versions = { messages: 2 }
  versions_seen[__interrupt__] = { messages: 1 }  ← 旧版本
  → 2 > 1 → 触发中断 ✓

阶段 2:恢复执行时(_first 中同步)
  versions_seen[__interrupt__] = { messages: 2 }  ← 同步到最新

阶段 3:恢复后继续执行
  → 中断游标已对齐,不会重复触发 ✓

5.4 时间旅行(Time Travel)

断点续行的进阶玩法------从历史某个 Checkpoint 恢复并创建分支:

typescript 复制代码
// 1. 列举历史 Checkpoint
const history = [];
for await (const tuple of checkpointer.list(config)) {
  history.push(tuple);
}

// 2. 从 agent 完成后的状态分支,注入不同的决策
await graph.updateState(
  { configurable: { thread_id: "approval-001", checkpoint_id: history[2].checkpoint.id } },
  { decision: "rejected" },
  "human_review"
);

// 3. 继续执行 → 走不同的路径
const r3 = await graph.invoke(null, config);
// → { messages: [..., "方案已取消。"] }

5.5 崩溃恢复

每个 Superstep 自动 Checkpoint,进程崩溃后只需重新 invoke(null, config)(同一 thread_id),引擎自动从最近的 Checkpoint 恢复,从未完成的节点继续执行。

三重保障

  1. _applyWrites_putCheckpoint 之前执行 → Channel 状态一致
  2. _putCheckpointshouldInterrupt 之前执行 → 中断前断点已持久化
  3. putWrites 在任务完成时立即持久化 → 即使 Checkpoint 未保存,中间写入也不丢失

5.6 多次 interrupt 的逐步恢复

如果节点内有多个 interrupt(),resume 数组会逐步累积,按中断计数器索引消费:

typescript 复制代码
const multiNode = (state) => {
  const intent = interrupt({ step: 1, question: "确认意图?" });   // 第 0 次
  const params = interrupt({ step: 2, question: "确认参数?" });   // 第 1 次
  return { messages: [`意图: ${intent}, 参数: ${params}`] };
};
ini 复制代码
第 1 次调用:interrupt(idx=0) → 无 resume → 抛出中断
第 2 次调用 resume="yes":interrupt(idx=0) → 返回 "yes";interrupt(idx=1) → 抛出中断
第 3 次调用 resume="ok":interrupt(idx=0) → 跳过(已恢复);interrupt(idx=1) → 返回 "ok";节点完成!

六、并行执行 → Checkpoint 全景时序

把以上所有机制串联起来,看一个包含 3 个并行节点的 Superstep 完整数据流:

sequenceDiagram participant User participant PE as Pregel Engine participant PL as PregelLoop participant PR as PregelRunner participant CP as Checkpointer User->>PE: invoke(input, config) PE->>PL: initialize() PL->>CP: getTuple(config) CP-->>PL: CheckpointTuple PL-->>PE: loop instance rect rgb(240, 248, 255) Note over PE,CP: Superstep N PE->>PL: tick() Note right of PL: first tick: _first()<br/>otherwise: apply previous writes PL->>PL: _prepareNextTasks() Note right of PL: Pull + Push dual-track alt tasks found PL-->>PE: return true PE->>PR: tick() PR->>PR: Task A, B, C in parallel Note right of PR: Promise.race + maxConcurrency PR-->>PE: all tasks settled PE->>PL: tick() next round PL->>PL: _applyWrites() Note right of PL: sort, versions_seen,<br/>group by channel, update PL->>CP: putCheckpoint(step N) Note right of PL: step++ opt interruptAfter hit PL-->>PE: throw GraphInterrupt end else no tasks PL-->>PE: return false (done) end end PE-->>User: final result

七、Pregel vs 传统 Agent 循环

javascript 复制代码
┌──────────────────┬───────────────────┬─────────────────────┐
│     特性          │ 传统 Agent 循环    │ LangGraph Pregel    │
├──────────────────┼───────────────────┼─────────────────────┤
│ 执行模型          │ 顺序调用链          │ Superstep 并行      │
│ 状态传递          │ 函数返回值传递       │ Channel 解耦传递    │
│ 状态聚合          │ 手动合并            │ Channel reducer 自动│
│ 持久化            │ 需自行实现          │ 每步自动 Checkpoint │
│ 断点恢复          │ 不支持             │ 原生支持             │
│ 并行执行          │ 需手动 Promise.all  │ 自动并行无依赖节点   │
│ 人机交互          │ 不支持             │ interrupt 原生支持   │
│ 时间旅行          │ 不支持             │ updateState 回溯     │
│ 动态路由          │ if-else            │ Send() + 条件边      │
└──────────────────┴───────────────────┴─────────────────────┘

八、设计启示录

8.1 "冲突"不是 bug,而是设计信号

Pregel 不试图隐式解决所有冲突。LastValue 的并发写入直接报错,告诉图设计者"你的状态 Schema 设计有问题"。这种尽早失败的哲学,远比静默产生不确定行为要负责。

8.2 Channel 选择 = 并发策略选择

图设计者在定义 State Schema 时,通过选择 Channel 类型(LastValue / Reducer / Topic)就已经确定了并发策略。这是一种声明式的并发控制,而非命令式。

8.3 BSP 模型的复兴

写入缓冲 + 屏障同步 = 无锁并发。这是对 1990 年代 BSP 并行计算模型的现代演绎------既有并行的性能优势,又有顺序执行的正确性保证。

8.4 Checkpoint 先行

整个执行流程中,Checkpoint 的持久化始终先于 中断判断。这保证了一个不变量:任何中断点都有完整断点。无论是人类审批、进程崩溃还是时间旅行,都能从断点精确恢复。


九、关键源码索引

模块 文件 核心职责
Pregel 入口 pregel/index.ts _streamIterator, _runLoop
循环控制 pregel/loop.ts tick(), _first(), _putCheckpoint(), acceptPush
并发执行 pregel/runner.ts _executeTasksWithRetry(), _commit()
任务调度 pregel/algo.ts _prepareNextTasks(), _applyWrites()
中断机制 interrupt.ts interrupt(), resume 值消费
LastValue channels/last_value.ts 并发冲突检测
Reducer channels/binop.ts Reducer 合并
Topic channels/topic.ts 收集所有并行写入
Checkpoint checkpoint/src/base.ts 持久化抽象层

本文基于 LangGraph.js 源码分析,版本对应 @langchain/langgraph 核心运行时。

相关推荐
艾莉丝努力练剑2 小时前
OpenCode AI 编程:Ubuntu 24.04 环境安装与使用指南
linux·服务器·网络·人工智能·tcp/ip·ubuntu
一次旅行2 小时前
DeepSeek-V4 原厂直供模型即将登陆腾讯云!峰谷定价详解 + 实战调用指南
人工智能·腾讯云·ai编程
阿里云大数据AI技术2 小时前
DataWorks Data Agent的演进与工程化实践
人工智能·agent
HERR_QQ3 小时前
强化学习的数学原理 学习笔记
人工智能·笔记·学习·自动驾驶
IvorySQL3 小时前
PG 技术日报|2026-07-04
数据库·人工智能·postgresql·开源
程序员cxuan3 小时前
Claude Code 为了封禁中国用户,竟然在代码里下毒
人工智能·后端·程序员
shushangyun_3 小时前
2026年AI知识库系统选型指南
人工智能
小短腿的代码世界3 小时前
Qt WebEngine多进程架构与IPC通信:从Chromium多进程到Qt信号槽融合
qt·架构·系统架构
喵叔哟3 小时前
从聊天记录到真正的记忆:为 AI Agent 设计一套分层记忆系统
人工智能·langchain
谷雪_6583 小时前
Linux 网络命名空间:从内核原理到企业级容器网络架构全景实战
linux·网络·架构