概述

Part 1 讲了”LLM 做决策、代码做组装”的硬编码路线。Part 2 换一条更激进的路——SparkCanvas Pro 节点让 LLM agent 直接驱动 ComfyUI 工作流:agent 自己规划、自己执行、自己验证。核心方案是双 ComfyUI 实例 + Comfy-Cozy agent 框架 + 65K context 的 Qwen3.6-35B-A3B-AWQ。

关键要点

  • Agent 驱动而非硬编码:Pro 节点把 prompt 扔给 agent,agent 在独立 ComfyUI 实例里自己搭工作流、跑渲染、收结果,再把图片返回画布
  • 双 ComfyUI 实例是必须的:ComfyUI 的 prompt_worker 是单线程串行,Pro 节点占着 worker 又往同一队列塞任务会死锁,必须起第二个实例
  • Comfy-Cozy 四阶段循环:UNDERSTAND → DISCOVER → PILOT → VERIFY,103 个工具,最多 30 turns
  • 知识库驱动扩展:12 个 Markdown 文件(1492 行)拼入 system prompt,新能力只需加模板 + 加知识库描述,不改代码
  • 静默失败是最大敌人:8 个踩坑中一半以上时间花在”为什么没有输出”——context 溢出、aiohttp 自调用死锁、except 吞错误

技术细节

为什么需要 Pro 版本

Part 1 的 SparkCanvas 走”LLM 做决策 + 代码走分支”路线,可靠但有三个硬伤:

  1. 模式天花板:只有五种链路。”先换衣服再换背景再加光效”的多步任务没法拆。
  2. 新模型/新节点要改代码:加一个 ControlNet 预处理器、换一个 inpaint 模板,都要动 Python。
  3. 没有自我修正:LLM 选错参数或工作流执行出错,节点直接返回黑图或崩溃,没有”再试一次”的能力。

Pro 节点的目标:让 agent 真正”会用” ComfyUI——看到新模板就能调,出错了会换方案,渲染完会自己看结果判断是否完成。

总体方案:agent + 双 ComfyUI 实例

一句话:用户在 ComfyUI 画布上放一个 Pro 节点,Pro 节点把 prompt 扔给 agent,agent 在另一个 ComfyUI 实例里自己搭工作流、跑渲染、收结果,再把图片返回给画布。

┌────────────────────────────────────────────────────────────────┐
│ NVIDIA GB10 (128GB 统一内存) │
│ │
│ ┌──────────────┐ ┌───────────────┐ ┌───────────────────┐ │
│ │ vLLM (:8000) │ │ ComfyUI Main │ │ ComfyUI Agent │ │
│ │ Qwen3.6-VL │ │ (:8188) │ │ (:8189) │ │
│ │ 35B-A3B-AWQ │ │ │ │ │ │
│ │ 65K context │ │ 用户画布 │ │ headless │ │
│ │ ~65GB VRAM │ │ Pro 节点 │ │ Comfy-Cozy agent │ │
│ │ │ │ ~0.2GB VRAM │ │ workflow 执行 │ │
│ │ │ │ │ │ ~7GB VRAM │ │
│ └──────┬───────┘ └──────┬────────┘ └──────┬────────────┘ │
│ │ │ │ │
│ │ OpenAI API │ POST /chat │ │
│ ◄──────────────────┼────────────────────┤ │
│ │ ◄────────────────────┤ │
│ │ │ {output_images, │ │
│ │ │ content, errors} │ │
└─────────┴──────────────────┴────────────────────┴──────────────┘
组件 端口 职责
vLLM 8000 托管 Qwen3.6-35B-A3B-AWQ,给 agent 提供推理能力
ComfyUI Main 8188 用户画布,Pro 节点就住在这里
ComfyUI Agent 8189 headless 实例,Comfy-Cozy sidebar 在这里跑 agent loop,渲染也在这里

为什么是双实例:ComfyUI 的队列死锁

最初版本是单实例:Pro 节点在 8188 上跑,它调 agent,agent 把 workflow 也提交到 8188 的 /prompt。实际一启动就死锁。

ComfyUI 的 prompt_worker 是单线程串行的:

while True:
prompt = queue.get() # 阻塞等下一个任务
execute(prompt) # 执行(里面调用所有节点的 execute())
queue.task_done()

Pro 节点本身是一个正在执行的 prompt——它占着 worker。它内部调 agent,agent 又往同一个队列里塞新 prompt。新 prompt 永远排不上,因为它等的那个 worker 正在等它自己完成。经典的自己等自己。

解决方案:起第二个 ComfyUI。8188 给用户画,8189 专门给 agent 用,两个独立的 prompt_worker,互不阻塞。

两个实例共享同一个代码目录、模型目录、output 目录,所以 Pro 节点可以直接读 8189 生成的图片文件。唯一必须隔离的是 sqlite 数据库——comfyui.db 不支持多进程并发写入,启动 8189 时加 --database-url sqlite:///.../comfyui-agent.db 即可。

Agent 怎么工作:Comfy-Cozy 四阶段循环

8189 里住的是 Comfy-Cozy——一个把 ComfyUI 抽象成”工具集”的 agent 框架。它给 LLM 提供 103 个工具(load_templateget_editable_fieldsset_inputvalidate_workflowexecute_workflowget_execution_status 等),agent 按 UNDERSTAND → DISCOVER → PILOT → VERIFY 的节奏迭代:

Turn 1-3: UNDERSTAND + DISCOVER
load_template("pony_inpaint_depth")
get_editable_fields() # 拿到可改的节点和字段
list_models() # 看有哪些 checkpoint / ControlNet

Turn 4-8: PILOT
set_input(node="KSampler", field="seed", value=42)
set_input(node="CLIPTextEncode+", field="text", value="...tags...")
validate_workflow()
execute_workflow() # ← 这一 turn 里包含 SD 渲染,60-180s

Turn 9-11: VERIFY
get_execution_status()
# agent 确认有输出,done=True → 退出循环

LLM 每个 turn 只需要决定”下一步调哪个工具、传什么参数”。工具本身是 Python 代码,直接调 ComfyUI 的内部 API 搭图、验证、提交。

关键参数:

  • max_turns = 30:防止 agent 死循环。3-6 tok/s 的推理速度下,30 turns 约 10 分钟。
  • MAX_TOKENS = 4096:每个 turn 最多输出 4K tokens。
  • enable_thinking = false:关掉 Qwen 的 thinking 模式。thinking 额外吃 token,tool use 场景下收益很小。

知识库:让 agent 知道 Pony V6 XL 怎么用

通用 LLM 不懂 Pony V6 XL 那套 score_9, score_8_up 的引导 tag,也不懂 e621 标签体系、ControlNet strength 该给多少、inpaint 什么时候该用 CLIPSeg 什么时候该用 AnimeSeg。这些知识必须喂给它。

Comfy-Cozy 允许把一组 Markdown 文件拼到 system prompt 里。共 12 个知识库文件(1492 行):

文件 内容
pony_v6xl_guide.md Pony tag 体系、score tag、工作流模板决策树
comfyui_core.md ComfyUI 核心节点和连线规则
controlnet_patterns.md ControlNet 各类型的典型参数
common_recipes.md txt2img / img2img / inpaint 的常用组合
workflow_optimization.md 分辨率、采样器、denoise 的经验值
其他 7 个 video / audio / 3D / compositing 等扩展能力

12 个文件加起来约 15K tokens,工具 schema 约 13K tokens,留给对话历史和输出的预算大约 33K。这个”token 预算”是 Pro 节点第一个踩进去的大坑。

Pro 节点内部:一次请求的完整数据流

从用户点 “Queue Prompt” 到画布上出现新图的完整链路:

1. 用户在 8188 画布 Queue Prompt
↓
2. Pro 节点 generate() 被调用
- 把参考图 tensor 存到 ComfyUI input/spark_pro_input.png
- 拼 user_message(prompt + 图片文件名 + seed + preset)
↓
3. Pro 节点 → GET http://127.0.0.1:8189/superduper/status
(检查 agent brain 是否 ready)
↓
4. Pro 节点 → POST http://127.0.0.1:8189/superduper/chat
body: {"content": user_message}, timeout: 600s
↓
5. Sidebar handle_chat()
a. 快照 known_ids = PromptServer.instance.prompt_queue.get_history().keys()
b. 启动 agent loop(最多 30 turns)
c. 跑完后再取一次 history,取差集 → new_ids
d. 从 new_ids 里 status=success 的 entry 提取 outputs//images[].filename
e. 从 new_ids 里 status=error 的 entry 提取错误信息
↓
6. Sidebar → Pro 节点(JSON)
{
"content": "I've analyzed the reference image...",
"stage": "VERIFY",
"output_images": ["/home/rico/.../pony_inpaint_depth_00014_.png"],
"errors": []
}
↓
7. Pro 节点读图片 → 转成 [B,H,W,C] tensor
若 output_images 为空 → fallback: poll 8189 /queue 到 drain,再查 /history
↓
8. 返回 (image_tensor, info_string) 给 ComfyUI 画布

其中第 5 步的 known_ids / new_ids 差集是整个协议的关键——通过 ComfyUI 内部 API 拍两次快照取差,比目录扫描/时间戳匹配可靠得多。

踩坑记录

从”0/5 全部失败”到”3/3 稳定出图”,一共填了 8 个坑,按根因分四类。

类别一:vLLM context window 不够(静默失败之王)

现象:agent 跑 15 个 turn,8189 history 一直是空,LLM 看起来”正确理解了任务”但从来没调 execute_workflow

根因:vLLM 一开始 max_model_len=32768。但 system prompt + 12 个知识库(~15K tokens)+ 103 个工具 schema(~13K tokens)+ 对话历史已经 ~28K。只剩不到 4K 给输出,LLM 直接拒绝生成,agent 的 retry 逻辑又把错误吞了。外部看就是”agent 啥也没做”。

修复max_model_len 32K → 65K,gpu_memory_utilization 0.40 → 0.55。Qwen3.6-35B-A3B-AWQ 原生支持 256K,GB10 内存够。

教训:知识库每加一个文件,都要算总 token:

cat agent/comfy-cozy/agent/knowledge/*.md | wc -c
# 字符数 / 3.5 ≈ tokens

类别二:sidebar 自己调自己的 HTTP(aiohttp 死锁)

现象:history 里明明有成功的 workflow,但 sidebar 返回的 output_images 永远是 []

根因:一开始 _get_history_prompt_ids()urllib.requesthttp://127.0.0.1:8189/history——在 async handler 里发同步 HTTP 给自己。aiohttp 是单线程事件循环,handle_chat 占着 loop 在等 HTTP 响应;处理 /history 的那个 handler 永远轮不上。超时后 except Exception: return set() 直接吞掉,返回空集合。

修复:不走 HTTP,直接用 ComfyUI 内部 API:

from server import PromptServer
history = PromptServer.instance.prompt_queue.get_history()

教训:在 aiohttp async handler 里,永远不要用同步 HTTP 调自己的端点。sidebar 本身就住在 8189 进程里,所有 ComfyUI 状态都在内存里能直接访问。

类别三:基础设施共享资源

问题 修复
8189 启动失败,报 Could not acquire lock on database --database-url sqlite:///.../comfyui-agent.db,让 8189 用独立数据库
vLLM 重启报 GPU 内存不足 pkill -f vllm 没杀干净 VLLM::EngineCore 子进程,改用 pkill -9 -f vllm + ps aux | grep EngineCore 确认干净

类别四:上游节点 bug 和错误被吞

  • Sapiens 节点崩溃ComfyUI_Sapiens/sapiens_node.py 第 67 行 config.detector = True if use_yolo else None——作者把布尔值当 flag 用,但 predictor.py 里直接调 self.detector.detect(img)。一行改成 Detector() if use_yolo else None 就好。注意这是本地 patch,git pull 可能覆盖。
  • Pro 节点 4.8s 就 ERROR_get_history_prompt_ids() 里用了 os.environ 但没 import osNameError 让 8189 返回 500。
  • curl 把错误吞了:原来用 curl -sf-f 在 HTTP 500 时返回空内容,-s 隐藏所有错误信息。换成自定义 _curl_json() 捕获 http_codebody
    curl -s -w "n%{http_code}" ...
    # 再用 Python 拆开 body 和 status code

然后 sidebar 新增 _collect_workflow_errors(known_ids),把 history 里 error entry 的报错([NodeType] error_message)透传给 Pro 节点。用户现在能在画布上看到具体是哪个节点哪行代码炸了。

踩坑总结

类别 数量 共同特征
Context window 不够 1 静默失败,LLM 什么都不说
aiohttp 自调用死锁 1 except 吞掉超时,表象是数据为空
共享资源 sqlite / GPU 2 进程间资源竞争
节点 bug + 错误吞掉 4 上游代码问题 + 日志不透传

一半以上的时间花在”为什么没有输出”的排查上。经验:只要看到”什么都没发生”,大概率是 except 在某处把错误吃了

Inpaint 管线重构:从”能跑”到”好用”

跑通之后首先遇到的问题不是技术层面的,是结果质量。用户输入参考图 + 修改指令,Pro 节点返回的图片结构错位、残缺多肢、画风漂移、角色特征丢失。排查后确认是三个层次的问题叠加。

问题根因

1. VLM mask 节点代码 bug _VLM_PROMPT 模板字符串里塞了 JSON 花括号 {"items": ...},被 Python .format() 当成占位符,直接 KeyError("items") → mask 全黑 → 输出等于原图。VLM 返回格式 [{"bbox_2d": ..., "label": ...}] 跟代码期望的 {"items": [{"name", "bbox"}]} 又对不上,解析出来永远是空 list。

2. 矩形 mask 导致结构崩坏 VLM 只能吐 bounding box,框里混着衣服、身体、手臂。Pony V6 XL 在矩形区域内自由重绘,没有任何结构约束——解剖错误、色块模糊、边界突兀。

3. IP-Adapter 跟 Pony V6 XL 不兼容 通过 MSE 定量对比,开 IP-Adapter 和不开的结果色彩分布差异很大。Pony V6 XL 本身对 score tag 很敏感,IP-Adapter 的 cross-attention 注入把色调带偏了。

解决方案:两阶段修复

Phase 1:止血

  • VLM prompt 用字符串拼接替代 .format(),避免花括号冲突
  • VLM 响应解析兼容两种格式(bbox_2d/bboxlabel/name
  • 所有模板移除 IP-Adapter
  • 所有 inpaint 模板加 ImageScaleToTotalPixels(1MP),防止高分辨率输入结构崩坏

Phase 2:换架构

新建 pony_inpaint_depth 模板,把 VLM 从”生成 mask”的角色里解雇,改用 AnimeSeg 做像素级分割 + ControlNet Depth 做结构锚定:

LoadImage → ImageScaleToTotalPixels(1MP)
├→ AnimeSegMask(class=10 clothes) → GrowMask → FeatherMask
│ ↓
├→ VAEEncodeForInpaint ────────────────┤
│ ↓
├→ DepthAnythingV2 → ControlNet(depth, strength=0.45)
│ ↓
│ KSampler(denoise 0.65)
│ ↓
│ VAEDecode → SaveImage

VLM 的角色改为”看图提取 e621 角色 tags”,注入 positive prompt。结构约束用 ControlNet Depth 管,mask 精度用 AnimeSeg 的像素分割管,LLM 只负责它擅长的语义翻译。

效果对比:

指标 VLM bbox AnimeSeg + Depth
Mask 覆盖率 12% 矩形 30% 贴合轮廓
像素变化率 结构崩坏(量化无意义) 28.2% 且结构完整
Agent 端到端 时常选错模板 正确选 pony_inpaint_depth,12.9% 变化

决策树也在知识库里更新:pony_inpaint_depth 成为 clothing removal 首选,pony_inpaint_vlm 降级为 fallback。

关键指标

端到端走通后的实测数据:

指标
端到端延迟 120–300s(取决于 SD 渲染复杂度)
Agent turns 9–11 turns / 任务
LLM 推理速度 prompt ~2400 tok/s,generation 3–6 tok/s
单 turn 耗时 5–22s(execute turn 60–186s)
GPU 内存占用 vLLM ~65GB + ComfyUI ~7GB
知识库 12 文件 / 1492 行
工作流模板 16 个
修复前/后成功率 0/5 → 3/3
运行 耗时 Turns 输出
CLI 测试 303s 11/30 (done=True) pony_inpaint_clipseg00012.png
CLI 测试 124s 9/30 pony_inpaint_clipseg00013.png
画布验收 161s pony_inpaint_clipseg00014.png

与 Part 1 的对比:两条路线的取舍

Part 1(SparkCanvas) Part 2(SparkCanvas Pro)
LLM 输出 10 个字段的 JSON 工具调用序列(最多 30 turns)
可扩展性 新能力要改 Python 分支 新模板丢 templates/、知识库加段描述即可
执行链路 一次推理 → 代码硬走分支 多 turn 推理 → agent 选模板/改字段/执行/验证
可靠性 高(代码路径稳定) 中(LLM 偶尔选错模板、verify 阶段反复重试)
延迟 低(LLM 只跑 2 次) 高(120–300s,execute turn 就 60–180s)
LLM 要求 7B 足够 35B A3B + 65K context 才稳
典型故障 参数选错 → 结果差 静默失败(context 溢出、HTTP 自调用、except 吞错)

核心差异:Part 1 是 LLM 做决策,代码做执行;Part 2 是 LLM 做决策 + 执行(通过工具间接执行),代码只提供基础设施和工具。

两条路没有对错。需求场景有限、响应速度敏感 → Part 1 更合适。要支持持续扩展、复杂多步任务、或者希望系统能”自己想办法” → Part 2 的投入值得。

已知问题与后续方向

当前 Pro 节点待改进:

  • Agent 有时用满 30 turns 仍未完成,大多发生在 VERIFY 阶段反复”再检查一下”。需要给 verify 工具加硬停止条件。
  • 无多轮对话状态:每次 Pro 节点调用都是独立 session,上一次生成的参数不会被下一次利用。”denoise 调低一点再来一张”还没法工作。
  • 没有进度反馈:Pro 节点调 agent 是同步阻塞的,画布上没法显示 “Turn 4/30, 执行 execute_workflow 中…” 的中间状态。
  • vLLM generation 3–6 tok/s 偏慢:可以考虑 speculative decoding,或者小模型 draft + 大模型 verify 的组合。

中期方向:把”LLM 直接 agent 驱动”和 Part 1 的”硬编码快速通道”混合——常见需求(单步 txt2img / img2img / inpaint)走 Part 1 快速通道,复杂或不匹配的需求 fallback 到 Pro 的 agent 路线。既保留延迟优势,又不丢掉扩展能力。

Part 3 预告:对话式多轮迭代 + 角色库 + LoRA 训练接入。Pro 节点不只是”出一张图”,而是能跟用户对着一个角色一路改下去。

备注

经验总结(给本地想搭”丐版 NanoBanana”的同学):

  1. 双实例是必须的,别在一个 ComfyUI 里让 agent 套自己
  2. 知识库 token 预算要盯紧,vLLM 的 max_model_len 宁可给大点
  3. async handler 里别用同步 HTTP 调自己,用内部 API
  4. 所有 except 都要打日志,静默失败是 debug 黑洞
  5. 输出路径走结构化数据(history 差集),别靠目录扫描/时间戳猜

原文链接

原始笔记来源:收藏文章/SparkCanvas:从零搭建丐版 NanoBanana AI 绘图节点(二).md