Skip to Content
Ⅰ · 系统骨架04 · Workflow、Prompt 与图

04 · Workflow、API Prompt 与动态图

ComfyUI 最常见的接口误解,是把“保存的 Workflow”与“提交给 /prompt 的 Prompt”当成同一种 JSON。它们表达同一张逻辑图,却服务于不同读者:前者服务编辑器,后者服务执行器。

两种图,两种职责

维度WorkflowAPI Prompt
主要消费者Web 前端编辑器Python 后端执行器
节点形态数组,含 position、size、flags、widget values以节点 ID 为键的对象
连线独立 link 记录及 slot 索引直接嵌入目标输入:[source_id, output_index]
UI 信息分组、缩放、折叠、颜色等通常不需要
执行类型前端节点 typeclass_type
获取方式保存工作流前端 Export (API) / 客户端构造

官方示例脚本开头也明确说明它使用 API Prompt,并建议从前端 Export (API) 得到特定工作流的执行格式。

script_examples/basic_api_example.pyL1-12API Prompt 示例与 Export (API) 说明。

最小 API Prompt

下面省略了部分采样链,只保留类型表达方式:

{ "4": { "class_type": "CheckpointLoaderSimple", "inputs": { "ckpt_name": "model.safetensors" } }, "6": { "class_type": "CLIPTextEncode", "inputs": { "text": "a glass city at sunrise", "clip": ["4", 1] } } }

"model.safetensors" 是常量;["4", 1] 是链接,表示“取节点 4 的第 1 号输出”。这个表示法把边放进输入里,因此从目标节点可直接追到上游。

链接判定为什么不是“所有二元数组”

运行时的 is_link() 检查值形状与元素类型:第一个元素是字符串节点 ID,第二个是数字插槽。普通业务输入也可能是数组,所以协议要用更精确的判定,而不能见 list 就当边。

comfy_execution/graph_utils.pyL1-20链接的最小结构判定。

节点 ID 是字符串

前端常产生看似数字的 ID,如 "6",但运行时把它当字符串。运行时子图会生成带前缀的复合 ID,更说明不应在插件里强制转整数。

output index 是协议的一部分

连接不仅依赖上游节点,也依赖它的第几个输出。缓存签名会保留这个插槽位置;同一节点的 MODEL、CLIP、VAE 输出不可互换。

Prompt 的节点记录

基础字段包括:

node_id → { class_type: 节点注册名, inputs: 输入名 → 常量或 link, _meta: 可选显示信息 }

节点类的 INPUT_TYPES / V3 Schema 决定输入要求,RETURN_TYPES / outputs 决定链接类型。Prompt 本身不携带 Python 代码,也不序列化模型对象。

为什么从输出节点开始

画布中可能保留实验分支、预览节点和暂时断开的旧链路。执行器只需要满足用户声明的结果,因此验证器先找 OUTPUT_NODE / is_output_node,再沿链接反向递归。

上图只有到 SaveImage 的闭包必需。孤立节点即使 schema 完全合法,也不会因“出现在 JSON 中”自动运行。

这带来三个性质:

  1. 死分支天然消除:不需要独立优化 pass;
  2. 局部执行有自然语义:换一组输出根即可;
  3. 副作用必须显式成为输出:否则运行时没有理由求值。

partial execution targets

POST /prompt 可携带局部执行目标。验证器会把这些目标与输出节点规则结合,得到本次真正的 good_outputs。它适合只预览工作流的一部分,也使一个 Prompt 能被不同目标重复利用。

局部目标不是随便指定任意内部节点来绕过协议;目标仍需满足可执行根的约束,错误会在验证阶段返回。

node replacements

服务入口在验证前允许节点替换逻辑调整 Prompt。其目的通常是兼容或将一种高层节点映射到实际执行节点。因为替换先于验证,验证看到的是最终结构,而不是验证一张图、执行另一张图。

设计原则是:任何会改变依赖或类型的重写,都应在计算 good_outputs 和缓存签名前完成。

DynamicPrompt:原始图之外还有一层

执行器没有直接把原始 dict 当作唯一真相,而是创建 DynamicPrompt:

# 概念模型 class DynamicPrompt: original_prompt ephemeral_prompt ephemeral_parents ephemeral_display
comfy_execution/graph.pyL21-62原始节点与 ephemeral nodes 的统一查询层。

它提供 get_node_id_to_display()、get_real_node_id() 等映射,原因是运行时节点的内部唯一 ID 不一定适合直接显示给用户。错误消息需要指回生成它的父节点,而缓存又必须区分实际实例。

运行时子图怎样加入

允许 expand 的节点可返回:

{ "result": (...links_to_subgraph_outputs...), "expand": { "node_a": {"class_type": "...", "inputs": {...}}, "node_b": {"class_type": "...", "inputs": {...}} } }

GraphBuilder 为节点加唯一前缀,把本地链接改写成全局 ephemeral IDs,并序列化成 Prompt 片段。执行器将这些节点合并进 DynamicPrompt,为它们建立独立缓存层,然后把当前节点退回 pending。

comfy_execution/graph_utils.pyL13-114运行时子图构建、前缀化与序列化。

原始节点和临时节点的身份

属性原始节点临时节点
来源API Prompt某节点执行结果的 expand
ID客户端提供GraphBuilder 前缀化
生命周期整个 Prompt生成它的执行分支
显示自身 ID / _meta常映射到父节点显示 ID
缓存顶层缓存hierarchical cache 下的子缓存

临时节点不能污染下一次请求的原始 Prompt。它们属于这次动态展开,并通过父级缓存关系管理生命周期。

图中的三类输入

在真正调用节点前,get_input_data() 把 Prompt 输入归一为:

  1. 连接输入:从 cache / 上游输出提取指定 slot;
  2. 常量输入:放入单元素列表,以适配批/list mapping;
  3. 隐藏输入:由运行时注入,如 PROMPT、EXTRA_PNGINFO、UNIQUE_ID、DYNPROMPT。
execution.pyL159-227输入解链、隐藏值注入和 V1/V3 归一。

隐藏输入是能力注入,不应由客户端伪造同名普通值来获得同等语义。节点 schema 明确声明后,运行时才提供。

图不只是 DAG

在静态视角,合法 Prompt 必须无强依赖环。但运行时还会出现:

  • 懒链接暂时不计入强依赖;
  • 外部异步任务让节点挂起;
  • expand 使图增长;
  • 缓存让某些节点无需进入执行;
  • ExecutionBlocker 让结果沿边阻断。

因此更准确的模型是“可增长、有条件依赖、可挂起的数据流图”,而不是一次性排完序的 DAG。

一个改动如何传播

将正向提示从 A 改为 B:

CheckpointLoader signature unchanged → cache hit CLIPTextEncode(+) constant changed → recompute CLIPTextEncode(-) unchanged → cache hit EmptyLatentImage unchanged → cache hit KSampler upstream signature → recompute VAEDecode upstream signature → recompute SaveImage upstream signature → execute

这不是 Prompt 图自己决定的,而是图结构被缓存签名递归编码后的结果。第 08 章会把签名算法完整拆开。

API 客户端的最小正确姿势

  1. 从 Export (API) 得到后端格式,不把 Workflow 原样 POST;
  2. 只修改已知节点的常量输入,保留链接二元组;
  3. 为每次提交生成/接收独立 prompt ID;
  4. 设置 client_id 并监听 WebSocket,或轮询 history;
  5. 先处理 HTTP validation error,再等待执行事件;
  6. 不假设节点 ID 是整数或执行顺序。

可迁移的设计方法

  • 编辑模型与执行 IR 分离。 编辑器可以演进布局字段,执行器保持最小协议。
  • 边内嵌在消费者输入。 反向需求分析和递归签名更直接。
  • 原始 IR 不可变,动态扩展分层存储。 便于错误归因和缓存回收。
  • 副作用由显式输出根驱动。 防止死分支意外产生外部效果。
  • 链接位置进入语义身份。 上游对象相同但 output slot 不同,结果仍应不同。
Last updated on