概述
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 做决策 + 代码走分支”路线,可靠但有三个硬伤:
- 模式天花板:只有五种链路。”先换衣服再换背景再加光效”的多步任务没法拆。
- 新模型/新节点要改代码:加一个 ControlNet 预处理器、换一个 inpaint 模板,都要动 Python。
- 没有自我修正: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_template、get_editable_fields、set_input、validate_workflow、execute_workflow、get_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.request 去 http://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 os,NameError让 8189 返回 500。 - curl 把错误吞了:原来用
curl -sf,-f在 HTTP 500 时返回空内容,-s隐藏所有错误信息。换成自定义_curl_json()捕获http_code和body: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/bbox、label/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”的同学):
- 双实例是必须的,别在一个 ComfyUI 里让 agent 套自己
- 知识库 token 预算要盯紧,vLLM 的
max_model_len宁可给大点 - async handler 里别用同步 HTTP 调自己,用内部 API
- 所有 except 都要打日志,静默失败是 debug 黑洞
- 输出路径走结构化数据(history 差集),别靠目录扫描/时间戳猜
原文链接
原始笔记来源:收藏文章/SparkCanvas:从零搭建丐版 NanoBanana AI 绘图节点(二).md