LangGraph:BSP底座
执行引擎:BSP模型
LangGraph的执行引擎底座使用的是借鉴了Google Pregel 论文的 BSP(Bulk Synchronous Parallel) 模型,其中有3个基本概念
- channel:作为状态的载体,节点通信的唯一媒介
- PregelNode:订阅某些channel,被触发后执行并再写回channel
- super-step:一轮读取-并发执行-写回—创建checkpoint
在一个super-step内的被触发的多个节点是并发执行的,如下图:
┌→ A ─┐
START ──┤ ├→ C → END
└→ B ─┘
但super-step之间是严格串行+可checkponit的,用这种方式就可以在保证并发性能的同时,实现了可中断/可回溯/可持久化,依靠这种方式实现Agent所需要的human-in-the-loop,断点续跑等能力
super-step三阶段模型
Superstep N
┌─────────────────────────────────────────────┐
│ Phase 1: Plan(规划)
│ 遍历所有节点,检查其订阅的 channel 是否有更新
│ → 生成本轮要执行的任务集合 tasks
├─────────────────────────────────────────────┤
│ Phase 2: Execute(执行)
│ tasks 中的所有节点并行执行(线程池/asyncio)
│ 每个节点只能读上一轮结束时的 channel 快照
│ 节点执行完的写入先暂存,不立即生效
├─────────────────────────────────────────────┤
│ Phase 3: Update(更新/同步屏障)
│ 等待本轮所有任务执行完毕(同步点)
│ 把暂存的写入按 channel 的合并策略统一应用
│ bump 相关 channel 的版本号
│ 落一份 checkpoint
└─────────────────────────────────────────────┘
↓ 如果 Plan 阶段发现没有节点被触发 → 终止
↓ 否则 → 进入 Superstep N+1
- 同一个super-step内,写入被延迟到phase 3才统一应用,在这之前 所有节点看到的是同一份快照,避免并发读写的静态条件
- 跨super-step之间存在严格的同步屏障,第N+1轮的Plan必须等待第N轮的update阶段完全结束才能开始
接下来,结合具体的使用场景来理解BSP发挥的作用:
Agent循环语义
builder = StateGraph(State)
builder.add_node("agent", agent_node)
builder.add_node("tools", tools_node)
builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", should_continue, {"tools": "tools", END: END})
builder.add_edge("tools", "agent")
BSP每一轮会通过订阅channel观察版本变化情况,来决定节点是否被触发,而不是通过 具体的循环机制 来实现运转
具体过程如下:
第 1 轮 Plan: 谁被触发?→ START 触发了 agent(图刚启动)
第 1 轮 Execute: 跑 agent,它往 messages 通道写入一条带 tool_calls 的 AIMessage
第 1 轮 Update: messages 通道版本从 v1 变成 v2
第 2 轮 Plan: 谁被触发?→ 调度器检查每个节点订阅的通道版本有没有变
tools 节点订阅了 messages 通道,v2 > 它上次看到的版本 → tools 被触发
第 2 轮 Execute: 跑 tools,写入 ToolMessage,messages 通道版本变成 v3
第 3 轮 Plan: 谁被触发?→ agent 节点也订阅了 messages 通道,v3 > 它上次看到的版本 → agent 又被触发
第 3 轮 Execute: 跑 agent,这次 LLM 觉得不用再调工具了,没有 tool_calls
messages 通道版本变成 v4,但这次 agent 的输出没有触发 tools 的条件
第 4 轮 Plan: 谁被触发?→ 没有任何节点因为 v4 满足触发条件 → 空任务集合 → 终止
工具并发调用
from langgraph.types import Send
def route_tools(state: State):
last_msg = state["messages"][-1]
if not last_msg.tool_calls:
return END
# 一次性派发 N 个 Send,而不是让一个节点内部 for 循环
return [Send("tools", {"call": c}) for c in last_msg.tool_calls]
def tools_node(state):
call = state["call"]
result = execute_tool(call)
return {"messages": [ToolMessage(content=result, tool_call_id=call["id"])]}
builder.add_conditional_edges("agent", route_tools, ["tools", END])
agent节点返回3个Send之后,BSP调度器在同一个superstep中会为这3个send各自生成一个独立的task,
PregelRunner 用线程池实现它们的并发执行
浙公网安备 33010602011771号