上周把AIYA挂在测试频道里跑了两天群友的反馈基本只有三种一是刷出一堆图二是排队刷出一堆图三是问我怎么才能让图生成得快一点。我挺满意这个状态的——说明大家已经默认它应该存在在聊天框里就跟一个群友要表情包一样自然。这个项目说白了并不复杂给本地Stable Diffusion接一层Discord机器人接口让人在聊天框里输入/draw弹出参数表单回车之后等几秒到几十秒图片就直接出现在频道里。但就是这层看似简单的“接口”背后藏着不少值得展开的工程设计细节。如果你有一台跑得动SD的电脑或小服务器也想让朋友、群友、工作室同事“无门槛”用上你折腾好的模型这篇文章应该对你有用。我会把AIYA从架构设计、核心代码、部署踩坑到多用户排队机制完整拆一遍尽量让拿到手的人能照着自己造一个而不是只围观个Demo。1. 为什么非要把SD搬进Discord从“自己玩”到“一起玩”的临门一脚先说说动机。很多人折腾Stable Diffusion WebUI往往经历三个阶段刚跑通时兴奋得半夜调模型过两周发现自己一个人刷图刷到审美疲劳再后来就变成了“朋友想要一张图你得开电脑、等加载、出图、找文件、压缩、发过去”。这中间只要有一环嫌麻烦这次“帮朋友出图”就黄了。Discord机器人接口解决的就是这最后一公里问题。它把生成能力从一个需要手动打开浏览器操作的技术工具变成了一个聊天框里现成的斜杠命令。群里有人想要概念图、想看看某个角色换个姿势的样子、想给文案配一张参考图直接/draw一发几十秒后图自己出现在频道里整个过程没有任何学习成本。对于小工作室、兴趣社群、私人服务器这种几个人到几十个人的场景这比重新给每个人开WebUI账号、再解释什么是采样器和CFG要高效太多。AIYA这个项目的定位也因此很明确它不是做给所有人的公开机器人而是私有部署的“群聊生图助手”。适合这几类人来参考手里有本地SD环境想让朋友和同事一起用的人想给Discord社群加一个稳定出图入口、但又不想把服务器地址暴露在公网的管理员想把Stable Diffusion WebUI或ComfyUI封装成后端服务对外提供可编程接口的人。如果你属于其中任一类后面这些内容都可以直接抄作业。2. AIYA的整体骨架交互、调度、生成三层分离一个能用的SD Discord机器人不能只是“收到消息-调API-发图片”这么简单粗暴。真这么干第二天你就会被Discord的交互超时机制折磨到怀疑人生。AIYA的整体结构我分了三个层次每层职责单一出了问题也能快速定位。2.1 三个核心组件的职责边界第一层是交互层负责跟Discord API打交道。我这里用的是Python生态最成熟的discord.py库的新分支py-cord它对斜杠命令的支持比老版discord.py干净不少类型提示也更完整。交互层只做两件事接收命令、发送消息和图片。所有业务逻辑都不允许写在这一层否则后续加功能会非常痛苦。第二层是调度层这是AIYA的核心。它维护一个异步任务队列接收来自交互层的生成请求按顺序交给后端执行然后跟踪每个任务的状态。调度层最关键的设计是机器人进程必须保持事件循环不被阻塞任何耗时操作都不能卡在命令处理函数里。第三层是生成层真正调用Stable Diffusion后端的地方。AIYA在设计上做了一层抽象同一个调度层既能对接WebUI Forge的REST API也能对接ComfyUI的WebSocket接口。实际使用中我默认走Forge的HTTP API因为对文生图这种单次请求场景REST接口足够简单直接。2.2 为什么必须用异步任务队列Discord那3秒交互窗口这里涉及Discord的一个硬性机制很多人第一次写bot都会踩用户发出一条斜杠命令后Discord要求机器人在3秒内对这次交互做出响应否则这个命令就会在客户端显示“应用无响应”。3秒够干什么够你从队列里取任务但根本不够SD出图哪怕用最快的模型也要好几秒。正确做法是收到命令后立刻调用response.defer()做延迟响应告诉Discord“我收到请求了正在处理稍后会给结果”。这条延迟响应的通道有15分钟的有效期超过这个时间就不能再向原交互发送消息了。有了这个机制AIYA就可以放心地异步处理命令来了先排队告诉用户“你的任务已进入第3位”然后Worker慢慢跑SD跑完之后再通过之前保存的交互对象把图片发回频道。整个链路全都是异步的机器人进程不会因为某个用户生成超长分辨率图片就卡死其他人依然可以正常发命令、查排队状态。2.3 后端选型对比Forge和ComfyUI怎么选很多人在第一步就纠结用Stable Diffusion WebUI Forge还是ComfyUI我两个都接过简单说一下实际体验。对比维度WebUI ForgeComfyUI文生图APIREST接口简单直接参数和WebUI界面一一对应WebSocket接口需要先导出工作流JSON再改造进度反馈轮询/sdapi/v1/progress接口拿当前进度值WebSocket会主动推送进度事件体验更好图生图/工作流二次元调参便利Hires.fix、ADetailer、LoRA接入成熟节点式工作流灵活适合批处理和复杂管线排队与并发单任务为主多任务需自己控制原生支持排队机制但接入代码量更大我的结论是如果主要做文生图、图生图、LoRA调参这些日常操作Forge优先。API可以理解为“界面操作的程序化版本”几乎不需要额外学习成本。如果后面要做ControlNet多人协作、批量出图、视频生图这类复杂需求再考虑给AIYA加一个ComfyUI后端让同一条命令在不同后端之间切换。3. 从斜杠命令到出图回传核心流程一步步拆解这一节是全文最核心的实操部分。我会把/draw命令从参数设计到图片回传的完整链路拆开讲包括关键代码写法。你可以在自己机器上跑通这一段AIYA的最小可用版本就诞生了。3.1 命令参数怎么设计才顺手给群里人用的命令参数不能太复杂。你在WebUI里可以调几十个参数但让群友理解什么是一个“好的CFG Scale”属实是强人所难。AIYA的/draw命令只暴露了这几个参数剩下的全部走后端默认值prompt必填正向提示词negative可选负面提示词默认用后端配置的通用负面词width/height可选默认512x768限制最大1024steps可选默认20限制范围1-40cfg可选默认7限制范围1-15seed可选默认-1随机model可选切换模型用不传则保持当前模型这里的关键点是每个参数都必须有边界限制。Discord群里什么人都有有人真会给你发一个2048x2048加50步的请求如果你不限制一块12G显存的显卡可能直接OOM然后整个Worker线程被卡住后面所有人排队陪葬。限制参数本质上是保护后端可用性。参数有了接下来看它们怎么映射到SD API。Forge的接口路径是/sdapi/v1/txt2img请求体的JSON字段和命令参数的对应关系如下命令参数SD API字段说明promptprompt直接透传negativenegative_prompt直接透传widthwidth像素宽度heightheight像素高度stepssteps采样步数cfgcfg_scale提示词引导强度seedseed-1表示随机3.2 Worker核心代码队列消费和调用SD API下面这段代码是AIYA调度层的核心骨架一个常驻的异步Worker从队列里取任务、打SD API、把结果写回任务对象。import asyncio import aiohttp import base64 from dataclasses import dataclass dataclass class GenerationTask: interaction: object payload: dict channel_id: int author_id: int status: str queued class SDWorker: def __init__(self, api_url: str): self.api_url api_url self.queue asyncio.Queue() async def start(self): asyncio.create_task(self._run()) async def submit(self, task: GenerationTask): await self.queue.put(task) async def _run(self): while True: task await self.queue.get() task.status generating try: images await self._call_sd_api(task.payload) task.images images task.status done except Exception as e: task.status failed task.error str(e) finally: self.queue.task_done() async def _call_sd_api(self, payload: dict) - list: async with aiohttp.ClientSession() as session: async with session.post( f{self.api_url}/sdapi/v1/txt2img, jsonpayload, timeoutaiohttp.ClientTimeout(total300) ) as resp: if resp.status ! 200: raise RuntimeError(fSD API返回HTTP {resp.status}) data await resp.json() # Forge返回的images是base64字符串列表 return [base64.b64decode(img) for img in data[images]]这段代码有几个设计点值得说明。首先是用aiohttp而不是同步的requests因为Worker运行在事件循环里任何同步阻塞都会卡住整个机器人。其次是超时时间设了300秒这是为了防止某些极端情况下SD进程死掉导致请求永久悬挂——遇到过真遇到过没有超时的Worker会变成僵尸。至于单个任务失败会不会拖垮队列不会。异常被捕获后标记成failed循环继续处理下一个任务。这是异步队列做任务调度最基础也最可靠的好处。3.3 回复交互先ack、再排队提示、最后上传图片生成流程结束后AIYA要把结果送回Discord频道。这里要注意交互对象的生命周期可以在命令处理函数里保存interaction即使命令函数已经返回后续异步任务依然可以通过这个对象发消息。class DrawCommand(commands.Cog): def __init__(self, bot, worker: SDWorker): self.bot bot self.worker worker app_commands.command(namedraw, description生成一张AI图片) async def draw( self, interaction: discord.Interaction, prompt: str, negative: str , width: int 512, height: int 768, steps: int 20, cfg: float 7.0, seed: int -1 ): # 第一步立刻告诉Discord“我在处理” await interaction.response.defer() # 第二步构造任务丢进队列 payload { prompt: prompt, negative_prompt: negative or lowres, bad anatomy, bad hands, width: min(width, 1024), height: min(height, 1024), steps: max(1, min(steps, 40)), cfg_scale: max(1.0, min(cfg, 15.0)), seed: seed, sampler_name: DPM 2M Karras, } task GenerationTask( interactioninteraction, payloadpayload, channel_idinteraction.channel_id, author_idinteraction.user.id, ) await self.worker.submit(task) # 第三步先发一条排队提示让用户知道任务状态 await interaction.edit_original_response(content图片生成中预计需要20-60秒请稍候……)生成完成后的发送逻辑在Worker完成后触发往同一个交互通道发送图片附件async def _send_result(task: GenerationTask): if task.status failed: await task.interaction.edit_original_response( contentf生成失败了{task.error} ) return files [discord.File(io.BytesIO(img), filenamefaiya_{task.i}.png) for i, task_line in enumerate(task.images)] await task.interaction.edit_original_response(content生成完毕图片如下, attachments[]) await task.interaction.channel.send( contentf{task.author_id} 你的图片来了耗时约 {task.elapsed:.1f} 秒, filesfiles )这里用一个细节图片单独通过频道消息发送而不是塞进交互的原始回复里。原因是交互回复如果带多个附件客户端显示格式比较奇怪而且在频道里单独发一张带用户提及的图其他人也能看到、能互动更有“群聊一起玩”的感觉。4. 部署走查几个绕不开的坑和排查方法前面讲的都是理想状态下的流程实际部署到AIYA走上正轨我至少踩了三轮坑。这些坑不在官方文档里纯靠搜索引擎也不一定能快速找到答案这里完整记下来。4.1 run.bat卡在installing requirement的真实情况这个现象在Stable Diffusion WebUI Forge用户里非常常见双击run.bat后终端输出停留在installing requirement或者某个pip install阶段半小时没有动静。新手第一反应是“是不是卡死了”其实大部分时间是卡在依赖下载慢上。排查链路是这样的。第一步先看CPU和磁盘活动。在Windows任务管理器里如果Python进程的CPU占用率很低但是磁盘或网络活动的数字一直在跳说明pip还在下载只是慢。第二步看下载的内容Forge安装时会拉取torch、torchvision这些体积巨大的包单看进度条它可能一直停留在“Downloading”状态一个多G的包在高峰期走CDN通道确实要等很久。解决办法在我看来最有效的是两条。一是给pip配置国内镜像源在用户目录下建一个pip.ini文件写入[global] index-url https://pypi.tuna.tsinghua.edu.cn/simple [install] trusted-host pypi.tuna.tsinghua.edu.cn二是先手动单独安装torch系列再让run.bat安装剩余依赖。这样即使后续出问题你也能明确判断是哪一步出错而不是面对一整屏的pip日志无从下手。还有一个容易忽略的点Forge启动时除了Python依赖还会从HuggingFace下载基础模型文件。如果看到日志卡在一个Downloading model...但不是pip进度条可以去设置环境变量HF_ENDPOINThttps://hf-mirror.com指向镜像站点模型下载速度会有明显提高。4.2 Discord机器人连不上的隐藏原因代码写好了运行之后Discord机器人却连不上网关这个环节能把人逼疯。很多人第一时间会怀疑环境问题但根据我自己的排查经验绝大多数情况其实是几个看似不起眼的配置错误。第一是bot token没配对。Discord开发者平台创建应用后会自动生成一个token但如果你在代码里配置的是Client Secret而不是Bot Tokenpy-cord不会报“token错误”而是抛出更迷惑的TypeError或者一直连不上。我的经验是token一定要从Bot页面单独复制不要从General Information页面抄。第二是intents没有开全。现在的Discord API要求机器人显式声明需要哪些意图Intents。如果你要监听消息内容、看频道成员列表但不启用在开发者后台勾选对应的Privileged Intents连接本身能成功但功能上会静默失效——表现为命令能注册但找不到服务器或者忽略部分消息事件。第三是重复启动了多个机器人实例。这个问题特别隐蔽之前测试窗口没关又重新跑了一次python main.py两个进程抢同一个token的网关连接Discord会把其中一个连接踢下线。日志里会出现反复的WebSocket closed with code 4004这就是典型的多实例冲突。还有一个容易忽略的小点如果服务器的系统时间不准JWT的签名校验会失败表现也是连接不稳定。确保机器上开了NTP时间同步这个坑手动排查起来极其费劲。4.3 出图画质与本地不一致跑通之后下一个大坑通过机器人生成出来的图跟本地WebUI里手动生成的效果怎么都对不上。模型没错、提示词也一样但画质就是差一截。这类问题90%出在API调用时省略了WebUI界面里默认启用的一些关键选项。最典型的三个Hires.fix没开本地人习惯在WebUI里勾选高清修复但API请求只包含基础参数如果你在构造payload时忘了加enable_hr: true那出的图自然没有修复后的细节。VAE没设对某些底模不内置VAE依赖WebUI默认选择外置VAE。API请求不会自动带上界面里的VAE选项需要单独调/sdapi/v1/options设置sd_vae。采样器和调度器组合不一致本地WebUI的采样器选择框在API里被拆成了sampler_name和scheduler两个字段如果API请求只指定了前者实际用的调度方式会回到默认值最终画风会和本地有明显差异。我的经验是做一个“参数对齐清单”把本地WebUI里每次生成都会在PNG info里显示的所有选项提取出来然后确保API请求JSON里的每个字段都与之对应。不要相信“跟界面一样的名字就一定传了同样的值”这种直觉。5. 多人共用一个bot的作业机制排队、限流和显存控制一个人用机器人很轻松几个人同时发命令才是考验设计的开始。我这里说几个经过实战检验的机制。5.1 并发数怎么定大部分机器串行最稳从理论上看SD并发生成可以提高显卡利用率特别是显存足够大的卡。但在聊天机器人的场景里我强烈建议默认串行也就是一次只处理一个生成任务。原因很简单多个SD进程同时跑每个进程都要加载模型到显存16G显存跑两个任务时单任务速度会明显下降而总吞吐量并没有提升多少反而大幅增加了OOM概率。串行执行时显存只存在一份模型驻留单个图生成时间稳定队伍虽长但“可预期”。判断依据其实不用很复杂单张512x768、20步、DPM 2M Karras在自己的机器上实测一次要多少秒然后直接用这个时间乘以队列长度告诉用户预计等待时间体验远好于“我看到好几个人同时发了命令但谁也不出图”的混乱感。5.2 优先级队列和按用户限流多用户场景下只靠一个先进先出的队列会有问题如果有人故意刷几十个任务其他人的请求会被堵在最后面整个频道的生图体验瞬间崩塌。AIYA的处理方式是双队列设计——普通队列和高优队列Worker每次先尝试从高优队列取任务取不到再从普通队列取。async def _run(self): while True: waiters [self.high_priority_queue.get(), self.normal_queue.get()] done, pending await asyncio.wait(waiters, return_whenasyncio.FIRST_COMPLETED) for task in pending: task.cancel() task done.pop().result() await self._process_task(task)同时AIYA还会按用户维度做频率限制。最简单的实现是维护一个dict[user_id] 最近任务时间戳限制每人对全局队列的提交间隔不低于20秒。这个数字可以根据群聊活跃度调节但这套机制确保任何单个用户都无法“霸占”整个bot的生成能力。5.3 超时、失败重试与通知设计排队排长了的直接后果是Discord那条15分钟的交互通道可能过期。AIYA的处理是如果任务在排队时已经超过10分钟Worker就不再做图片生成了直接标记为超时并用新的一次性Webhook消息通知用户重新发送命令。import time async def _process_task(self, task: GenerationTask): elapsed time.time() - task.created_at if elapsed 600: task.status timeout await task.interaction.edit_original_response( content排队时间过长任务已取消请重新发送命令。 ) return # 正常生成流程……单个生成任务失败的处理我采用的是线性重试机制失败后等3秒再试一次最多重试两次仍然失败才向用户返回错误。不要用指数退避这里不是大型分布式系统线性重试更直观也更容易在日志里判断故障模式。6. 进阶玩法模型切换、画廊统计和自动审核基础链路稳定之后AIYA可以往这两个方向长这部分是真正提高“群聊生图”幸福感的功能。6.1 指令化的模型管理与LoRA入口一个机器人只用一个模型时间长了群友会腻。AIYA通过/model命令切换Checkpoint模型同时提供/lora命令动态加载LoRA权重。Switching的实现其实就是调用Forge的/sdapi/v1/options接口更新sd_model_checkpoint字段但要注意切换之后首次生成需要重新加载模型加载期间不能处理其他任务否则会出现“图是旧模型生成的”这种混乱。我的策略是切换模型时锁定Worker强制空转5秒保证模型就绪再开始从队列取任务。对LoRA直接在提示词里追加lora:模型名:权重即可但对群友来说这段语法过于抽象。AIYA的做法是把LoRA封装成“风格标签”用户只需输入style: pixel机器人自动映射到对应的正向提示词和LoRA组合。这层映射表放在一个YAML配置文件中非技术成员也能自己维护。6.2 画廊与统计给AIYA加一个简单的SQLite记录表每个生成请求都记下用户ID、提示词、参数、生成耗时和图片文件名。好处有两个一是群友可以/mystats查看自己今天的生成数量主动玩出“谁刷图最多”的竞速二是维护者可以通过统计数据判断机器人运行状态比如当平均生成耗时突然从15秒涨到40秒大概率是模型被切换成了更重的版本或者样本图片分辨率被某些参数绕过限制。数据库记录还有一个隐藏收益做“再生成一次”功能。群友如果觉得某张图不错但不小心关掉了Discord窗口直接对结果图片点击/againAIYA会读取该图片绑定的存储ID把原参数重新提交给队列。某些可复现的好图就是这样被反复利用的。6.3 内容安全性词过滤、私服优先这是在Discord上跑生成机器人绕不开的一环。我的建议是AIYA默认只在私有服务器或指定的白名单频道开放不公开邀请到大型公共服务器。同时在命令入口加一个简单的关键词过滤模块命中高危类别词直接拒绝执行并记录日志。这里不用做得很复杂能防住明显的违规内容就够了。另外还要注意Discord的Age-Restricted频道机制。如果频道启用了年龄限制机器人在该频道发布的图片即使包含普通生成内容也可能被系统标记影响频道声誉。AIYA默认会检测频道类型遇到Age-Restricted频道会限制一部分内容生成避免模糊地带。最后再分享一个我自己的优化习惯AIYA启动后先预加载一次默认模型。手动调用一次空生成的文生图接口让模型驻留显存后面群友的第一次请求就能直接出图不用现场加载。这个方法没有写在任何教程里但对“第一个发命令的人体验”提升非常明显。如果你也想搭一个类似的群聊生图机器人建议从最小可用版本开始先把一条生成链路完整跑通后面再慢慢加功能。这个项目的乐趣在于你搭好它的那一刻看到群里的人因为一张图开始聊天、讨论、催更你会觉得前面踩的那些坑都值了。