Dify 与 LangGraph 图执行引擎原理对比:从定义层到运行时的架构拆解

Dify 与 LangGraph 图执行引擎原理对比:从定义层到运行时的架构拆解

目标读者:初中级后端 / AI 应用开发者。你已经能写简单的 LLM 调用链,但面对 Dify 的可视化工作流和 LangGraph 的代码化图编排时,想搞清楚它们底层执行引擎的本质差异。

文章目录

    • [Dify 与 LangGraph 图执行引擎原理对比:从定义层到运行时的架构拆解](#Dify 与 LangGraph 图执行引擎原理对比:从定义层到运行时的架构拆解)
    • [📚 内容大纲](#📚 内容大纲)
  • [📖 第一节:为什么需要对比这两个引擎](#📖 第一节:为什么需要对比这两个引擎)
    • [1.1 从一个真实的困惑说起](#1.1 从一个真实的困惑说起)
    • [1.2 两个引擎的定位:不是竞品,是不同层面的抽象](#1.2 两个引擎的定位:不是竞品,是不同层面的抽象)
    • [1.3 本文要回答的核心问题](#1.3 本文要回答的核心问题)
  • [📖 第二节:图定义层------配置驱动 vs 代码驱动](#📖 第二节:图定义层——配置驱动 vs 代码驱动)
    • [2.1 Dify:JSON 是图的"第一公民"](#2.1 Dify:JSON 是图的"第一公民")
      • [从 JSON 到可执行对象](#从 JSON 到可执行对象)
      • 工程意义
    • [2.2 LangGraph:代码是图的"第一公民"](#2.2 LangGraph:代码是图的"第一公民")
    • [2.3 对比:两种定义哲学的工程后果](#2.3 对比:两种定义哲学的工程后果)
  • [📖 第三节:执行运行时------事件驱动线程池 vs Pregel 状态推进](#📖 第三节:执行运行时——事件驱动线程池 vs Pregel 状态推进)
    • [3.1 Dify 的运行时:GraphEngine + WorkerPool + Dispatcher](#3.1 Dify 的运行时:GraphEngine + WorkerPool + Dispatcher)
    • [3.2 LangGraph 的运行时:Pregel Loop](#3.2 LangGraph 的运行时:Pregel Loop)
      • [一次 superstep 内部具体做什么](#一次 superstep 内部具体做什么)
      • [LangGraph 运行时的关键特征](#LangGraph 运行时的关键特征)
    • [3.3 对比:两种执行模型的本质差异](#3.3 对比:两种执行模型的本质差异)
  • [📖 第四节:状态管理------全局变量池 vs Channel + Reducer](#📖 第四节:状态管理——全局变量池 vs Channel + Reducer)
  • [📖 第五节:暂停/恢复与容错------两种中断哲学](#📖 第五节:暂停/恢复与容错——两种中断哲学)
  • [📖 第六节:工程决策------你该选哪个,怎么用对](#📖 第六节:工程决策——你该选哪个,怎么用对)
    • [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,发现同样的流程要用代码写:StateGraphadd_nodeadd_conditional_edgescompile()。他觉得有点麻烦。

于是他的第一反应是:"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

这里有几个值得注意的设计点:

  1. Node 工厂模式 :每个节点类型(LLM、条件判断、人工输入等)都通过 NodeFactory 动态创建。新节点类型只需要注册到 NODE_TYPE_CLASSES_MAPPING,不需要改引擎核心代码。
  2. 边带 sourceHandlesourceHandle 用于条件分支,比如 "sourceHandle": "true" 表示这条边是"条件为真"时走的分支。
  3. 运行时和定义分离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,
        ...
    )

这里的关键设计点是:

  1. 状态即契约ArticleState 这个 TypedDict 不只是类型提示,它决定了哪些字段会被当作共享状态,以及每个字段的 Reducer 行为(覆盖还是追加)。
  2. 编译时绑定运行时compile() 不只是"生成一个可调用对象",它还会根据 State 的字段类型,为每个字段创建对应的 Channel,并绑定 Reducer 合并规则。
  3. 图和代码同构 :图的结构就是代码的结构,没有中间的 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 类型系统,StateReducer 可静态检查
扩展节点类型 注册到 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 的源码实现。

相关推荐
donglianyou2 小时前
Agent技术详解与实战
python·langchain·agent·langgraph
qq_372906932 小时前
如何处理SQL循环逻辑_探索递归CTE实现复杂计算
jvm·数据库·python
林深时见鹿v2 小时前
《后端开发全栈工具安装踩坑指南 & 经验沉淀手册》
java·人工智能·python·oracle
扬帆破浪2 小时前
察元 WPS AI助手技术手记:从源码构建到各平台安装与上手
人工智能·wps
zero.cyx2 小时前
更换Live2D模型具体步骤
人工智能·计算机视觉·语音识别
阿星AI工作室2 小时前
Codex登录又崩了?零基础用CCSwitch秒连教程
人工智能
扬帆破浪2 小时前
察元 WPS AI插件:工程边界与阅读地图
人工智能·开源·wps
m0_674294642 小时前
C#怎么使用Channel异步通道 C#如何用BoundedChannel实现有界队列限流异步数据流【进阶】
jvm·数据库·python
m0_748920362 小时前
HTML函数在系统更新后变卡是硬件老化吗_软硬兼容性排查【方法】
jvm·数据库·python