跳到主要内容

01 · 图模型:节点、边、触发与条件

本章讲清三件事:节点是什么数据结构、边携带哪些规则、triggercondition 为什么是两个独立维度。这是看懂后面执行引擎的地基。

1. 节点(Node):带输入输出队列的执行单元

它要解决的小问题。 图里的每个框,既要能「接收上游的多条消息」,又要能「产出消息给下游」,还要记住「我这一轮被触发了没」。

Node 这个 dataclass 就是这一切的载体(entity/configs/node/node.py:62-80):

  • input: List[Message] / output: List[Message] —— 输入队列、输出队列。
  • config: BaseConfig —— 类型专属配置(agent 的就是 AgentConfig,loop 的就是 LoopCounterConfig)。
  • context_window: int —— 执行后保留多少条输入(见第 04 章)。
  • predecessors / successors / _outgoing_edges —— 图结构。
  • start_triggered: bool —— 是不是被显式标成了起点。

节点类型是插件化注册的,不是写死的 if-else。runtime/node/builtin_nodes.py:27-101 把每种类型注册进 registry:

type干什么executor
agent调 LLM + 工具,核心AgentNodeExecutor
human暂停等人类输入HumanNodeExecutor
python跑仓库内 Python 片段PythonNodeExecutor
passthrough原样转发上游输出PassthroughNodeExecutor
literal每次被触发就吐出固定文本LiteralNodeExecutor
loop_counter计数,达到上限才放行LoopCounterNodeExecutor
loop_timer计时,到时间才放行LoopTimerNodeExecutor
subgraph内嵌另一张图SubgraphNodeExecutor

注册表机制见 runtime/node/registry.py:46-67register_node_type——它同时把节点的配置 schema 注册进 schema_registry,这样前端「配置图」UI 能自动拿到每种节点的可填字段。

2. 边(EdgeLink):一条边携带的全部规则

它要解决的小问题。 「A 连到 B」远不止画根线。真实问题是:A 跑完后,要不要把数据搬给 B?满足什么条件才搬?搬完要不要把 B 点亮让它执行?

这三件事在 EdgeLink(entity/configs/node/node.py:37-56)里是三个独立字段:

字段含义直觉
condition放行条件(默认 true)「闸门开不开」
carry_data是否把上游 Message 入队到目标「搬不搬货」
trigger是否把目标节点点亮(使其可执行)「按不按下游的启动按钮」
keep_message搬过去的消息打 keep 标记(清理时不删)「这条货是常驻库存」
clear_context / clear_kept_context入队前先清目标已有输入「先清空 B 的桌面再放新文件」

trigger 与 condition 是两个正交维度(最容易混)

这是 ChatDev 图模型最关键、也最反直觉的一点。看 ChatDev_v1.yaml 里的两类边:

# 示意,摘自 ChatDev_v1.yaml:744-752
- from: USER
to: Programmer Coding
trigger: false # ← 只送数据、不启动
carry_data: true
keep_message: true
# 示意,摘自 ChatDev_v1.yaml:506-511
- from: Coding Phase Prompt for Assistant
to: Programmer Coding
trigger: true # ← 既送数据、又启动
carry_data: true

两条边都指向 Programmer CodingUSERtrigger: false 表示「我只是把用户任务作为背景塞进它的输入队列(且 keep_message,常驻),但不是我让它跑」;真正让它跑的是 trigger: true 的那条 prompt 边。

为什么这么设计? 因为一个 agent 节点常常需要「多份输入(任务 + 角色提示 + 上游产物)」拼成一次对话,但只该被其中一个信号启动一次。把「搬数据」和「启动」拆开,就能精确表达这种「多份素材、单点触发」的语义。

判定「节点是否被触发」的逻辑见 Node.is_triggered()(entity/configs/node/node.py:452-459):只要有一条 trigger=truetriggered=true 的入边,或自己是 start 节点,就算被点亮。

3. 边的执行:condition → 清理 → 搬数据 → 点亮

一条边在上游跑完后被处理时,顺序是固定的。核心在 EdgeConditionManager._process_with_condition(runtime/edge/conditions/base.py:91-175):

出边处理流程(从上到下):
1. 算 condition(把上游输出文本喂给 evaluator)
│ 不满足 → 直接 return,这条边当作不存在
▼ 满足
2. clear_context / clear_kept_context → 先清目标输入队列
3. carry_data?
是 → 准备 payload(可能经 payload_processor 改写)→ append 到目标 input
否 → 跳过搬数据
4. trigger?
是 → edge_link.triggered = True (目标在本轮变得可执行)

思路/直觉: 边是一个「带门的传送带」。门(condition)先决定通不通;通了才搬货(carry_data);搬完再决定按不按目标的启动键(trigger)。三步解耦,组合出极强的表达力。

4. 起点与「谁也点不亮的节点」

图必须显式声明起点。GraphManager._determine_start_nodes(workflow/graph_manager.py:273-319)要求 YAML 里 start: 至少有一个节点,否则直接报错(workflow/graph_manager.py:312-316)。起点节点在执行前被打上 start_triggered=True 并灌入初始任务消息(workflow/graph.py:288-298)。

建图后还会扫一遍「有前驱、但没有任何 trigger=true 入边」的节点并告警——它们永远不会执行(workflow/graph_manager.py:321-342)。这是给「画错图」的人的一个安全网。

5. 小结

  • 节点 = 带 input/output 队列 + 触发态的执行单元,类型插件化注册。
  • 边 = condition(放不放行)× carry_data(搬不搬数据)× trigger(点不点亮)三个正交开关。
  • 「多份素材、单点触发」靠 trigger:false + trigger:true 的边组合表达。

下一章:执行器拿到这张图后,到底按什么顺序跑节点,以及循环/环怎么实现。