前面我们已经介绍了ComfyUI的工作流原理,并搭建了运行环境。在我们创建的环境中,ComfyUI运行在后端,应用程序通过http和web socker协议与ComfyUI服务进行数据交换。本文重点介绍该架构涉及的协议和技术栈选型。
一、架构说明
下图说明了应用程序与ComfyUI服务之间的关系:

与在应用程序中启动和管理ComfyUI服务相比,这种架构有如下好处:
1.1 多应用共享一份模型与显存
Qwen-Image 2.1 三件权重共约 14.6GB。如果每个应用各自拉起 ComfyUI 子进程,模型会被重复加载 N 份------显存翻倍、启动变慢、还互相争抢 GPU。独立常驻后:
- 模型只在首次生成时加载一次,之后常驻(ComfyUI 自带权重缓存管理)
- 多个应用同时提交任务,由 ComfyUI 内部队列串行执行,天然避免显存冲突
- 应用端进程可以随意重启,不影响正在进行的推理
1.2 应用端变得极薄
推理(torch、CUDA、GGUF 反量化、显存调度)全部留在 ComfyUI 侧,应用端不装 torch 也能工作。这意味着:
- 应用端依赖从 ~3GB(torch + CUDA 运行时)降到几十 MB
- 部署、升级应用不需要碰 GPU 环境;换显卡只重装 comfy-venv 一处
- 应用端崩溃不会带走推理进程;反之亦然
1.3 生命周期解耦
可以编写独立的脚本统一管理ComfyUI的生命周期:系统开机启动一次,所有应用整日可用。应用端只需要"连接"而不需要"管理"推理进程,取消任务也不再调用 /interrupt 全局中断------避免影响共用同一服务的其它应用。
1.4 技术栈自由
接口只是 HTTP + WebSocket,应用端用什么语言实现都可以------Python、Node.js + TypeScript、Go、Rust 均无障碍(详见第四节)。推理生态(PyTorch、diffusers、GGUF 加载器)只存在于 ComfyUI 一侧,不再绑架应用端的语言选择。
模型管理集中化
extra_model_paths.yaml 把模型目录映射给 ComfyUI 后,"有哪些模型可用"只有一个权威答案(ComfyUI 自己)。应用端通过 /object_info 查询即可,不必各自扫描磁盘、猜测路径。
二、ComfyUI 接口详细说明
应用端与 ComfyUI 的全部交互归结为:构建工作流 JSON → 提交 → 监听进度 → 取图。本节详细说明应用端与ComfyUI的接口。
2.1 提交任务:POST /prompt
请求体:
json
{
"client_id": "myapp-3f9c2a",
"prompt": {
"1": {"class_type": "UnetLoaderGGUF", "inputs": {"unet_name": "qwen-image-2.1-UC-Q4_K_M.gguf"}},
"2": {"class_type": "CLIPLoader", "inputs": {"clip_name": "qwen3vl_8b_int8_convrot.safetensors", "type": "qwen_image", "device": "default"}},
"3": {"class_type": "VAELoader", "inputs": {"vae_name": "qwen_image_2.1_vae_bf16.safetensors"}},
"9": {"class_type": "KSampler", "inputs": {"seed": 42, "steps": 20, "cfg": 1.0, "...": "..."}},
"11": {"class_type": "SaveImage", "inputs": {"images": ["10", 0], "filename_prefix": "myapp/api"}}
}
}
要点:
prompt就是 ComfyUI 的 API 格式工作流 :节点字典,key 是节点 id,inputs里引用其它节点写作["节点id", 输出槽位]- 模型按文件名引用 (
unet_name/clip_name/vae_name),ComfyUI 按extra_model_paths.yaml注册的搜索路径解析------应用端不需要知道模型 的磁盘绝对路径 - 响应
{"prompt_id": "...", "number": 1, "node_errors": {}};node_errors非空表示工作流有结构性错误(如引用了不存在的模型文件名),任务不会入队
2.2 监听进度:WebSocket ws://<host>/ws?clientId=<client_id>
提交时带的 client_id 决定你在 WS 上收到哪些事件。消息分两类:
- JSON 帧 (
data为对象):状态与进度事件 - 二进制帧:预览图(本套应用忽略)
关键事件(type 字段):
| 事件 | 含义 | 应用端用途 |
|---|---|---|
execution_start |
任务开始执行 | 更新任务状态 running |
progress |
采样步进 {value, max} |
进度条:value/max * 权重 |
executing |
节点切换 {node, display_node} |
阶段提示(加载模型/编码/采样) |
executed |
节点完成,output.images 含产物文件名 |
收集结果文件名 |
execution_error |
执行失败,含异常与节点栈 | 任务标记 failed,展示错误 |
execution_success |
整个任务成功 | 结束监听 |
status |
队列长度变化 {status: {remaining...}} |
队列展示 |
实践提示:不要只监听 WS 就断定完成------偶发丢帧时任务会永远"进行中"。可靠做法是收尾时再
GET /history/<prompt_id>校验一次,WS 负责"快",history 负责"准"。
2.3 查询结果:GET /history/{prompt_id} 与 GET /view
json
// GET /history/<prompt_id>
{
"<prompt_id>": {
"status": {"status_str": "success", "completed": true},
"outputs": {
"11": {"images": [{"filename": "myapp/api_00042_.png", "subfolder": "", "type": "output"}]}
}
}
}
拿到文件名后取图:
GET /view?filename=myapp%2Fapi_00042_.png&subfolder=&type=output
返回图片二进制。type=output 表示正式产物(区别于临时预览)。
2.4 系统与模型信息
| 端点 | 用途 |
|---|---|
GET /system_stats |
ComfyUI 版本、设备名、显存总量/余量------连接健康检查与设备展示 |
GET /object_info/{node} |
节点定义;其 input.required.<字段>[0] 就是可选值列表。GET /object_info/UnetLoaderGGUF 返回的列表 = ComfyUI 实际可见的全部 GGUF 扩散模型 ,CLIPLoader / VAELoader 同理------模型选择功能的权威数据源 |
POST /interrupt |
中断当前任务。共享模式下慎用:它中断的是整个 ComfyUI 的当前执行,会波及其它应用 |
2.5 上传参考图:POST /upload/image
multipart/form-data,字段 image(可选 overwrite=true)。返回:
{"name": "xxx.png", "subfolder": "", "type": "input"},随后在工作流中用LoadImage 节点按 name 引用。上传前应用端应自行预处理。
三、应用端架构与关键技术
一个编排型应用端包含以下模块(与语言无关):
前端(HTML/JS 单页)
│ REST + SSE
▼
应用后端
├── API 层 # generate / jobs / gallery / system 路由
├── 任务队列与调度 # 串行消费、队列上限、取消
├── ComfyUI 客户端 # 工作流构建 + HTTP 提交 + WS 监听 + 取图
├── 进度推送(SSE) # 后端把 WS 事件转发给浏览器
├── 数据库(SQLite) # 任务 / 图片元数据 / 图库
├── 图像处理 # 上传预处理、产物落盘、缩略图
└── 辅助功能 # 耗时自校准、内容策略、配额清理
本节逐一介绍这些关键技术。
3.1 工作流构建(核心)
应用端的"业务逻辑"很大程度上就是拼工作流 JSON:
- 文生图 :
EmptySD3LatentImage(定尺寸)→KSampler→VAEDecode→SaveImage - 传统重绘 :
LoadImage→VAEEncode(参考图回 latent)→KSampler(denoise=重绘强度) - 多参考图智能编辑 (Qwen-Image 2.1 特有):
TextEncodeQwenImage21同时接收文本与 N 张参考图(视觉塔 + VAE latent 双通道),输出三件套(positive, negative, latent)直连 KSampler,官方约定CFG=1、denoise=1, 输出尺寸跟随 image_1。提示词中用<image1>...<image9>按序号引用各图。
踩坑记录:该节点的 Autogrow 输入在 API 格式里必须用点号键
images.image_1、images.image_2...;写成嵌套字典{"images": {...}}会被静默忽略,参考图完全接不上,编辑退化成纯文生图。
模型切换 = 换工作流里的文件名字段,立即生效,ComfyUI 自动加载新文件并管理旧权重缓存------应用端无需(也不应)自行"加载/卸载模型"。
3.2 异步任务模型:队列 + SSE
生成一张图从几十秒到几分钟,必须异步化:
- 前端
POST /jobs提交,立即返回{job_id, position},可连续排队多个 - 后端调度器串行消费 队列(GPU 任务并行没有意义),每任务执行时:
构建 payload → 上传参考图 → 提交工作流 → 监听 WS → 进度回写数据库 - 前端通过 SSE (
GET /jobs/{id}/events)订阅进度------浏览器原生EventSource,自动重连;后端把引擎线程收到的 WS 事件转成 SSE 帧 - 同步等待场景(界面上"开始生成"按钮)用
POST /generate阻塞到完成, 注意设置略小于引擎侧的超时,保证取消逻辑有机会执行
取消语义 :应用端"删除任务"要先判断------共享模式下不能调 /interrupt(会打断其它应用的任务),只能放弃监听、在应用侧标记取消。
3.3 数据库(SQLite)
图片生成类应用有两类基础数据需要保存:任务数据和生成产物数据。建议选择轻量级的SQLite数据库(单文件、零运维、读写对本场景绰绰有余),图片本体存文件系统(按日期分目录),库里只存相对路径。
两张核心表(任务和产物):
jobs:任务参数快照(JSON)、状态机(queued → running → succeeded/failed/canceled)、 进度、时间戳。大字段(参考图 base64)外置成 blob 文件,绝不进数据库images:产物元数据------尺寸、步数、种子、提示词、elapsed_ms、收藏/标记。 它同时是耗时自校准的数据源(见 3.5)与图库/参数复现的依据
3.4 图像处理
- 上传预处理:参考图长边压到 1536、对齐 32 倍数(防 12MP 手机原图撑爆视觉序列与 latent,且超出模型训练范围会明显劣化)
- 产物落盘 :从
/view取回二进制 → 按日期归档 → 相对路径入库 - 前端直出 :
/view的 URL 可直接交给浏览器<img>(ComfyUI 静态服务),应用端不必中转大文件
3.5 耗时自校准(实践经验)
跨设备预估生成耗时没有意义(RTX 2060 与 RTX 5090 差 40 倍)。可靠做法:从 images 表取最近 30 条 elapsed / (steps × 百万像素) 的中位数拟合本机系数 k 与固定开销,缓存 30 秒;无历史时回退保守参考值并明确提示"未校准"。
实测:预估 128s vs 实际 116s(误差 ~10%),随样本增多更准。
3.6 状态与模型管理
- 状态徽章 =
GET /system_stats可达性(不要用引擎内部标志,引擎惰性加载要到首次生成才置位,会永远显示"启动中") - 模型列表 =
GET /object_info/{UnetLoaderGGUF,CLIPLoader,VAELoader}的可选值并集;选择后写回应用配置并即时生效
四、两种技术栈的实现对照
接口既然只有 HTTP + WebSocket,应用端语言就是自由选择。以下对照说明关键模块在 Python 与 Node.js + TypeScript 中的等价实现。
4.1 总体对照表
| 模块 | Python | Node.js + TypeScript |
|---|---|---|
| Web 框架 | FastAPI + uvicorn | Fastify / NestJS / Express + tsx |
| HTTP 客户端 | urllib / httpx | 内置 fetch(Node 18+) |
| WebSocket 客户端 | websocket-client | ws |
| SSE 推送 | FastAPI StreamingResponse | reply.raw / sse 库 |
| 数据库 | sqlite3(标准库) | better-sqlite3(同步 API,质量很高) |
| 图像处理 | Pillow | sharp |
| 配置 | pydantic-settings + .env | zod + dotenv(类型安全更彻底) |
| 校验 | pydantic 模型 | zod schema,可与前端共享类型 |
4.2 ComfyUI 客户端(Node.js + TypeScript 示例)
typescript
// comfy.ts ------ 提交工作流 + 监听进度,与 Python 版等价
import WebSocket from "ws";
const COMFY = "http://127.0.0.1:8188";
const clientId = `myapp-${crypto.randomUUID().slice(0, 8)}`;
export interface Workflow { [nodeId: string]: NodeDef }
export async function submit(workflow: Workflow): Promise<string> {
const res = await fetch(`${COMFY}/prompt`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ client_id: clientId, prompt: workflow }),
});
const data = await res.json();
if (Object.keys(data.node_errors ?? {}).length)
throw new Error(`工作流错误: ${JSON.stringify(data.node_errors)}`);
return data.prompt_id as string;
}
export function listen(promptId: string, steps: number,
onProgress: (pct: number, msg: string) => void,
onDone: (ok: boolean, err?: string) => void) {
const ws = new WebSocket(`ws://127.0.0.1:8188/ws?clientId=${clientId}`);
ws.on("message", (raw) => {
const msg = JSON.parse(raw.toString());
switch (msg.type) {
case "progress":
onProgress(5 + 90 * msg.data.value / msg.data.max, "采样中");
break;
case "execution_error":
onDone(false, msg.data.exception_message); ws.close(); break;
case "execution_success":
onDone(true); ws.close(); break;
}
});
}
export async function fetchImage(filename: string): Promise<Buffer> {
const q = new URLSearchParams({ filename, subfolder: "", type: "output" });
const res = await fetch(`${COMFY}/view?${q}`);
return Buffer.from(await res.arrayBuffer());
}
export async function ping(): Promise<boolean> {
try { await fetch(`${COMFY}/system_stats`, { signal: AbortSignal.timeout(3000) }); return true; }
catch { return false; }
}
对比 Python 版:
python
# 同样三步:requests/urllib 提交 → websocket-client 监听 → /view 取图
prompt_id = _post_prompt(workflow) # POST /prompt
history = _listen_progress(prompt_id, steps, cb) # ws + /history 兜底
image = _fetch_output(history) # /view
两者结构一一对应,复杂度相当。
4.3 工作流构建:TS 的类型优势
工作流是"字段名拼错就静默失败"的 JSON(尤其 images.image_1 这类点号键),TS 可以定义类型获得编译期防护:
typescript
type NodeRef = [string, number]; // ["9", 0]
interface QwenEditWorkflow {
"1": { class_type: "UnetLoaderGGUF"; inputs: { unet_name: string } };
"2": { class_type: "CLIPLoader"; inputs: { clip_name: string; type: "qwen_image"; device: string } };
// ...
"12": {
class_type: "TextEncodeQwenImage21";
inputs: {
clip: NodeRef; prompt: string; negative_prompt: string; vae: NodeRef;
resolution: number;
// 点号键:Autogrow 输入的 API 格式,拼错会静默忽略参考图!
[key: `images.image_${number}`]: NodeRef;
};
};
}
Python 侧靠 pydantic 校验请求、靠人工约定工作流结构;TS 侧类型可以直接覆盖到工作流内部,前端参数表单与后端工作流共享同一套类型定义。
4.4 SSE 转发
typescript
// Fastify:把引擎事件转发给浏览器
app.get("/jobs/:id/events", async (req, reply) => {
reply.raw.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
});
const send = (event: string, data: unknown) =>
reply.raw.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
bus.on(String(req.params.id), send); // 应用内事件总线
req.raw.on("close", () => bus.off(String(req.params.id), send));
});
Node 的事件循环与"单线程串行队列"模型天然契合,这部分甚至比Python 的 asyncio + run_coroutine_threadsafe 桥接更简洁。
4.5 取舍建议
| 场景 | 建议 |
|---|---|
| 团队前端已是 TS,应用以编排/交互为主 | Node.js + TS:前后端共享类型,栈统一 |
| 需要写 ComfyUI 自定义节点、做 CV 预处理、复用社区脚本 | Python:与 ComfyUI 生态同语言 |
| 需要"绕过 ComfyUI 直连 diffusers"的备用通道 | 只有 Python 可行(torch/diffusers 生态)------但这与本架构方向相悖,不建议保留该路径 |
| 高并发 API 服务 | 两者皆可;瓶颈在 GPU 不在应用端 |
推理下沉到独立 ComfyUI 服务之后,应用端是"薄编排层",Python 与 Node.js + TypeScript 都能完整实现且没有明显劣势;选型依据应是团队技能与生态偏好,而非技术约束。
五、应用示例
Image Studio就是采用这种架构开发一款简单的图片生成和处理程序,下面是这个项目的github地址:
Github: zhenl/image-studio
后续会对这个项目做详细说明。