跳到主要内容

02 · 执行引擎:先跑谁、循环怎么实现、环怎么跑

本章回答执行器最核心的两个问题:节点的执行顺序怎么来,以及**「循环 N 次」和「真正的图环」两种回路怎么实现**。

1. 三种执行策略,按图的形状选

建完图后,GraphExecutor.run 根据图的形状选一种策略(workflow/graph.py:301-326):

图形状策略怎么跑
配了 is_majority_votingMajorityVoteStrategy所有节点无边、全并行跑,最后对输出做众数投票
含环(has_cycles)CycleExecutionStrategy超节点调度 + 逐迭代
普通无环图DagExecutionStrategy拓扑分层、层内并行

判断含不含环在建图阶段做完(workflow/graph_manager.py:179-186):_detect_cycles 找环,有环走环调度,无环算 DAG 分层。

2. DAG:拓扑分层 + 层内并行 + 触发门控

它要解决的小问题。 无环图里,「B 依赖 A」就得 A 先跑。怎么自动排出这个顺序,还能让互不依赖的节点并行?

思路。 拓扑排序把图切成「层」:第 0 层是没有前驱的节点,第 1 层是只依赖第 0 层的……同一层内的节点彼此独立,可以并行。

DAGExecutor.execute(workflow/executor/dag_executor.py:40-55)逐层跑,每层用 ParallelExecutor 并行。关键细节在 _execute_layer:

# 示意,非源码 —— dag_executor.py:46-55 的核心
def execute_if_triggered(node_id):
node = nodes[node_id]
if node.is_triggered(): # ← 在拓扑顺序之上再加一道「触发门控」
execute_node_func(node)
else:
log("skipped - not triggered")

重点看这里:拓扑顺序只决定「轮到谁」,但轮到了不一定跑。 还得 is_triggered() 为真才真正执行。这就把「静态结构(谁在前)」和「动态数据流(谁真被点亮)」分开了——同一张图,不同输入会点亮不同分支。

3. loop_counter:不用环也能「循环 N 次」

它要解决的小问题。 软件开发流程要「复审循环最多 10 轮、测试循环最多 3 轮」。但如果用真正的图环,调度会复杂。能不能在 DAG 里就表达有限循环?

巧妙思路:把「循环计数」做成一个普通节点。 loop_counter 节点的执行逻辑出奇简单(runtime/node/executor/loop_counter_executor.py:16-52):

# 示意,非源码 —— 复刻 loop_counter 的核心语义
counter["count"] += 1
if counter["count"] < config.max_iterations:
return [] # ← 没到上限:返回空,下游边全部被「抑制」(没输出可搬)
if config.reset_on_emit:
counter["count"] = 0
return [Message(content=config.message or "Loop limit reached")] # 到上限才放行

它的全部魔法就一句:没到上限就返回空 list。 节点产出空输出时,执行器不会触发任何下游边(workflow/graph.py:605-612 把这种情况记为「produced no output; downstream edges suppressed」)。于是:

Programmer 写代码 ──► loop_counter ──► (空) 下游被抑制,本轮到此为止
▲ │
└───────────────────┘ 下一轮又有别的边把 Programmer 点亮,counter++
...重复直到 count == max_iterations,loop_counter 才吐出 message,放行进入下一阶段

reset_on_emit: true 让计数器放行后归零,这样同一个计数器能在外层循环里被反复复用。loop_timer 是同一招的「按时间」版本。

这是 2.0 的招牌设计之一:把控制流(循环、门闩)降维成数据流里的普通节点,执行器本身不需要懂「循环」概念。

4. 真正的图环:超节点抽象 + 逐迭代

当图里确实有回路(节点 A→B→A),不能简单分层。ChatDev 的做法是把每个强连通的环「打包」成一个超节点,这样「超节点图」就又变成 DAG,可以拓扑排序;轮到某个超节点时,再进去做多轮迭代。

建超节点图与排序在 GraphManager._build_cycle_execution_order(workflow/graph_manager.py:209-239),底层用 GraphTopologyBuilder.create_super_node_graph / topological_sort_super_nodes

环内的逐迭代执行在 CycleExecutor._execute_cycle_with_iterations(workflow/executor/cycle_executor.py:186-252),每轮做的事:

  1. 在环的范围内临时去掉入口节点的入边(把环「剪开」成 DAG),算出本轮的拓扑层(cycle_executor.py:215-223)。
  2. 跑这些层;若有边触发了环外的节点,说明该退出循环了,记录并返回(cycle_executor.py:434-546_execute_scope_layers / record_external)。
  3. 检查入口节点有没有被环内的边重新点亮——是则继续下一轮,否则结束(_is_initial_node_retriggered,cycle_executor.py:590-614)。
  4. 兜底:到 max_iterations 强制停。

入口唯一性有校验:一个环只能有一个被外部触发的入口节点,多于一个直接报错(_validate_cycle_entry,cycle_executor.py:139-184)。

直觉对比: loop_counter 是「在 DAG 里假装循环」,适合「固定跑 N 轮」;真正的图环是「执行器真的绕回去」,适合「跑到满足某条件才退出」。两者可叠加——环里也能放计数器当保险丝。

5. 多数投票:并行 + 取众数

MajorityVoteStrategy(workflow/runtime/execution_strategy.py:67-149)是个特例:图里没有边,所有节点拿同一份初始输入、全并行跑,最后对输出文本取众数(Counter.most_common,execution_strategy.py:121-132,优先在非空输出里取众数)。用途:同一问题让多个 agent 独立作答,投票降低单次幻觉。

6. 一次节点执行的完整收尾(对所有策略通用)

不论哪种策略,跑单个节点都走 GraphExecutor._execute_node(workflow/graph.py:547-655)。它做的事:

  1. 取该节点 input 队列、清掉自己的入边触发态(reset_triggers,等下一轮新信号)。
  2. 调对应 executor 产出输出消息(_process_resultnode_executors[type].execute)。
  3. context_window 清理 input(见第 04 章)。
  4. 对每条出边跑 _process_edge_output(算条件、搬数据、点亮)。
  5. 若节点 context_window != 0 且本次没恢复 trace,补一条「伪自环边」把输出回灌自己——用于带工具 trace 的上下文延续。

下一章: 钻进 agent 节点,看一次 LLM 调用从 prompt 组装到工具循环的全过程。