Dify 与 LangGraph 图执行引擎原理对比:从定义层到运行时的架构拆解
目标读者:初中级后端 / AI 应用开发者。你已经能写简单的 LLM 调用链,但面对 Dify 的可视化工作流和 LangGraph 的代码化图编排时,想搞清楚它们底层执行引擎的本质差异。
文章目录
-
- [Dify 与 LangGraph 图执行引擎原理对比:从定义层到运行时的架构拆解](#Dify 与 LangGraph 图执行引擎原理对比:从定义层到运行时的架构拆解)
- [📚 内容大纲](#📚 内容大纲)
- [📖 第一节:为什么需要对比这两个引擎](#📖 第一节:为什么需要对比这两个引擎)
-
- [1.1 从一个真实的困惑说起](#1.1 从一个真实的困惑说起)
- [1.2 两个引擎的定位:不是竞品,是不同层面的抽象](#1.2 两个引擎的定位:不是竞品,是不同层面的抽象)
- [1.3 本文要回答的核心问题](#1.3 本文要回答的核心问题)
- [📖 第二节:图定义层------配置驱动 vs 代码驱动](#📖 第二节:图定义层——配置驱动 vs 代码驱动)
- [📖 第三节:执行运行时------事件驱动线程池 vs Pregel 状态推进](#📖 第三节:执行运行时——事件驱动线程池 vs Pregel 状态推进)
-
- [3.1 Dify 的运行时:GraphEngine + WorkerPool + Dispatcher](#3.1 Dify 的运行时:GraphEngine + WorkerPool + Dispatcher)
-
- 一次执行的完整路径
- [Dify 运行时的关键特征](#Dify 运行时的关键特征)
- [3.2 LangGraph 的运行时:Pregel Loop](#3.2 LangGraph 的运行时:Pregel Loop)
-
- [一次 superstep 内部具体做什么](#一次 superstep 内部具体做什么)
- [LangGraph 运行时的关键特征](#LangGraph 运行时的关键特征)
- [3.3 对比:两种执行模型的本质差异](#3.3 对比:两种执行模型的本质差异)
- [📖 第四节:状态管理------全局变量池 vs Channel + Reducer](#📖 第四节:状态管理——全局变量池 vs Channel + Reducer)
-
- [4.1 Dify:VariablePool 是唯一的"数据源"](#4.1 Dify:VariablePool 是唯一的"数据源")
- [4.2 LangGraph:State、Channel、Reducer 三层分离](#4.2 LangGraph:State、Channel、Reducer 三层分离)
- [4.3 对比:状态模型的工程后果](#4.3 对比:状态模型的工程后果)
- [📖 第五节:暂停/恢复与容错------两种中断哲学](#📖 第五节:暂停/恢复与容错——两种中断哲学)
-
- [5.1 Dify:节点驱动暂停 + 命令通道恢复](#5.1 Dify:节点驱动暂停 + 命令通道恢复)
- [5.2 LangGraph:interrupt 是一等公民](#5.2 LangGraph:interrupt 是一等公民)
-
- [节点内部调用 interrupt()](#节点内部调用 interrupt())
- 恢复时节点会重新执行
- 恢复方式
- [Checkpoint 与 Interrupt 的绑定](#Checkpoint 与 Interrupt 的绑定)
- [5.3 对比:中断模型的工程后果](#5.3 对比:中断模型的工程后果)
- [📖 第六节:工程决策------你该选哪个,怎么用对](#📖 第六节:工程决策——你该选哪个,怎么用对)
-
- [6.1 场景适配表](#6.1 场景适配表)
- [6.2 常见选型误区](#6.2 常见选型误区)
- [6.3 落地检查清单](#6.3 落地检查清单)
- [✅ 最后帮你收成一句话](#✅ 最后帮你收成一句话)
- 参考资料
📚 内容大纲
| 章节 | 主题 | 核心对比点 | 学习目标 |
|---|---|---|---|
| 第一节 | 🎯 为什么需要对比这两个引擎 | 两个框架的定位、适用边界、看似相似实则不同的设计目标 | 建立正确的对比视角,破除"都是图编排工具"的误解 |
| 第二节 | 🏗️ 图定义层:配置驱动 vs 代码驱动 | Dify 的 JSON + Node 工厂 vs LangGraph 的 StateGraph + compile() |
理解"图是如何被描述出来的",以及两种定义方式的成本与收益 |
| 第三节 | ⚙️ 执行运行时:事件驱动线程池 vs Pregel 状态推进 | Dify 的 GraphEngine + WorkerPool + Dispatcher vs LangGraph 的 SyncPregelLoop |
理解一次调用从入口到结束的完整路径,这是全文最深的章节 |
| 第四节 | 🔄 状态管理:全局变量池 vs Channel + Reducer | Dify 的 VariablePool + selector vs LangGraph 的 State + Channel + Reducer |
理解状态如何在节点间流转,以及两种模型对并发和调试的影响 |
| 第五节 | ⏸️ 暂停/恢复与容错:两种中断哲学 | Dify 的 PauseRequestedEvent + 命令通道 vs LangGraph 的 interrupt() + Checkpoint |
理解人工介入和流程恢复在两种架构中的实现差异 |
| 第六节 | 🛠️ 工程决策:你该选哪个,怎么用对 | 场景适配表、常见选型误区、落地检查清单 | 把原理对比转化为可操作的工程判断 |
📖 第一节:为什么需要对比这两个引擎
1.1 从一个真实的困惑说起
小 B 刚入职一家做 AI 应用的团队,Leader 让他调研工作流引擎。
他打开 Dify,发现工作流可以拖拖拽拽就能搭出一个内容审核流程:开始 → LLM 生成 → 条件判断 → 人工确认 → 发布。他觉得很方便。
然后他去看 LangGraph,发现同样的流程要用代码写:StateGraph、add_node、add_conditional_edges、compile()。他觉得有点麻烦。
于是他的第一反应是:"Dify 适合业务人员,LangGraph 适合开发者,对吧?"
这个理解有一定道理,但远远不够。
因为一旦你需要回答下面这些问题,上面的分层就站不住了:
- 如果 Dify 的工作流在运行中途卡住了,你能基于检查点恢复它吗?恢复粒度是什么?
- 如果 LangGraph 的某个节点执行失败了,框架会怎么处理下游节点?是跳过还是阻断?
- 两个引擎在并发执行多个节点时,各自如何保证状态一致性?
- 两个引擎的"暂停等人输入"机制,在实现层面是不是一回事?
- 如果你的系统需要同时支持"可视化编排"和"代码级调试",你该怎么选底层引擎?
这些问题不是靠"谁更可视化"就能回答的。它们触及的是执行引擎的架构设计哲学。
1.2 两个引擎的定位:不是竞品,是不同层面的抽象
在进入技术细节之前,先把两者的定位澄清清楚:
| 维度 | Dify Graph Engine | LangGraph |
|---|---|---|
| 定位 | AI 应用平台的核心工作流执行内核 | 面向 LLM Agent 的状态化图运行时 |
| 图定义方式 | JSON 配置 + 可视化编辑器 | Python 代码 + StateGraph API |
| 主要用户 | 低代码平台用户、AI 应用开发者 | Python 开发者、Agent 系统构建者 |
| 状态模型 | 全局变量池(VariablePool),基于 selector 寻址 |
类型化 State,基于 Channel + Reducer 更新 |
| 执行模型 | 队列 + 线程池 + 事件分发(imperative) | Pregel superstep + 统一写入(declarative/functional) |
| 持久化 | 显式快照序列化(dumps() / loads()) |
内置 Checkpointer,支持多种后端 |
| 人机协作 | 节点发出 PauseRequestedEvent,外部通过命令通道恢复 |
interrupt() 一等公民,Command(resume=...) 恢复 |
| 典型场景 | 企业内部 AI 工作流、自动化审批、RAG 流水线 | 多 Agent 协作、对话式 Agent、需要人工介入的决策流 |
关键洞察:
Dify 的图引擎是为了让一个 AI 应用平台能跑起来各种业务工作流 而设计的;LangGraph 是为了让 LLM Agent 的执行过程变得可观测、可恢复、可协作而设计的。
这个定位差异会贯穿全文。理解它,你就不会在"谁更好"的问题上纠结,而是会问"谁更适合我的场景"。
1.3 本文要回答的核心问题
Dify 和 LangGraph 的图执行引擎,在"图怎么定义""运行时怎么推进""状态怎么流转""中断怎么恢复"这四个维度上,各采用了什么架构设计?这些设计差异会导致什么样的工程后果?
为了把这个问题讲透,我们会围绕一个共同的业务场景展开:
输入一个主题 → LLM 生成草稿 → 判断风险等级 →
├─ 低风险:直接发布
└─ 高风险:暂停等待人工审核 → 审核通过后发布
这个场景足够小,但已经覆盖了图定义、条件分支、状态流转、暂停恢复四个关键点。
下面我们先从"图是怎么被定义出来的"开始。
📖 第二节:图定义层------配置驱动 vs 代码驱动
2.1 Dify:JSON 是图的"第一公民"
在 Dify 里,一个工作流在数据库里是这样存的:
json
{
"nodes": [
{
"id": "start_node",
"data": { "type": "start", "title": "开始" }
},
{
"id": "llm_node",
"data": {
"type": "llm",
"title": "生成草稿",
"model": "gpt-4",
"prompt": "请根据主题{{#start_node.topic#}}生成一篇草稿"
}
},
{
"id": "if_else_node",
"data": { "type": "if-else", "title": "风险判断" }
},
{
"id": "human_node",
"data": { "type": "human-input", "title": "人工审核" }
},
{
"id": "end_node",
"data": { "type": "end", "title": "发布" }
}
],
"edges": [
{ "source": "start_node", "target": "llm_node" },
{ "source": "llm_node", "target": "if_else_node" },
{ "source": "if_else_node", "target": "human_node", "sourceHandle": "true" },
{ "source": "if_else_node", "target": "end_node", "sourceHandle": "false" },
{ "source": "human_node", "target": "end_node" }
]
}
这串 JSON 就是 Dify 工作流的全部静态定义。 前端画布上拖拖拽拽,本质上就是在生成和修改这段 JSON。
从 JSON 到可执行对象
Dify 的运行时不是直接跑 JSON,而是先把它"物化"(materialize)成对象:
python
# api/dify_graph/graph/graph.py
class Graph:
@classmethod
def init(cls, *, graph_config: Mapping[str, object], node_factory: NodeFactory, ...):
# 1. 从 JSON 里解析出边的配置
edge_configs = graph_config.get("edges", [])
# 2. 从 JSON 里解析出节点的配置
node_configs = graph_config.get("nodes", [])
# 3. 找到根节点(通常是 START 节点)
root_node_id = cls._find_root_node_id(node_configs_map, edge_configs, root_node_id)
# 4. 把边的配置变成 Edge 对象
edges, in_edges, out_edges = cls._build_edges(edge_configs)
# 5. 用工厂把每个节点配置变成具体的 Node 实例
nodes = cls._create_node_instances(node_configs_map, node_factory)
# 6. 构建 Graph 对象
graph = cls(nodes=nodes, edges=edges, in_edges=in_edges, out_edges=out_edges, root_node=root_node)
# 7. 校验图是否合法(比如是否有环、是否有孤立节点)
get_graph_validator().validate(graph)
return graph
这里有几个值得注意的设计点:
- Node 工厂模式 :每个节点类型(LLM、条件判断、人工输入等)都通过
NodeFactory动态创建。新节点类型只需要注册到NODE_TYPE_CLASSES_MAPPING,不需要改引擎核心代码。 - 边带
sourceHandle:sourceHandle用于条件分支,比如"sourceHandle": "true"表示这条边是"条件为真"时走的分支。 - 运行时和定义分离 :
Graph对象是可执行的,但它是从纯数据(JSON)构造出来的。这意味着图的结构和运行时可以完全解耦。
工程意义
生成/修改
存储
读取
物化
交给
前端画布
JSON 配置
数据库 workflow.graph_dict
Graph.init
Graph + Node + Edge 对象
GraphEngine 执行
这个设计的最大好处是:前端、后端、执行引擎可以独立演进。 前端只负责生成合法的 JSON;引擎只负责把 JSON 跑起来。两边通过 JSON 契约交互。
代价是:图的表达能力受限于 JSON schema。 如果你需要一个 JSON 里描述不了的复杂控制流,就得改 Dify 的核心 schema 和前端画布。
2.2 LangGraph:代码是图的"第一公民"
LangGraph 没有 JSON 配置层。图是用 Python 代码直接声明的:
python
from typing_extensions import TypedDict, Literal
from langgraph.graph import StateGraph, START, END
from langgraph.types import Command, interrupt
# 1. 先定义状态结构
class ArticleState(TypedDict, total=False):
topic: str
draft: str
risk_level: str
approved: bool
publish_note: str
# 2. 定义节点函数
def plan(state: ArticleState):
topic = state["topic"]
risk = "high" if "金融" in topic else "low"
return {
"draft": f"关于《{topic}》的初稿",
"risk_level": risk,
}
def review(state: ArticleState):
if state["risk_level"] == "high":
approved = interrupt({
"message": "该内容涉及高风险主题,请人工确认",
"draft": state["draft"],
})
return {"approved": bool(approved)}
return {"approved": True}
def publish(state: ArticleState):
if not state["approved"]:
return {"publish_note": "已终止发布"}
return {"publish_note": f"已发布:{state['draft']}"}
def decide_after_review(state: ArticleState) -> Literal["publish", END]:
return "publish" if state["approved"] else END
# 3. 用 StateGraph 组装图
builder = StateGraph(ArticleState)
builder.add_node("plan", plan)
builder.add_node("review", review)
builder.add_node("publish", publish)
builder.add_edge(START, "plan")
builder.add_edge("plan", "review")
builder.add_conditional_edges("review", decide_after_review)
builder.add_edge("publish", END)
# 4. 编译成可执行对象
graph = builder.compile(checkpointer=InMemorySaver())
从代码到运行时
LangGraph 的转折点也在 compile():
python
# langgraph/graph/state.py 中 StateGraph.compile() 的简化逻辑
def compile(self, checkpointer=None, ...):
# 1. 基于 State 的 schema 构建 channels
channels = self._build_channels()
# 2. 把节点函数包装成 Pregel 可调用的任务
nodes = self._compile_nodes(channels)
# 3. 把条件边编译成路由函数
edges = self._compile_edges(channels)
# 4. 生成 CompiledStateGraph(本质上是一个 Pregel 对象)
return CompiledStateGraph(
nodes=nodes,
channels=channels,
edges=edges,
checkpointer=checkpointer,
...
)
这里的关键设计点是:
- 状态即契约 :
ArticleState这个TypedDict不只是类型提示,它决定了哪些字段会被当作共享状态,以及每个字段的Reducer行为(覆盖还是追加)。 - 编译时绑定运行时 :
compile()不只是"生成一个可调用对象",它还会根据State的字段类型,为每个字段创建对应的Channel,并绑定Reducer合并规则。 - 图和代码同构 :图的结构就是代码的结构,没有中间的 JSON 层。这意味着图的表达能力等于 Python 的表达能力。
工程意义
add_node/add_edge/compile
invoke/stream
开发者写 Python 代码
CompiledStateGraph
运行时执行
这个设计的最大好处是:表达能力极强。 节点可以是任何 Python 函数,条件逻辑可以写任意复杂的判断,状态结构可以嵌套、可以泛型。
代价是:没有可视化。 如果你想让非开发者编辑流程,就必须自己再搭一层可视化工具,把拖拽动作翻译成 LangGraph 的 Python 代码。
2.3 对比:两种定义哲学的工程后果
| 对比维度 | Dify(JSON 配置驱动) | LangGraph(代码驱动) |
|---|---|---|
| 表达上限 | 受限于 JSON schema,复杂逻辑需要扩展 schema | 受限于 Python,几乎无上限 |
| 可视化友好度 | 原生支持,JSON 和画布一一对应 | 不支持,需要额外工具 |
| 版本控制 | JSON diff 清晰,但大文件难 review | 代码 diff 自然,可读性好 |
| 动态修改 | 运行时替换 JSON 即可重新物化图 | 需要重新 compile(),或设计动态子图 |
| 类型安全 | 运行时校验,配置错误可能在执行时才暴露 | 借助 Python 类型系统,State 和 Reducer 可静态检查 |
| 扩展节点类型 | 注册到 NODE_TYPE_CLASSES_MAPPING,前后端都要改 |
直接写新函数,add_node() 即可 |
| 工程适用场景 | 平台型产品、需要让业务人员自助配置流程 | 开发者工具、复杂 Agent 系统、需要深度定制的场景 |
一句话判断规则:
如果你的核心需求是"让非开发者能搭工作流" → Dify 的 JSON + 画布模式更合适。如果你的核心需求是"用代码精确控制 Agent 的每一步行为" → LangGraph 的代码模式更合适。
但定义层只是起点。真正决定两个引擎差异的,是运行时怎么执行这些定义。这就是下一节的内容。
📖 第三节:执行运行时------事件驱动线程池 vs Pregel 状态推进
这一节是全文的核心。
先给一个压缩版答案,然后再拆开:
Dify 的执行引擎是一个基于队列和线程池的"事件驱动编排器":节点完成后发出事件,调度器根据事件决定下一步该跑谁。LangGraph 的执行引擎是一个基于 Pregel 思想的"状态推进系统":每一轮先选出任务集,执行节点并收集写入,再统一提交状态更新,然后决定是否写检查点、是否中断。
如果你第一次看有点密,不急。我们用同一个业务场景,分别看两个引擎是怎么跑的。
3.1 Dify 的运行时:GraphEngine + WorkerPool + Dispatcher
Dify 的执行入口是 GraphEngine.run(),它返回一个事件生成器:
python
# api/dify_graph/graph_engine/graph_engine.py
class GraphEngine:
def run(self) -> Generator[GraphEngineEvent, None, None]:
# 1. 初始化各层中间件(比如日志、监控)
self._initialize_layers()
# 2. 判断是首次执行还是恢复执行
is_resume = self._graph_execution.started
if not is_resume:
self._graph_execution.start()
# 3. 启动执行:创建 WorkerPool 和 Dispatcher
self._start_execution(resume=is_resume)
# 4. 以生成器方式产出事件(调用方可逐步消费)
yield from self._event_manager.emit_events()
# 5. 处理最终状态(完成 / 暂停 / 中止 / 失败)
...
GraphEngine 内部由多个子系统组成:
GraphEngine 内部结构
GraphEngine
GraphStateManager
管理节点/边状态
WorkerPool
1-5 个线程
Dispatcher
单线程事件分发
EventManager
事件收集与产出
EventHandler
处理节点完成/失败/暂停
EdgeProcessor
决定下游节点
CommandProcessor
处理外部命令
ready_queue
待执行节点队列
event_queue
事件队列
一次执行的完整路径
假设我们的"生成草稿 → 风险判断 → 人工审核 → 发布"流程首次执行:
第一步:启动 WorkerPool 和 Dispatcher
python
# api/dify_graph/graph_engine/worker_management/worker_pool.py
class WorkerPool:
def start(self, initial_count: int | None = None) -> None:
# 根据图的节点数自动决定线程数
node_count = len(self._graph.nodes)
if node_count < 10:
initial_count = self._config.min_workers # 默认 1
elif node_count < 50:
initial_count = min(self._config.min_workers + 1, self._config.max_workers)
else:
initial_count = min(self._config.min_workers + 2, self._config.max_workers)
# 创建并启动工作线程
for i in range(initial_count):
worker = Worker(...)
worker.start()
self._workers.append(worker)
Worker 是一个 threading.Thread,它的核心逻辑是:
python
# api/dify_graph/graph_engine/worker.py
class Worker(threading.Thread):
def run(self) -> None:
while not self._stop_event.is_set():
try:
# 从 ready_queue 取一个待执行的节点 ID
node_id = self._ready_queue.get(timeout=0.1)
except queue.Empty:
continue
node = self._graph.nodes[node_id]
try:
# 执行这个节点
self._execute_node(node)
self._ready_queue.task_done()
except Exception as e:
# 如果执行失败,把错误事件推进 event_queue
self._event_queue.put(NodeRunFailedEvent(node_id=node_id, error=e))
第二步:把根节点放入 ready_queue
python
# GraphEngine._start_execution
if not resume:
root_node = self._graph.root_node
self._state_manager.enqueue_node(root_node.id)
self._state_manager.start_execution(root_node.id)
这里 enqueue_node 把节点 ID 推进 ready_queue,Worker 线程立刻就能取到并开始执行 start_node。
第三步:Worker 执行节点,产出事件
节点执行完成后,会产出 NodeRunSucceededEvent 并推进 event_queue:
python
# 节点执行成功后的简化逻辑
event = NodeRunSucceededEvent(
node_id=node.id,
node_run_result=NodeRunResult(outputs={"topic": "金融营销文案"})
)
self._event_queue.put(event)
第四步:Dispatcher 消费事件,决定下一步
Dispatcher 是一个独立的线程,它不断从 event_queue 取事件并处理:
python
# api/dify_graph/graph_engine/orchestration/dispatcher.py
class Dispatcher:
def _dispatcher_loop(self) -> None:
while not self._stop_event.is_set():
if self._execution_coordinator.aborted or self._execution_coordinator.execution_complete:
break
if self._execution_coordinator.paused:
break
try:
event = self._event_queue.get(timeout=0.1)
self._event_handler.dispatch(event) # 关键:处理事件
self._event_queue.task_done()
except queue.Empty:
time.sleep(0.1)
事件处理的核心在 EventHandler:
python
# api/dify_graph/graph_engine/event_management/event_handlers.py
@EventHandler.register(NodeRunSucceededEvent)
def _(self, event: NodeRunSucceededEvent) -> None:
# 1. 把节点的输出存到全局变量池
self._store_node_outputs(event.node_id, event.node_run_result.outputs)
# 2. 根据当前节点的类型,决定怎么处理下游边
node = self._graph.nodes[event.node_id]
if node.execution_type == NodeExecutionType.BRANCH:
# 分支节点:根据 source_handle 决定走哪条边
ready_nodes, edge_streaming_events = self._edge_processor.handle_branch_completion(
event.node_id, event.node_run_result.edge_source_handle
)
else:
# 普通节点:所有出边都"通过"
ready_nodes, edge_streaming_events = self._edge_processor.process_node_success(event.node_id)
# 3. 把下游节点推进 ready_queue
if self._graph_execution.is_paused:
# 如果图被暂停了,先寄存起来,等恢复时再执行
for node_id in ready_nodes:
self._graph_runtime_state.register_deferred_node(node_id)
else:
for node_id in ready_nodes:
self._state_manager.enqueue_node(node_id)
self._state_manager.start_execution(node_id)
# 4. 标记当前节点执行完毕
self._state_manager.finish_execution(event.node_id)
第五步:循环直到没有可执行节点
当 ready_queue 为空、event_queue 也为空,且没有暂停节点时,Dispatcher 判断执行完成,设置 execution_complete = True,整个 run() 结束。
Dify 运行时的关键特征
| 特征 | 说明 | 工程后果 |
|---|---|---|
| 生产者-消费者模式 | ready_queue + event_queue 双队列 |
解耦了"节点执行"和"图遍历调度",Worker 只管跑节点,Dispatcher 只管决定下一步 |
| 多线程并发 | WorkerPool 默认 1-5 个线程 | 无依赖的节点可以并行执行,但要注意 Python GIL 对 CPU 密集型任务的限制 |
| 事件驱动 | 节点完成 → 发事件 → Dispatcher 处理 → 决定下游 | 架构清晰,但事件处理是单线程的,如果事件处理逻辑太重,会成为瓶颈 |
| 状态即时可见 | 节点输出立刻写入 VariablePool,下游节点立刻能读到 |
简单直观,但如果两个节点并行写同一个变量,存在竞态风险 |
3.2 LangGraph 的运行时:Pregel Loop
LangGraph 的入口是 graph.invoke(),但它本质上是对 stream() 的封装:
python
# langgraph/pregel/__init__.py 中 invoke 的简化逻辑
def invoke(self, input, config=None, ...):
# invoke 就是包了一层 stream,把流式结果消费完,返回最终值
latest = None
for chunk in self.stream(input, config, stream_mode="values", ...):
latest = chunk
return latest
真正的执行核心在 stream() → PregelLoop:
Checkpointer 节点函数 PregelRunner SyncPregelLoop graph.stream graph.invoke 调用方 Checkpointer 节点函数 PregelRunner SyncPregelLoop graph.stream graph.invoke 调用方 alt [节点内部调用 interrupt()] loop [每个 superstep] invoke(input, config) 转成 stream 执行 创建 PregelLoop(runtime、thread、checkpoint) prepare_next_tasks(基于当前 checkpoint 与 channel versions) 下发本轮可执行任务 调用节点函数 返回 state writes / 控制信号 抛出 GraphInterrupt 汇总本轮任务结果 apply_writes(统一更新 channels / state) 保存 checkpoint 发出 values / updates / checkpoints / tasks 事件 返回最终 state 输出结果
一次 superstep 内部具体做什么
第一步:prepare_next_tasks()
LangGraph 不会"遍历所有节点"。它会基于当前 checkpoint 里的信息,动态决定这一轮该跑哪些节点:
python
# 简化逻辑:基于 channel version 和边关系选任务
# 哪些 channel 在上一轮被更新了?
# 哪些节点会监听这些 channel?
# 当前有没有待消费的 Send 任务?
tasks = self._prepare_next_tasks(checkpoint, channels, ...)
这个设计的关键点是:节点的触发不是静态顺序,而是基于状态变更的动态订阅。
第二步:PregelRunner.tick() 执行节点
Runner 会去调用节点函数。节点函数的返回值不是"直接修改全局状态",而是"提出写入请求"(writes):
python
def plan(state: ArticleState):
topic = state["topic"]
risk = "high" if "金融" in topic else "low"
# 返回的不是"直接改 state",而是"建议写入这些字段"
return {
"draft": f"关于《{topic}》的初稿",
"risk_level": risk,
}
第三步:apply_writes() 统一提交
这一步是 Pregel 模型的核心:
同一轮(superstep)内,节点产生的 writes 不会立刻对其他节点可见。等这一轮所有节点都执行完,才统一把 writes 应用到 state 上。
python
# 简化逻辑
for write in all_writes_this_step:
channel = channels[write.channel_name]
# 用 Reducer 合并多路写入
channel.update(write.value)
这个设计的好处:
- 并发安全:同轮节点看到的是一致的"上一轮结束后的状态快照",不会因为别的节点"半途写入"而看到混乱的中间态。
- 可预测:每一轮的推进边界非常清晰,调试和恢复都很稳定。
代价是:节点之间不能"实时共享"状态,必须通过 superstep 边界来传递。
第四步:写 checkpoint 并判断是否中断
python
# 保存本轮运行快照
self._put_checkpoint(checkpoint)
# 检查是否触发 interrupt
if self._should_interrupt(...):
raise GraphInterrupt(...)
LangGraph 运行时的关键特征
| 特征 | 说明 | 工程后果 |
|---|---|---|
| Superstep 边界 | 每轮"选任务 → 执行 → 统一提交"是一个完整边界 | 状态更新可预测,但节点之间无法实时共享中间结果 |
| Channel + Reducer | 状态不是直接覆盖,而是通过 Channel 的 Reducer 规则合并 | 支持复杂合并逻辑(如列表追加、字典归并),但学习成本高 |
| 单线程调度 | PregelLoop 本身是单线程推进的 | 调度逻辑简单可靠,但节点执行可以是并发的(通过 asyncio / 线程池) |
| 检查点内建 | 每个 superstep 结束自动写 checkpoint | 恢复粒度细,但写 checkpoint 有性能开销 |
3.3 对比:两种执行模型的本质差异
| 对比维度 | Dify(事件驱动线程池) | LangGraph(Pregel 状态推进) |
|---|---|---|
| 调度方式 | Dispatcher 单线程消费事件,决定下游 | PregelLoop 按 superstep 推进,动态选任务 |
| 并发模型 | WorkerPool 多线程执行节点 | Runner 可以并发执行,但状态提交是单点统一的 |
| 状态可见性 | 节点输出立刻写入 VariablePool,下游马上可见 | 节点 writes 到 superstep 结束才统一提交,同轮不可见 |
| 恢复粒度 | 显式序列化 GraphRuntimeState,粒度由实现决定 |
每个 superstep 结束自动 checkpoint,粒度固定 |
| 调试体验 | 需要追踪事件流和 VariablePool 变化 | 可以逐 superstep 回放,状态变更有明确边界 |
| 心智模型 | "节点跑完发事件,调度器决定下一步" | "每轮选一批任务,跑完统一提交,推进到下一轮" |
最重要的工程判断:
如果你的流程需要节点之间频繁共享中间结果 (比如 A 跑一半 B 就要读 A 的输出),Dify 的"即时可见"模型更直观。如果你需要严格的执行边界和可恢复性(比如人工审核后必须从精确位置恢复),LangGraph 的"superstep + checkpoint"模型更可靠。
📖 第四节:状态管理------全局变量池 vs Channel + Reducer
状态管理是两个引擎差异最显著的领域之一。Dify 用的是"全局变量池 + selector 寻址",LangGraph 用的是"类型化 State + Channel + Reducer"。
4.1 Dify:VariablePool 是唯一的"数据源"
在 Dify 里,所有节点共享同一个 VariablePool 实例:
python
# api/dify_graph/runtime/variable_pool.py
class VariablePool(BaseModel):
# 核心存储:node_id -> {variable_name -> Variable}
variable_dictionary: defaultdict[str, dict[str, Variable]] = Field(default=defaultdict(dict))
user_inputs: Mapping[str, Any] = Field(default_factory=dict)
system_variables: SystemVariable = Field(default_factory=SystemVariable.default)
environment_variables: Sequence[Variable] = Field(default_factory=list)
conversation_variables: Sequence[Variable] = Field(default_factory=list)
def add(self, selector: Sequence[str], value: Any, /):
# selector = [node_id, variable_name]
# 比如 add(("llm_node", "draft"), "关于金融的草稿")
node_id, name = self._selector_to_keys(selector)
self.variable_dictionary[node_id][name] = Variable(name=name, value=value)
def get(self, selector: Sequence[str], /) -> Segment | None:
# 支持嵌套访问:["llm_node", "draft", "content"]
...
节点怎么读写状态
写:节点执行完成后,输出被存入 VariablePool:
python
# EventHandler 处理节点成功事件时
self._store_node_outputs("llm_node", {"draft": "关于金融的草稿", "tokens": 150})
# 实际调用的是:
# variable_pool.add(("llm_node", "draft"), "关于金融的草稿")
# variable_pool.add(("llm_node", "tokens"), 150)
读:下游节点通过模板语法引用上游输出:
json
{
"type": "llm",
"prompt": "请润色这篇草稿:{{#llm_node.draft#}}"
}
运行时引擎会解析 {``{#llm_node.draft#}},转换成 selector ["llm_node", "draft"],然后从 VariablePool 取值。
并发问题
Dify 的 VariablePool 是同一个实例被所有节点共享的。GraphEngine 启动时甚至会校验这一点:
python
def _validate_graph_state_consistency(self) -> None:
expected_state_id = id(self._graph_runtime_state)
for node in self._graph.nodes.values():
if id(node.graph_runtime_state) != expected_state_id:
raise ValueError("节点使用了不同的 runtime state 实例!")
这意味着:
- 优点:状态读写简单直接,不需要额外的同步层。
- 风险 :如果两个 Worker 线程同时写同一个
node_id.variable_name,后写的会覆盖先写的,没有自动合并机制。
Dify 目前主要通过"DAG 无环"和"合理调度"来避免冲突,而不是通过状态合并规则。
4.2 LangGraph:State、Channel、Reducer 三层分离
LangGraph 的状态管理比 Dify 复杂得多,但也更精确。它有三层概念:
LangGraph 运行时视角
开发者视角
输入或恢复命令
节点执行
本轮 writes
Reducer 合并规则
Channel 更新与版本推进
对外可见的 State 视图
Checkpoint 快照
State:开发者看到的业务状态
python
class ArticleState(TypedDict, total=False):
topic: str
draft: str
risk_level: str
approved: bool
publish_note: str
这个 TypedDict 定义了图的"公共数据结构"。每个节点读写的就是这个结构。
Channel:运行时真正承载状态更新的载体
compile() 时,LangGraph 会为 ArticleState 的每个字段创建一个 Channel:
python
# 简化示意:为每个 state 字段创建对应 Channel
channels = {
"topic": LastValue(), # 最新值覆盖
"draft": LastValue(),
"risk_level": LastValue(),
"approved": LastValue(),
"publish_note": LastValue(),
}
Channel 的类型决定了多路写入时的合并行为:
| Channel 类型 | 合并行为 | 适用场景 |
|---|---|---|
LastValue |
后写入覆盖先写入 | 大多数字段(默认) |
Topic |
追加到列表 | 消息历史 |
BinaryOperatorAggregate |
自定义二元归并 | 计数器、累加器 |
Reducer:决定多路写入如何合并
Reducer 是在定义 State 时通过 Annotated 指定的:
python
from typing import Annotated
from operator import add
class ChatState(TypedDict):
messages: Annotated[list, add] # 用 operator.add 合并,即列表追加
count: Annotated[int, add] # 整数累加
并发安全从何而来
LangGraph 的并发安全不是通过锁实现的,而是通过执行模型实现的:
同一 superstep 内,所有节点读取的是上一轮结束时的状态快照;它们产生的 writes 先暂存,等本轮所有节点执行完后,才统一通过
apply_writes()+Reducer合并,写入 Channel。
这意味着:
- 节点之间**不存在"读到一个正在被修改的状态"**的情况。
- 多节点并发写同一个字段时,Reducer 规则决定最终值,而不是"谁最后写谁赢"。
4.3 对比:状态模型的工程后果
| 对比维度 | Dify VariablePool | LangGraph State + Channel + Reducer |
|---|---|---|
| 学习成本 | 低,就是全局变量池 + selector | 高,需要理解 State / Channel / Reducer 三层 |
| 并发安全 | 依赖 DAG 无环 + 调度顺序,同一字段并行写有覆盖风险 | 通过 superstep 边界 + Reducer 合并天然安全 |
| 合并能力 | 无内置合并机制,后写覆盖 | Reducer 支持列表追加、累加、自定义归并 |
| 调试难度 | 需要追踪 VariablePool 的所有 add 操作 | 可以逐 superstep 看 checkpoint,变更有明确边界 |
| 类型安全 | 运行时动态类型,配置错误可能在执行时才暴露 | 借助 Python 类型提示,State 结构可静态检查 |
| 适用场景 | 简单工作流、节点之间数据依赖清晰 | 复杂 Agent、多节点协作、需要精细状态控制的场景 |
一句话判断规则:
如果你的状态主要是"节点 A 产出 X,节点 B 消费 X"的简单传递 → Dify 的 VariablePool 足够。如果你的状态需要"多个节点同时追加消息历史"或"复杂归并逻辑" → LangGraph 的 Channel + Reducer 更合适。
📖 第五节:暂停/恢复与容错------两种中断哲学
5.1 Dify:节点驱动暂停 + 命令通道恢复
Dify 的暂停是由节点自己决定的。以 HumanInputNode 为例:
python
# api/dify_graph/nodes/human_input/human_input_node.py
class HumanInputNode(Node[HumanInputNodeData]):
def _run(self) -> Generator[NodeEventBase, None, None]:
# 1. 先看看这个节点之前有没有创建过表单
form = repo.get_form(self._workflow_execution_id, self.id)
if form is None:
# 第一次执行:创建表单,然后发出暂停请求
form_entity = self._form_repository.create_form(params)
yield self._form_to_pause_event(form_entity)
return # 节点在这里结束,图进入暂停状态
if not form.submitted:
# 表单还没被提交:继续暂停
yield self._form_to_pause_event(form)
return
# 表单已提交:把表单数据作为输出返回
yield NodeRunSucceededEvent(...)
暂停时发生了什么
python
# EventHandler 处理 PauseRequestedEvent
@EventHandler.register(NodeRunPauseRequestedEvent)
def _(self, event: NodeRunPauseRequestedEvent) -> None:
# 1. 标记图进入暂停状态
self._graph_execution.pause(event.reason)
# 2. 标记当前节点执行完毕(但不是成功,是暂停)
self._state_manager.finish_execution(event.node_id)
# 3. 把节点状态重置为 UNKNOWN(这样恢复时可以重新执行)
self._graph.nodes[event.node_id].state = NodeState.UNKNOWN
# 4. 记录暂停节点,方便恢复时找到它
self._graph_runtime_state.register_paused_node(event.node_id)
# 5. 收集事件并产出
self._event_collector.collect(event)
恢复时发生了什么
python
# GraphEngine._start_execution 的恢复逻辑
if resume:
# 1. 取出之前暂停的节点
paused_nodes = self._graph_runtime_state.consume_paused_nodes()
# 2. 取出暂停期间被推迟的下游节点
deferred_nodes = self._graph_runtime_state.consume_deferred_nodes()
# 3. 把这些节点重新放入 ready_queue
for node_id in paused_nodes + deferred_nodes:
self._state_manager.enqueue_node(node_id)
self._state_manager.start_execution(node_id)
外部如何触发恢复
Dify 使用**命令通道(CommandChannel)**来接收外部控制指令:
python
# 生产环境通常用 Redis 作为命令通道
command_channel = RedisChannel(redis_client, f"workflow:{task_id}:commands")
# 外部系统(比如前端用户点了"确认")发送恢复命令
command_channel.send(ResumeCommand())
序列化与恢复
Dify 的恢复依赖显式的状态序列化:
python
# GraphRuntimeState.dumps() 会序列化整个运行时状态
snapshot = {
"version": "1.0",
"variable_pool": self.variable_pool.model_dump(mode="json"),
"ready_queue": self.ready_queue.dumps(),
"graph_execution": self.graph_execution.dumps(),
"paused_nodes": list(self._paused_nodes),
"graph_state": self._snapshot_graph_state(),
}
恢复时,GraphRuntimeState.from_snapshot() 会重建整个状态。
5.2 LangGraph:interrupt 是一等公民
LangGraph 的暂停机制更简洁,但背后的设计哲学不同。
节点内部调用 interrupt()
python
from langgraph.types import interrupt
def review(state: ArticleState):
if state["risk_level"] == "high":
# interrupt() 会抛出 GraphInterrupt,图暂停在这里
approved = interrupt({
"message": "请人工确认是否允许发布",
"draft": state["draft"],
})
return {"approved": bool(approved)}
return {"approved": True}
恢复时节点会重新执行
这是 LangGraph 最容易踩坑的地方:
恢复后,节点会从头重新执行一遍,而不是从
interrupt()的下一行继续。
python
# 第一次执行:跑到 interrupt(),暂停
# 第二次恢复:整个 review 函数重新执行
# → state["risk_level"] 还是 "high"
# → 再次进入 if 分支
# → 但这次 interrupt() 会返回 resume 值,而不是暂停
# → 函数继续执行,返回 {"approved": True}
这个设计带来一个重要的工程约束:
interrupt()之前的代码必须是幂等的,或者没有副作用。 如果你先发了邮件再 interrupt,恢复后可能会再发一次邮件。
恢复方式
python
# 用同一个 thread_id 恢复
config = {"configurable": {"thread_id": "demo-1"}}
# 第一次执行,会在 review 节点暂停
result1 = graph.invoke({"topic": "金融营销文案"}, config)
# 人工确认后,传入 Command(resume=True) 恢复
result2 = graph.invoke(Command(resume=True), config)
thread_id 是恢复的关键。同一个 thread_id 才能找到对应的 checkpoint。
Checkpoint 与 Interrupt 的绑定
LangGraph 的 checkpoint 不只是"存状态",它还存了:
- 当前 state values
next:下一步要执行哪些节点- interrupt 元数据
- channel versions
- 父检查点关系
所以恢复时,LangGraph 知道:
- 上次停在哪一轮
- 下一步该跑哪个节点
- 那个节点里的
interrupt()该返回什么 resume 值
5.3 对比:中断模型的工程后果
| 对比维度 | Dify | LangGraph |
|---|---|---|
| 暂停触发方式 | 节点发出 PauseRequestedEvent |
节点调用 interrupt() |
| 恢复粒度 | 可以精确恢复单个节点 | 节点会重新执行,不是原地续行 |
| 幂等性要求 | 相对较低(节点不会自动重跑) | 必须保证 interrupt 前逻辑幂等 |
| 外部恢复方式 | 通过 CommandChannel 发送命令 | 通过 Command(resume=...) + thread_id |
| 状态持久化 | 显式 dumps() / loads(),需要自行管理 |
内置 Checkpointer,自动持久化 |
| 调试体验 | 需要追踪事件流和状态快照 | 可以 get_state(config) 查看完整运行状态 |
最重要的工程判断:
如果你的暂停场景主要是"等人填表单、等人审批",且节点逻辑不需要重跑 → Dify 的模型更省心。如果你的暂停场景需要"精确恢复到某一步,且框架自动处理重跑语义" → LangGraph 的
interrupt()+checkpoint更强大,但对幂等性要求更高。
📖 第六节:工程决策------你该选哪个,怎么用对
6.1 场景适配表
| 场景 | 推荐选择 | 原因 |
|---|---|---|
| 企业内部 AI 工作流平台,需要让业务人员自助搭建 | Dify | JSON + 可视化画布是平台型产品的最佳抽象 |
| 复杂多 Agent 协作系统,需要精细控制每个节点的行为 | LangGraph | 代码级控制 + Pregel 状态模型更适合 Agent 系统 |
| 需要频繁人工审核、表单填写、审批流 | 两者皆可 | Dify 的 HumanInputNode 开箱即用;LangGraph 的 interrupt() 更灵活但需更多代码 |
| 高并发、大批量数据处理流水线 | 谨慎评估 | 两者都不是专门的批处理引擎,Dify 的线程池和 LangGraph 的 Python 运行时都有瓶颈 |
| 需要严格的可恢复性、审计、逐 step 回放 | LangGraph | 内置 checkpoint 机制更成熟 |
| 需要快速原型验证、MVP 开发 | Dify | 可视化搭建 + 内置节点类型,上手更快 |
6.2 常见选型误区
误区 1:"Dify 是低代码,LangGraph 是专业开发,所以复杂项目一定选 LangGraph"
不一定。Dify 的图引擎本身是非常成熟的,它的 JSON 配置层并不限制运行时的复杂度。如果你的"复杂"是指"流程步骤多、分支多",Dify 完全可以胜任。如果你的"复杂"是指"每个节点的逻辑需要精细控制、状态合并规则复杂",LangGraph 更合适。
误区 2:"LangGraph 有 checkpoint,所以更可靠"
Dify 也有显式的状态序列化和恢复机制。"可靠"不取决于有没有 checkpoint,而取决于:
- 恢复粒度是否满足业务需求
- 状态持久化是否满足一致性要求
- 节点逻辑是否满足幂等性要求
误区 3:"两个引擎可以无缝互替"
不能。它们的图定义方式、状态模型、执行语义都不一样。如果你已经用 Dify 搭了一套工作流,想迁移到 LangGraph,几乎等于重写。反之亦然。
6.3 落地检查清单
无论你选哪个,接入生产前确认以下事项:
- 图的定义方式是否与团队的技能栈匹配?(JSON 配置 vs Python 代码)
- 状态管理机制是否满足并发安全要求?
- 暂停/恢复机制是否满足业务场景的幂等性要求?
- 检查点/快照的存储后端是否可靠?(数据库、Redis、文件系统等)
- 失败节点的重试策略是否已配置?
- 超时控制是否已设置?(节点级、图级)
- 运行时事件是否可被观测?(日志、监控、Tracing)
- 团队是否有能力 debug 执行引擎内部的问题?
✅ 最后帮你收成一句话
Dify 的图引擎是一个为了"让 AI 工作流跑起来"而设计的、基于事件驱动和线程池的 imperative 编排器;LangGraph 的图引擎是一个为了"让 Agent 执行过程可观测、可恢复"而设计的、基于 Pregel 思想的 declarative 状态推进系统。
它们不是谁比谁好,而是谁更适合你的场景:
- 要平台化、可视化、快速搭建 → Dify
- 要代码级控制、精细状态管理、强可恢复性 → LangGraph
理解了这个底层差异,你在技术选型时就不会再纠结于表面的功能对比,而是能从架构设计哲学的层面做出正确判断。
参考资料
本文对 Dify 的分析基于 dify/v1.13.0 Github开源项目(截至 2026 年 3 月),核心参考文件包括:
api/dify_graph/graph_engine/graph_engine.py--- 执行引擎核心api/dify_graph/graph/graph.py--- 图定义与物化api/dify_graph/runtime/variable_pool.py--- 状态管理api/dify_graph/graph_engine/worker_management/worker_pool.py--- 并发模型api/dify_graph/graph_engine/event_management/event_handlers.py--- 事件处理api/dify_graph/nodes/human_input/human_input_node.py--- 暂停机制
本文对 LangGraph 的分析基于项目目录内已发布的《LangGraph 入门与原理:从 invoke 到 Pregel、Checkpoint 和 Interrupt》一文,以及 langgraph==1.1.8 的源码实现。