让AI指挥AI画图
我玩了一段时间的ComfyUI之后,突然有一个想法:既然都是生成提示词来绘制画面,为什么我不能让LLM全自动为我生成提示词,然后生成画面后再让模型进行审核呢?
我很早之前就了解到ComfyUI是有API接口的,只需要调用就可以,而Ollama本身就支持调用。这意味着,我只需要写一个中间沟通的桥梁。让两个AI对接互撕就可以了。
说干就干,我打算用我之前拿来跑SheepIt的P104-100双卡渲染机来做这件事情。毕竟矿卡嘛,就应该有矿卡的样子——没日没夜高强度工作,直到寿终正寝。都是哥布林洞穴里面出来的玩意,RTX2080Ti我会让她好好当作正常的显卡用,至于职业矿渣P104-100,你就做一辈子矿卡吧!
模型选型
由于P104-100的显存只有8GB,所以太大的模型是跑不了的。所幸,我想玩的NetaYume v4画图模型,是有分体版本的,可以在8G显存的显卡上面完美运行,那接下来选型的重点,就是多模态模型的选择了:主要是能输入图片的模型,毕竟需要模型对图片进行把关。
由于显存限制,模型应该选择 7B-8B 的模型会比较合适,因为既能充分利用显存,又能提供还可以的智商,不至于太傻。
看了一下Ollama官网上带视觉功能的最近的模型,比较适合的,主要有这几个选择:
- Qwen3.5 4B (9B试过了,显存不够用,会卸载到CPU导致速度奇慢)
- Qwen3-VL 8B
- Gemma3 4B
- Ministral-3 8B
- Minicpm-V 8B
目前我只尝试使用了Qwen3-VL 8B进行测试,效果还可以,提示词要求的格式服从性尚可,虽然也会偶尔给你发癫乱来导致产生一些废图。
另外,我还尝试了一下MiniCPM-V 8B,这个是最开始Gemini推荐的,发现这个模型对画面的评审能力,似乎确实是要比Qwen3-VL 8B要强得多,很多画面崩坏的细节都能察觉到。
对接ComfyUI API
至于如何部署ComfyUI和Ollama,我就不再赘述了,前面的文章都有讲过,让我们直接步入正题。
踩大坑记录
一开始我以为给API提交的JSON文件,就是保存在ComfyUI的user/default/workflows/目录下的JSON,然后发现这么尝试提交都是失败。后面去问了Gemini,才知道是不一样的。对于提交给API的JSON,需要打开高级设置,然后在文件中导出工作流。
具体的开启流程是:
点击ComfyUI面板右侧的齿轮图标(设置),勾选开启 ‘Enable Dev mode Options’(启用开发者模式选项)。开启后,面板上才会多出一个 ‘Save (API Format)’ 的按钮。点击它导出的 JSON 才是能给 API 用的。
步骤如图所示:


正确的JSON应该是长这样的:
{
"1": {
"inputs": {
"vae_name": "vae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "加载VAE"
}
},
"2": {
"inputs": {
"clip_name": "gemma_2_2b_fp16.safetensors",
"type": "stable_diffusion",
"device": "default"
},
"class_type": "CLIPLoader",
"_meta": {
"title": "加载CLIP"
}
},
"3": {
"inputs": {
"unet_name": "NetaYumev4_unet.safetensors",
"weight_dtype": "default"
},
"class_type": "UNETLoader",
"_meta": {
"title": "UNet加载器"
}
},
"4": {
"inputs": {
"shift": 4,
"model": [
"3",
0
]
},
"class_type": "ModelSamplingAuraFlow",
"_meta": {
"title": "采样算法(AuraFlow)"
}
},
"8": {
"inputs": {
"seed": 927904769887424,
"steps": 30,
"cfg": 4.0,
"sampler_name": "res_multistep",
"scheduler": "linear_quadratic",
"denoise": 1,
"model": [
"4",
0
],
"positive": [
"6:7",
0
],
"negative": [
"7:7",
0
],
"latent_image": [
"9",
0
]
},
"class_type": "KSampler",
"_meta": {
"title": "K采样器"
}
},
"9": {
"inputs": {
"width": 1920,
"height": 1088,
"batch_size": 1
},
"class_type": "EmptySD3LatentImage",
"_meta": {
"title": "空Latent图像(SD3)"
}
},
"10": {
"inputs": {
"samples": [
"8",
0
],
"vae": [
"1",
0
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE解码"
}
},
"12": {
"inputs": {
"filename_prefix": "NetaYume_v4",
"images": [
"10",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "保存图像"
}
},
"6:23": {
"inputs": {
"value": "You are an assistant designed to generate high-quality images with the highest degree of image-text alignment based on structural summary. <Prompt Start> "
},
"class_type": "PrimitiveStringMultiline",
"_meta": {
"title": "System prompt"
}
},
"6:24": {
"inputs": {
"value": "一幅充满温馨氛围的高质量室内场景二次元插画,主角是 NEKOPARA 作品里面的 #chocola_nekopara ,画面采用的画风风格是 @sayori_(neko_works) 的画风风格,与猫娘乐园系列游戏CG画风类似。\n作品为人物特写,人物站在画面中心偏右的位置,要求能够表现出她的可爱气质。她脸上挂着满足的笑容,可爱的眼睛在闪闪发亮。她的眼睛瞳孔是橄榄形的,符合她的猫娘人设。她的两条双马尾都各绑着一条白色丝带发饰。她身穿淡粉色的洋装,露出可爱的猫耳朵。她面向画面,双手手指交叉托住下巴,手肘放在桌子上,坐在一张欧式复古的沙发椅上,正在看着桌子上各种精美的糕点和甜品。她的尾巴在身后立起,末端卷曲。\n画面的前景是摆满桌面各式精美的糕点和甜点。\n画面的整体是一个温馨的女仆咖啡厅,背景装饰以淡雅的壁纸和古典家具为主。\n时间是下午,阳光透过窗户洒进房间,给整个场景增添了一种温暖的感觉。\n"
},
"class_type": "PrimitiveStringMultiline",
"_meta": {
"title": "Prompt"
}
},
"6:22": {
"inputs": {
"string_a": [
"6:23",
0
],
"string_b": [
"6:24",
0
],
"delimiter": ""
},
"class_type": "StringConcatenate",
"_meta": {
"title": "连接"
}
},
"6:7": {
"inputs": {
"text": [
"6:22",
0
],
"clip": [
"2",
0
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Positive Prompt)"
}
},
"7:22": {
"inputs": {
"string_a": [
"7:23",
0
],
"string_b": [
"7:24",
0
],
"delimiter": ""
},
"class_type": "StringConcatenate",
"_meta": {
"title": "连接"
}
},
"7:23": {
"inputs": {
"value": "You are an assistant designed to generate anime images based on textual prompts. <Prompt Start> "
},
"class_type": "PrimitiveStringMultiline",
"_meta": {
"title": "System prompt"
}
},
"7:24": {
"inputs": {
"value": "blurry, worst quality, low quality, jpeg artifacts, signature, watermark, username, error, deformed hands, bad anatomy, extra limbs, poorly drawn hands, poorly drawn face, mutation, deformed, extra eyes, extra arms, extra legs, malformed limbs, fused fingers, too many fingers, long neck, cross-eyed, bad proportions, missing arms, missing legs, extra digit, fewer digits, cropped, multiple tails, "
},
"class_type": "PrimitiveStringMultiline",
"_meta": {
"title": "System prompt"
}
},
"7:7": {
"inputs": {
"text": [
"7:22",
0
],
"clip": [
"2",
0
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Negative Prompt)"
}
}
}编写对接程序
整个程序我打算是使用Python来编写,在Gemini的代写下,我搞定了和ComfyUI对接的Python程序comfyui_client.py:
import json
import random
import uuid
import urllib.request
import urllib.parse
import json
import time
import websocket # 需要执行: pip install websocket-client
class ComfyUIClient:
def __init__(self, server_address: str = "127.0.0.1:8188"):
self.server_address = server_address
self.client_id = str(uuid.uuid4())
# 你要求的固定系统提示词
self.positive_prefix = "Drawn by @sayori_(neko_works)."
self.negative_prefix = ""
def load_workflow(self, workflow_path: str = "NetaYume_Example.json") -> dict:
"""读取 API 格式的工作流文件"""
with open(workflow_path, 'r', encoding='utf-8') as f:
return json.load(f)
def queue_prompt(self, prompt: dict) -> dict:
"""将准备好的工作流发送到 ComfyUI"""
p = {"prompt": prompt, "client_id": self.client_id}
data = json.dumps(p).encode('utf-8')
req = urllib.request.Request(f"http://{self.server_address}/api/prompt", data=data)
response = urllib.request.urlopen(req)
return json.loads(response.read())
def get_history(self, prompt_id: str) -> dict:
"""获取生成历史(包含图片信息)"""
with urllib.request.urlopen(f"http://{self.server_address}/api/history/{prompt_id}") as response:
return json.loads(response.read())
def get_image(self, filename: str, subfolder: str, folder_type: str) -> bytes:
"""从 ComfyUI 下载图片"""
data = {"filename": filename, "subfolder": subfolder, "type": folder_type}
url_values = urllib.parse.urlencode(data)
with urllib.request.urlopen(f"http://{self.server_address}/view?{url_values}") as response:
return response.read()
def generate_image(self, user_positive: str, user_negative: str = "", seed: int = None, save_path: str = "output.png"):
"""主执行流程"""
# 1. 加载并修改工作流
prompt_data = self.load_workflow()
if seed is None:
seed = random.randint(0, 2**32 - 1)
# ---------------------------------------------------------
# 2. 注入参数 (基于 NetaYume_Example.json 的节点 ID)
# ---------------------------------------------------------
# 节点 8 是 KSampler
prompt_data["8"]["inputs"]["seed"] = seed
# 节点 6:24 是正向提示词输入框
prompt_data["6:24"]["inputs"]["value"] = self.positive_prefix + user_positive
# 节点 7:24 是负向提示词输入框
prompt_data["7:24"]["inputs"]["value"] = self.negative_prefix + user_negative
print(f"[*] 准备提交任务...")
print(f"[-] 随机种子: {seed}")
# 3. 连接 WebSocket
ws = websocket.WebSocket()
ws.connect(f"ws://{self.server_address}/ws?clientId={self.client_id}")
# 4. 提交任务
queue_response = self.queue_prompt(prompt_data)
prompt_id = queue_response.get("prompt_id")
print(f"[*] 任务已提交,Prompt ID: {prompt_id}")
print(f"[*] 正在生成中,请稍候...")
# 5. 监听 WebSocket 知道任务完成
while True:
out = ws.recv()
if isinstance(out, str):
message = json.loads(out)
# 打印进度条 (可选)
if message['type'] == 'progress':
data = message['data']
print(f"\r[-] 渲染进度: {data['value']}/{data['max']}", end="")
# 如果收到 executing 消息且 node 为 None,说明当前 client 的任务队列已执行完毕
if message['type'] == 'executing':
data = message['data']
if data['node'] is None and data.get('prompt_id') == prompt_id:
print("\n[*] 渲染完成!")
break # 跳出监听循环
# 断开 WebSocket
ws.close()
# 6. 获取图片结果并保存
history = self.get_history(prompt_id)
# 提取当前 prompt_id 的输出数据
outputs = history[prompt_id].get("outputs", {})
for node_id, node_output in outputs.items():
if "images" in node_output:
for image_info in node_output["images"]:
# 下载图片
image_data = self.get_image(
image_info["filename"],
image_info["subfolder"],
image_info["type"]
)
# 写入本地文件
with open(save_path, "wb") as f:
f.write(image_data)
print(f"[*] 图片已保存至: {save_path}")
# 如果只需一张图,这里可以 return
return save_path
# ==========================================
# 测试运行
# ==========================================
if __name__ == "__main__":
client = ComfyUIClient(server_address="127.0.0.1:8188")
# 你只需要输入你想要的核心画面描述,系统提示词会自动拼接到前面
my_positive = "1girl, chocola_nekopara, sitting on a vintage sofa, detailed cafe background, masterpiece, best quality"
my_negative = "blurry, worst quality, low quality, jpeg artifacts, deformed"
result_path = client.generate_image(
user_positive=my_positive,
user_negative=my_negative,
save_path="my_generated_anime.png" # 图片保存路径
)其中,利用了websocket来获取图片输出进度,以便第一时间把生成的图片丢给视觉模型进行评价和提示词改进。
对接Ollama API
这里需要注意一个点,为了避免P104-100来回卸载和装载模型,我们需要给Ollama设置模型显存常驻,启动的脚本应该是这样的:
export HOME=/home/rin/Ollama/data/
export CUDA_VISIBLE_DEVICES=1
export OLLAMA_HOST=0.0.0.0:11434
export OLLAMA_KEEP_ALIVE=-1
./bin/ollama serve另外,因为是双卡,ComfyUI默认调用GPU0,那我们就需要让Ollama调用GPU1,避免和ComfyUI进行争抢,两张P104-100可以各司其职,一张画图,一张跑模型。转接板接上的那个PCIe 1.0 x1 的P104-100只拿来跑Ollama并且模型常驻,这样就可以避开带宽的劣势。
然后就是编写对接Ollama的API,这个倒是没有遇到什么坑,最开始在Qwen的帮助下也顺利完成了vision_agent.py:
import requests
import base64
import json
from typing import List, Dict, Union, Tuple, Optional
class VisionAgent:
"""
专门用于处理视觉理解和文本生成的 Agent。
支持两种模式:
1. VL 模式:接收图片和文本,进行视觉问答(用于评审)。
2. Text 模式:仅接收文本,进行纯文本推理(用于剧本家)。
"""
def __init__(self, ollama_url: str = "http://127.0.0.1:11434", default_model: str = "qwen3-vl:8b"):
self.url = f"{ollama_url}/api/chat"
self.default_model = default_model
# 初始化空的消息历史,用于维护上下文
self.messages: List[Dict] = []
# --- 功能一:构建系统提示词 ---
def set_system_prompt(self, role: str, task: str = "") -> str:
"""
预设几种常用的系统角色,方便调用。
你可以根据需要扩展这个字典。
"""
prompts = {
"critic": (
"你是一位严苛的世界顶级二次元插画视觉评审专家。你的任务是严格审核图片质量和动态表现。\n"
"【评审标准】(触发以下任何一条即为不合格):\n"
"1. 肢体崩坏:请拿放大镜检查双手、双脚的关节和手指!只要手指扭曲、数量不对,或存在多余肢体,立即判不合格。\n"
"2. 动态死板:角色动作像个僵硬的木偶,直挺挺地站着/坐着。\n"
"3. 依赖道具:老套地让角色手里拿着某物,缺乏自然的肢体互动。\n"
"4. 尾巴异常:角色尾巴缺失、画成了两条及以上尾巴,或与身体连接处畸形。\n"
"【强制回复格式】(极其重要):\n"
"请你先用一两句中文简短地分析画面的缺陷(例如:“左手手指融合畸形”、“没有画出尾巴”)。\n"
"分析完毕后,请另起一行,严格使用 [RESULT] 标签输出你的最终决定:\n"
"情况 A:如果图片完美,请输出:[RESULT] PASS\n"
"情况 B:如果不合格,请基于下方原始提示词重写,优化动作和环境。角色名称必须使用 #chocola_nekopara 。请输出:[RESULT] <纯英文提示词>\n"
"以下是生成当前图片的原始提示词,请开始你的评审:"
),
"scriptwriter": (
"你是一个极具创造力的二次元插画画师。你的任务是根据用户需求,生成高质量的纯英文自然语言提示词。\n"
"【核心动作要求】(极重要):\n"
"1. 拒绝死板:严禁老套的罚站/端坐姿势!\n"
"2. 拒绝手持道具:绝对禁止总是让角色手里拿着物品!\n"
"3. 肢体互动:请为角色设计自然、生动、与环境深度互动的全身动作(例如:双手向后撑在草地上、身体前倾趴在栏杆上远眺、单手高举在远处打招呼、蜷缩在沙发里、在半空中轻盈跳跃等)。\n"
"【画面构成逻辑】:\n"
"请依照“构图视角 -> 角色具体肢体动作 -> 角色表情与服装 -> 背景细节与光影效果”的逻辑进行全英文自然语言描述。释放你的想象力,让画面具有故事感!不要包含负面词汇。\n"
"【强制输出格式】(必须严格遵守):\n"
"1. 提示词角色名称必须是 #chocola_nekopara ,不要描述固有的身体结构(如发色、猫耳等),但必须详细描述服装、神态表情,以及四肢和猫尾巴的具体动态。\n"
"2. 你的回复必须是一段纯粹的英文提示词文本。绝对禁止输出任何中文、标题、说明、分析或礼貌性问候!你的输出必须能直接丢到画图模型中运行!"
),
"default": "你是一个有用的助手。"
}
return prompts.get(role, task)
# --- 功能二:上下文管理逻辑 ---
def add_message(self, role: str, content: str, image_path: Optional[str] = None):
"""
向上下文消息列表中添加一条消息。
role: 'user', 'assistant', 'system'
image_path: 如果有图片,将其转为 Base64 嵌入。
"""
message = {"role": role, "content": content}
# 如果有图片,添加 images 字段
if image_path:
message["images"] = [self._image_to_base64(image_path)]
self.messages.append(message)
def clear_context(self):
"""清空上下文,开始新的对话任务。"""
self.messages = []
def get_context(self) -> List[Dict]:
"""获取当前的上下文,用于发送请求。"""
return self.messages.copy()
# --- 功能三:发起请求与返回结果 ---
def chat(self,
content: str,
image_path: Optional[str] = None,
system_role: Optional[str] = None,
model: Optional[str] = None,
keep_context: bool = False) -> str:
"""
核心交互函数。
参数:
content: 用户输入的文本。
image_path: 可选,要分析的图片路径。
system_role: 可选,指定系统预设角色(如 'critic', 'scriptwriter')。
model: 可选,覆盖默认模型。
keep_context: 是否保留本次对话到历史中(用于多轮迭代)。
返回:
模型的文本回复。
"""
# 1. 构建消息列表
# 如果指定了 system_role,每次都把 System Prompt 放在最前面(Ollama 有时会忽略历史中的 System)
temp_messages = []
if system_role:
system_prompt = self.set_system_prompt(system_role)
temp_messages.append({"role": "system", "content": system_prompt})
# 如果需要保持上下文,加入历史记录
if keep_context:
temp_messages.extend([msg for msg in self.messages if msg['role'] != 'system']) # 避免重复 System
else:
# 仅本次对话,不依赖历史
temp_messages.append({"role": "user", "content": content})
if image_path:
temp_messages[-1]["images"] = [self._image_to_base64(image_path)]
# 2. 准备 Payload
payload = {
"model": model or self.default_model,
"messages": temp_messages,
"stream": False
}
# 3. 发送请求
try:
response = requests.post(self.url, json=payload, timeout=300) # 增加超时时间,图片处理较慢
if response.status_code == 200:
reply = response.json()["message"]["content"]
# 4. 上下文管理:如果开启 keep_context,保存这次交互
if keep_context:
self.add_message("user", content, image_path)
self.add_message("assistant", reply)
return reply
else:
raise Exception(f"HTTP {response.status_code}: {response.text}")
except requests.exceptions.RequestException as e:
return f"请求错误: {str(e)}"
# --- 辅助函数:图片转 Base64 ---
@staticmethod
def _image_to_base64(image_path: str) -> str:
with open(image_path, "rb") as f:
return base64.b64encode(f.read()).decode("utf-8")
# --- 简化接口(供外部直接调用,不需要维护对象状态)---
def simple_review(image_path: str, question: str = "", model: str = "qwen3-vl:8b") -> str:
"""
简单的评审接口,用于快速测试。
"""
agent = VisionAgent(default_model=model)
# 如果没有提供具体问题,使用 Critic 角色的默认逻辑
prompt = question if question else "请评审这张图片。如果完美,请回复 PASS;否则请指出修改意见。"
return agent.chat(content=prompt, image_path=image_path, system_role="critic")
if __name__ == "__main__":
# 测试代码
print("正在测试 Vision Agent...")
# 测试 1: 纯文本 - 剧本家模式
agent = VisionAgent()
prompt = agent.chat(content="我需要画一张人物插画,主角是来自猫娘乐园的巧克力,你可以发挥想象进行构图,但是要求画面明亮,清新。",
system_role="scriptwriter",
keep_context=False)
print(f"生成的提示词: {prompt}")
# 测试 2: 视觉评审模式 (请确保当前目录有一张 test.jpg 图片)
result = simple_review("av.jpg", "{}".format(prompt))
print(result)但是到了后面,我才意识到,真正难于驾驭的,是LLM本身——因为即使你在系统提示词已经强制规定了输出格式,它有时候还是会给你乱来!
对接与缝合
有了两个调用的模块,我们现在可以开始对接和缝合工作了。
但是,我发现导致程序不稳定和异常的,永远是Ollama那边运行的视觉语言模型,因为输出是真的不可控。有时候他给你输出的提示词夹杂了一大堆废话,ComfyUI那边就会画出奇奇怪怪的东西,还浪费了时间和算力。
在提示词服从性这一块,虽然说Qwen系列确实在各种测评中表现突出,但是也是会给你耍脾气乱来的,这方面确实比较玄学。
这是最终缝合起来调用的main.py:
import os
import time
from PIL import Image
from vision_agent import VisionAgent
from comfyui_client import ComfyUIClient
import random
# 将约束列表定义在顶部,方便随时添加新点子
POSE_MODIFIERS = [
"【动作要求】:角色必须双手空空(不能拿任何物品),双手正在做某种表达情绪的手势(如伸懒腰、托腮、双手叉腰、整理头发等)。",
"【环境互动】:角色的身体必须与环境发生物理接触(例如:背靠在大树上、上半身趴在咖啡桌上、慵懒地躺在花海中、单手扶着墙壁)。",
"【动态抓拍】:这是一个极具动感的抓拍瞬间(例如:正在奔跑、在半空中轻盈跳跃、被风吹得失去平衡、听见呼唤后的转身回眸)。",
"【日常情绪】:这是一个极其放松的日常瞬间(例如:慵懒地瘫坐在复古沙发里、双手抱膝蜷缩在角落、趴在窗台上看着外面发呆)。",
"【独特视角】:视角非常独特(例如:从下往上的仰视、从上往下的俯视、透过窗户的窥视),角色的肢体语言需要配合这种视角展开。"
]
def resize_image_for_vision(input_path: str, output_path: str, target_size=(960, 540)):
"""将生成的原图缩放并转换为 JPEG,以节省视觉大模型的处理时间和 token"""
try:
with Image.open(input_path) as img:
# 使用 LANCZOS 重采样算法保证缩放后的画面质量
resized_img = img.resize(target_size, Image.Resampling.LANCZOS)
# 转换为 RGB (如果原图是带有 Alpha 透明通道的 PNG,直接存 JPG 会报错)
if resized_img.mode in ("RGBA", "P"):
resized_img = resized_img.convert("RGB")
# 降低一点质量存为 JPEG,体积更小
resized_img.save(output_path, format="JPEG", quality=85)
return True
except Exception as e:
print(f"[-] 图片缩放失败: {e}")
return False
def run_creative_pipeline(base_idea: str):
# 初始化客户端
agent = VisionAgent()
comfy_client = ComfyUIClient()
# 固定的负面提示词
negative_prompt = "blurry, worst quality, low quality, jpeg artifacts, signature, watermark, username, error, deformed hands, bad anatomy, extra limbs, poorly drawn hands, poorly drawn face, mutation, deformed, extra eyes, extra arms, extra legs, malformed limbs, fused fingers, too many fingers, long neck, cross-eyed, bad proportions, missing arms, missing legs, extra digit, fewer digits, cropped, multiple tails"
max_retries = 3
# 最外层循环:如果 10 次拉扯失败,则从头开始新一轮
while True:
# --- 核心改动:在这里进行随机突变 ---
selected_modifier = random.choice(POSE_MODIFIERS)
current_idea_with_modifier = f"{base_idea}\n\n{selected_modifier}"
print("\n" + "="*60)
print("[*] 开始全新的插画创作流程!")
print("="*60)
# 1. 剧本家构思初始提示词
print("[*] 正在请求剧本家构思初始画面...")
current_prompt = agent.chat(
content=f"请帮我构思一个插画提示词,核心需求是:{current_idea_with_modifier}",
system_role="scriptwriter",
keep_context=False
)
print(f"\n[+] 初始提示词:\n{current_prompt}\n")
# =================【新增:剧本家错误拦截】=================
if current_prompt.startswith("请求错误:"):
print("[!] Ollama 响应超时或出错。让显卡休息 5 秒钟,准备重试本轮抽卡...")
time.sleep(5)
continue # 跳过下面的所有步骤,直接回到 while True 开头重新抽卡
# ==========================================================
success = False
# 2. 内层循环:生成、缩放、评审(最多拉扯 10 次)
for attempt in range(1, max_retries + 1):
print("-" * 50)
print(f"[*] 迭代回合: 第 {attempt}/{max_retries} 次尝试")
# 定义本次生成的文件路径
original_img_path = f"output_highres_v{attempt}.png"
resized_img_path = f"output_review_v{attempt}.jpg"
# --- 步骤 A: 生成 1080p 原图 ---
print("[-] 正在调用 ComfyUI 绘制高清插画...")
comfy_client.generate_image(
user_positive=current_prompt,
user_negative=negative_prompt,
save_path=original_img_path
)
if not os.path.exists(original_img_path):
print("[!] 图像生成似乎失败了,跳过本次审核...")
continue
# --- 步骤 B: 缩放图片 ---
print("[-] 正在将原图缩放为 960x540 交给评审专家...")
if not resize_image_for_vision(original_img_path, resized_img_path):
continue
# --- 步骤 C: 视觉评审 ---
print("[-] 评审专家正在拿着放大镜检查画面细节...")
critic_question = f"{current_prompt}"
review_result = agent.chat(
content=critic_question,
image_path=resized_img_path,
system_role="critic",
keep_context=False # 每次评审都是独立客观的,不看历史
)
print(f"\n[+] 评审专家反馈:\n{review_result}\n")
# =================【新增:评审专家错误拦截】=================
if review_result.startswith("请求错误:"):
print("[!] 评审阶段 Ollama 响应超时或出错,跳过本回合拉扯,直接进入下一次尝试...")
time.sleep(5)
continue # 跳过本回合剩余判断,直接进入 for attempt in range 的下一次循环
# ==========================================================
# =================【新增:解析带有思维链的回复】=================
# 找到 [RESULT] 标签并提取后面的内容
if "[RESULT]" in review_result:
final_decision = review_result.split("[RESULT]")[-1].strip()
else:
# 兜底:如果模型忘了输出标签,强行把它的回复当做提示词
final_decision = review_result.strip()
# ==========================================================
# --- 步骤 D: 判断结果 ---
# 判断切割后的内容是否仅仅是 PASS (加个长度限制防止误判)
if "PASS" in final_decision.upper() and len(final_decision) < 10:
print(f"[!!!] 恭喜!本次创作通过了魔鬼评审!")
print(f"[!!!] 最终成品高清大图保留在: {original_img_path}")
success = True
break
else:
print("[-] 专家不满意,已提取新的修改版提示词,进入下一轮迭代...")
# 将切出来的纯英文提示词喂给下一轮
current_prompt = final_decision
# 3. 检查内层循环退出原因
if success:
print("[*] 整个工作流完美结束。")
print("[*] 准备从头开始下一张插画构思...")
time.sleep(2)
else:
print(f"\n[!] 已经反复拉扯了 {max_retries} 次,模型陷入了死胡同。")
print("[*] 放弃当前的创意方向,准备从头开始重新构思...")
time.sleep(2)
if __name__ == "__main__":
# 你可以在这里输入你想要的最初始、最粗略的创意想法
my_idea = "我需要你帮我构思一张二次元风格插画。主角是来自猫娘乐园的巧克力,需要确保画面富有美感,颜色搭配准确,角色表现自然。你可以尝试异世界魔法、日常、科幻、幻想、欧式等多种题材,鼓励有意义的创新。"
run_creative_pipeline(my_idea)这里调用的时候,我也发现一个问题:如果是放开不做任何限制,LLM就会懒得动脑,基本上都是描述角色站着,手里拿个东西这样的图,毫无动感,角色就像罚站。所以后面在Gemini的帮助下,设置了强制条件,不允许罚站了,必须要画面富有动感,有层次。(果然要AI来压迫AI,Gemini直接说是Qwen偷懒,难绷)
事已至此,基本上使用Ollama运行VLM指挥ComfyUI画图就跑通了。
不过我也发现一些问题,运行到后面,即使是作画崩坏了,Qwen3-VL 8B还是无脑给PASS了,即使在系统提示词加上了严苛的限制条件,依然如此。虽然说有时候也能打回,但是那已经是作画崩坏很严重的情况下了。对于MiniCPM-V 8B,情况就要好不少,很多画面细节的崩坏都能抓的出来,还算是可以。
当然了,我这部分代码中安排的提示词是专门去画巧克力相关的插画的,如果需要去画别的角色,需要按需修改对应的提示词让模型生成对应的提示词。
成果展示
我就挑一些画的比较好的图展示一下吧,出好图的概率很低,真的说得上是百里挑一。当然,细看肯定还是有问题的,但是第一眼观感还可以。





哎,目前AI这一块还是玄学多,不确定性还是有点大了。我现在也是驾驭不住,如果有更好的思路,欢迎在评论区交流。









































































































































































































