
把本地大模型和Stable Diffusion真正串起来做成一套服务前前后后花了我小半个月。起因其实很简单我一直在用Diffusers本地跑文生图但提示词这件事始终是个麻烦。直接说中文模型理解得七零八落自己硬写英文提示词又常常词不达意。SDXL这类模型对提示词的讲究远比你想象中严格——主体、环境、光线、风格、画质词缺一样出图就偏。于是我就想既然本地已经能跑大模型了为什么不让它来当这个翻译官这才有了这篇实战总结用Ollama部署一个7B量级的本地大模型负责把用户随口说出的想法整理成Stable Diffusion能听懂的提示词用Diffusers调用SDXL生成图片再用FastAPI把整条链路包成一个干净的REST接口。这套东西适合谁想在内网或本地搭建文生图服务的人、做AI绘画工具但不想依赖云端接口的开发者、以及所有被提示词折磨过的朋友。它解决的核心问题就一句话让AI绘画更听话。1. 先想清楚为什么这三样东西要凑在一起1.1 裸用Stable Diffusion的痛其实全在提示词上很多人第一次用Diffusers本地出图第一反应是模型好难控制。这事真不怪模型怪提示词。Stable Diffusion系列模型是在海量英文图文对上训练的它理解的漂亮女孩在花园里和你脑子里的画面根本不是一回事。你需要给出足够具体的主体描述、场景定义、光线方向、镜头语言、风格流派还要顺手加上masterpiece, best quality这类画质词否则出图就是灰蒙蒙一片。更要命的是中文输入——模型训练语料里中文占比很低直接丢一句月光下的古镇进去经常出来一张构图混乱、语义断裂的废图。我最早的做法是写死一套英文模板让用户往里面填词。但填词这个事对普通用户来说门槛太高了而且每个人描述习惯不一样模板一限死画面就千篇一律。后来我才意识到与其逼用户去学提示词语法不如让另一个模型去理解用户的话、再翻译成模型听得懂的行话。这就是Ollama在这个项目里的位置。1.2 Ollama在这里不是画画的是当翻译官的项目标题里把Ollama放在前面很多人误以为图片是它生成的其实不是。Ollama负责的是自然语言理解这一层接收用户一句大白话输出一段结构化的英文提示词甚至顺带给出英文负面提示词。选它的理由很朴素——本地部署、免费、数据不出机器、一条命令启动而且它对显卡的利用率比裸跑Python大模型脚本高不少模型管理也省心。模型我选的是qwen2.5:7b。7B这个体量在普通消费级显卡上能跑得动理解中文和改写英文提示词的活儿它完全够用。如果你显卡更大换14B或者32B版本效果会更好但说实话提示词改写这个任务7B和32B的差距很小远不如在图像模型本身上下功夫。这里有一个新玩家容易踩的坑如果你选的是带思维链或思考模式的模型响应里会夹带一长串推理过程直接污染提示词。新版Ollama对这类模型提供了关闭思考的选项或者在system prompt里明确要求不要输出任何分析过程只输出提示词本身实测后者更稳。1.3 FastAPI为什么是合适的胶水层链路里最后一个环节是FastAPI。选它不是因为流行而是因为它实在太贴合这个场景了。第一Pydantic的请求校验能帮我把接口参数限制得明明白白分辨率、步数、引导系数这些数值该在什么范围直接在模型定义里写死非法请求根本进不了业务逻辑。第二FastAPI自动生成交互式文档接口调完还能在浏览器里直接试参数调试效率高出一截。第三Python生态和torch无缝衔接不需要像Java、Go那样再包一层。最重要的一点是FastAPI对同步阻塞任务的态度很务实——你定义普通函数它自动丢进线程池执行不会卡死事件循环。这个细节在文生图场景里非常关键后面会专门说。2. 整体架构与目录设计2.1 一次完整的请求是怎么流转的先把全链路讲清楚你才知道代码该怎么组织。用户向FastAPI发一个POST请求JSON里带着一句口语化的创作想法比如月光下的江南古镇河面有倒影。FastAPI先做参数校验然后调用Ollama客户端把这句话加上系统提示词模板一起发给Ollama的/api/generate接口。Ollama返回的是一段英文结构化提示词可能还附一句负面提示词。拿到它之后封装好的Diffusers管线开始工作加载的Stable Diffusion模型根据提示词做去噪生成出一张PIL图片。最后FastAPI把图片编码成PNG字节流通过HTTP响应返回给调用方。整条链路里Ollama是翻译官Diffusers是画师FastAPI是调度台。2.2 项目目录结构参考我实际用的是这样一个结构麻雀虽小五脏俱全后续加功能也不至于推倒重来text2image-api/ ├── app/ │ ├── __init__.py │ ├── main.py # FastAPI入口路由注册 │ ├── config.py # 全局配置模型名、路径、Ollama地址 │ ├── schemas.py # Pydantic请求/响应模型 │ ├── ollama_client.py # Ollama提示词增强模块 │ ├── sd_pipeline.py # Diffusers文生图封装 │ └── utils.py # 图片处理、种子处理等小工具 ├── models/ # 本地模型缓存目录按需设置 ├── output/ # 生成图片落盘目录 ├── requirements.txt └── README.mdconfig.py里我额外做了两件事一是所有路径用环境变量兜底比如MODEL_CACHE_DIR、OLLAMA_BASE_URL方便以后迁移到Docker二是模型名和默认参数集中管理免得散落在代码里改一处漏一处。2.3 开工前必须想清楚的三个设计决策这个项目有几个决策点想清楚了后面能省大量返工时间。一是模型加载时机。Stable Diffusion模型加载一次要几十秒甚至几分钟绝不能每次请求都重新加载。正确做法是在FastAPI的启动生命周期里把模型加载到内存进程退出时再卸载。新版FastAPI建议用lifespan上下文管理器代替早年的app.on_event写法更干净也避免弃用警告。二是请求同步还是异步。Diffusers的生成任务是典型的重CPU/GPU阻塞任务。如果你把接口函数定义成async def一旦生成开始整个事件循环就被占死了其他所有请求都排队等它这绝对是灾难。反过来用普通的def定义接口函数FastAPI会让它在线程池里执行多个请求可以并行跑。当然线程池也不适合承载几十个并发的重度任务真正的高并发场景要上任务队列这个我放到最后说。三是Ollama请求的超时控制。大模型改写提示词一般几秒内完成但偶尔也会因为模型加载、显存抖动拖到几十秒。我给Ollama客户端设了60秒超时调用失败时直接降级返回原始提示词而不是让整个接口报错。这个降级逻辑很实用——用户等一张图已经够久了不能再因为翻译官掉链子而白等。3. 环境准备硬件、依赖与模型下载3.1 硬件底线和CUDA环境先说硬件这是劝退最多人的地方。跑SDXL全精度推理最低建议12GB显存起步16GB能比较从容地同时跑Ollama和Diffusers——注意这两个模型是同时驻留显存的显存分配上要留余量。如果你的卡只有6到8GB也不是不能玩两个办法一是换用SD 1.5系列的模型出图尺寸512x512显存占用能压到4GB左右二是给Diffusers管线开enable_model_cpu_offload()让模型层在显存和内存之间自动换入换出速度慢一些但跑得动。纯CPU推理我不是很推荐一张512的图要几分钟甚至十几分钟体验很折磨。CUDA的坑主要是torch版本和显卡驱动不匹配。装torch之前先确认自己的CUDA版本然后去PyTorch官网选对应的安装命令。一个典型错误是装成CPU版的torch还在那折腾显存报错先用torch.cuda.is_available()验一下真金白银的检查比直觉靠谱。3.2 Ollama安装与模型拉取以及下载慢的解法Ollama的安装本身很简单官网下个安装包一路下一步就行。真正卡人的是两件事一是模型下载慢二是模型默认存到C盘。先解决C盘问题。Windows下设置环境变量OLLAMA_MODELSD:\ollama\models设置完重启Ollama服务新拉取的模型就会进D盘。很多人是拉了两三个模型之后才发现C盘爆了再迁移就麻烦得多建议第一步就设好。再解决下载慢。Ollama官方源的模型文件动辄几个GB网络状况不好时ollama pull qwen2.5:7b能挂一晚上。实际操作中我试过三种靠谱的路子第一安装包和模型都从GitHub releases或国内第三方转存地址拿速度比官网稳第二直接去HuggingFace把对应模型的GGUF文件下载下来放到本地后写一个简单的Modelfile用ollama create mymodel -f Modelfile从本地文件导入这招在离线环境里几乎是唯一解第三给Python侧的依赖下载配置镜像加速比如给transformers和diffusers设置HF_ENDPOINThttps://hf-mirror.com环境变量模型缓存路径统一指到本地目录。这些都属于常规的下载加速手段不是黑魔法但确实能把几小时的等待压缩到几分钟。3.3 Python依赖清单依赖文件长这样版本以你安装时最新的稳定版为准但diffusers和transformers建议锁大版本免得API变动fastapi uvicorn[standard] diffusers transformers accelerate safetensors torch Pillow requests python-multipartpython-multipart是FastAPI处理表单数据用的如果你的接口只用JSON其实可以不要。我加上它是因为后期给接口加了个上传参考图的功能不想再改依赖。Pillow用来做图片编码和尺寸转换requests用来调Ollama的HTTP接口。这里没引入官方ollama的Python SDK因为我只用一个API接口直接requests足够少一层依赖少一份坑。真要用SDK装ollama包然后ollama.generate(modelqwen2.5:7b, prompt...)也能达到同样效果看你习惯。4. 核心代码实现三块拼图逐一拆解4.1 Ollama客户端封装让大模型好好当翻译官先看代码。这里我调的是Ollama的/api/generate接口忽略输出流直接拿完整结果import requests OLLAMA_URL http://localhost:11434/api/generate MODEL_NAME qwen2.5:7b SYSTEM_PROMPT 你是一名AI绘画提示词专家。用户会给你一个简单的创作想法你需要把它改写为Stable Diffusion能理解的结构化提示词。 要求 1. 输出必须全部是英文包含主体、环境、光线、风格、画质词 2. 主体和风格用具体、可视觉化的词汇避免抽象概念 3. 额外输出一行英文负面提示词以NEGATIVE:开头 4. 不要输出任何解释不要输出分析过程只输出提示词本身 5. 整体控制在80个单词以内 def enhance_prompt(user_input: str) - str: payload { model: MODEL_NAME, prompt: f{SYSTEM_PROMPT}\n\n用户的想法{user_input}, stream: False, options: { temperature: 0.7, num_predict: 250 } } resp requests.post(OLLAMA_URL, jsonpayload, timeout60) resp.raise_for_status() data resp.json() raw data[response].strip() negative if NEGATIVE: in raw: prompt_part, neg_part raw.split(NEGATIVE:, 1) return prompt_part.strip(), neg_part.strip() return raw, 为什么temperature设0.7这个值在改写任务里是经验和权衡的结果。设太低比如0.2模型输出会过于保守每次都套同一套模板画面雷同设太高比如1.2改写结果会跑偏甚至编出和用户原意无关的词汇。0.7左右既能保证一定的多样性又不至于失控。另外我明确要求输出NEGATIVE:前缀就是为了让模型把负面词和正面词分开方便后续直接喂给Diffusers做negative prompt。你可能会问直接固定写一份负面提示词不行吗也行但让LLM根据画面内容动态生成更精准——画人像时的负面词和画风景时的负面词侧重点完全不同。4.2 Diffusers封装把SDXL包装成稳定的函数Diffusers这边我封装的是一个纯函数输入提示词和参数输出PIL图片import torch from diffusers import StableDiffusionXLPipeline pipe None def init_pipeline(model_pathstabilityai/stable-diffusion-xl-base-1.0, local_onlyFalse): global pipe pipe StableDiffusionXLPipeline.from_pretrained( model_path, torch_dtypetorch.float16, variantfp16, use_safetensorsTrue, local_files_onlylocal_only, ) pipe.enable_model_cpu_offload() def generate_image(prompt, negative_prompt, width768, height768, steps30, guidance_scale7.5, seed-1): if pipe is None: init_pipeline() if seed 0: seed torch.randint(0, 2 ** 32 - 1, (1,)).item() generator torch.Generator(devicecuda).manual_seed(seed) result pipe( promptprompt, negative_promptnegative_prompt, widthwidth, heightheight, num_inference_stepssteps, guidance_scaleguidance_scale, generatorgenerator, ) return result.images[0], seed有几点解释一下。variantfp16表示使用半精度版本权重显存占用能降低差不多一半use_safetensorsTrue用更安全的safetensors格式加载也更快。enable_model_cpu_offload()对中低端显卡非常友好它会把部分模块放在内存用到哪层再换入显存。代价是每张图可能慢个两三秒但换来的是6GB显存也能碰SDXL这笔交易划算。种子处理是个容易被忽略的细节。这里约定seed-1表示随机实际生成时取一个随机种子并且把用掉的种子返回给调用方。这个设计对调试太重要了——用户说这张图不错但我想微调一下你只要把上次的种子和参数原样传回来复现而不是让他重新碰运气。接口返回的参数里带上seed用户不满意又能基于同一张图微调体验完全不一样。另外一个常见的疑问是为什么用SDXL base而不是SD 1.5或SDXL TurboTurbo出图快但画质细节少适合实时预览base画质上限高配合30步采样质量稳定适合做正式出图。如果你想要速度和质量兼顾可以管线里同时准备两个模型接口加个speed_mode参数切换这是后续很容易扩展的优化项。4.3 FastAPI接口层参数校验和图片返回接口定义用Pydantic模型把参数约束写清楚这是最容易被新手忽略但价值极高的部分from fastapi import FastAPI, HTTPException from fastapi.responses import Response from pydantic import BaseModel, Field from contextlib import asynccontextmanager import io, time, logging logger logging.getLogger(t2i) class GenerateRequest(BaseModel): prompt: str Field(..., min_length2, max_length200, description用户的创作想法) width: int Field(768, ge512, le1024, multiple_of64) height: int Field(768, ge512, le1024, multiple_of64) steps: int Field(30, ge10, le80) guidance_scale: float Field(7.5, ge1.0, le15.0) seed: int Field(-1, ge-1) use_enhance: bool Field(True, description是否启用LLM提示词增强) NEGATIVE_BASE lowres, bad anatomy, bad hands, watermark, text, cropped, worst quality, low quality, jpeg artifacts asynccontextmanager async def lifespan(app: FastAPI): init_pipeline(local_onlyFalse) logger.info(SDXL pipeline loaded) yield logger.info(shutting down) app FastAPI(titleLocal Text-to-Image API, lifespanlifespan) app.post(/v1/generate) def generate(req: GenerateRequest): start time.time() try: final_prompt req.prompt negative_prompt NEGATIVE_BASE if req.use_enhance: enhanced, dynamic_neg enhance_prompt(req.prompt) if enhanced: final_prompt enhanced if dynamic_neg: negative_prompt f{NEGATIVE_BASE}, {dynamic_neg} img, used_seed generate_image( promptfinal_prompt, negative_promptnegative_prompt, widthreq.width, heightreq.height, stepsreq.steps, guidance_scalereq.guidance_scale, seedreq.seed, ) buf io.BytesIO() img.save(buf, formatPNG) logger.info(generate ok, prompt%s, seed%d, cost%.2fs, req.prompt, used_seed, time.time() - start) return Response(contentbuf.getvalue(), media_typeimage/png) except Exception as e: logger.exception(generate failed) raise HTTPException(status_code500, detailstr(e))分辨率限制在512到1024之间且必须是64的倍数这是SD模型的硬约束不符合会出各种奇怪的图像问题。steps限制在10到80一方面防止有人把步数拉满白耗显存另一方面也给快速预览留了空间。multiple_of64这个约束很多人不知道——Stable Diffusion下采样8倍latent空间对尺寸有对齐要求不是64的倍数容易出黑边或构图异常。图片直接以PNG字节流返回media_typeimage/png调用方拿到之后想存文件想转Base64都行。如果你需要同时返回种子和参数信息可以把图片存到output/目录然后在响应的Header里带上X-Seed: 12345这种自定义字段或者干脆改成交互式任务模型这取决于你的实际需求。这里我用的是同步def定义接口函数。按照前面说的FastAPI会把它扔到线程池里跑主事件循环不会被Diffusers的长时间推理堵住。如果你误写成async def后续所有请求都会排队卡死——这是我在一个早期版本里真实踩过的坑排查了半天才发现是事件循环被图形生成占住了。4.4 并发与性能的真实边界简单线程池方案能撑多少并发我用12GB显存的卡实测两个请求同时跑就已经开始明显变慢三个请求很可能直接OOM。原因很简单Diffusers每次生成都要把模型权重和中间激活值放进显存两个并发就接近翻倍占用。所以这个项目的现实定位是轻量内网服务接口响应时间本身就要好几秒并发意义有限。真要做成多人使用的正式服务我建议往任务队列方向演进FastAPI收到请求后把任务丢进Redis/RQ或Celery队列立刻返回一个task_id后台Worker负责实际生成前端轮询任务状态拿结果。步骤拆开之后接口层只管接收和查询生成任务全部排队执行显存占用天然受控。这是从能跑到好用的关键一步等基础版跑通后再上也不迟。5. 让AI绘画听话的提示词策略5.1 给LLM写好岗位说明书很多人用了Ollama做提示词增强但效果不佳问题几乎都出在system prompt上。大模型是极强的角色扮演者你给它清晰的角色定义和输出格式约束它的表现会完全不一样。我给Ollama的system prompt里反复强调了几件事输出全英文、包含结构化元素、禁止输出分析过程、给出负面词。这四条规定分别解决四个痛点——英文对齐训练分布、结构提升画面完整度、防止思维链污染、负面词提升构图干净度。这里有个实用技巧允许在改写结果里保留少量中文关键词比如江南古镇这种带有强文化属性的词汇直接音译或半中半英反而让Stable Diffusion难以理解。实测下来江南古镇保留中文比翻译成ancient town in Jiangnan效果更好。当然这个取决于你用的图像模型底模如果是偏写实的大模型这种文化词保留原样更稳妥。可以让LLM在拿不准时保留中文反正后续Diffusers的CLIP Text Encoder对中文也有一定理解能力。5.2 参数组合对照表别复制别人的默认值下面这张表是我跑了几百张图之后沉淀下来的参数建议不同出图目的用不同组合参数快速预览标准出图精细出图steps153045guidance_scale6.07.58.5分辨率512x512768x7681024x1024适合场景灵感试错常规生成成品交付steps和guidance_scale是相互关联的。steps太少低于10时模型还没收敛画面糊太高超过80纯属浪费算力后期收益几乎为零。guidance_scale控制的是提示词对画面的影响强度——太低画面自由发挥可能偏离提示词太高画面过度迎合提示词色彩会发灰发硬。7.5是SD系列比较经典的平衡点但具体模型和底模不同建议在6到9之间自己试几轮找到你手上这个底模的甜点区。5.3 实测对比同一个想法两种效果我给团队演示过一次最直观的对比。同一句输入一个女孩在雨夜的便利店门口霓虹灯赛博朋克风格。未加增强链路时把这句话原样喂给SDXL出来的是构图混乱、风格不明的图人物和环境搅在一起。走Ollama增强链路后改写出来的提示词长这样a girl standing outside a convenience store in the rainy night, neon lights reflecting on wet pavement, cyberpunk style, cinematic lighting, detailed face, masterpiece, best quality负面词补充了motion blur, messy composition。最终出图霓虹氛围、雨夜质感、人物主体都清晰可见。这就是听话的具体含义——模型不是理解你的中文而是理解了被翻译成它母语后的精确指令。这套链路还有一个隐性收益用户端的体验统一了。不管用户用词多口语化、多零碎LLM都会自动补全要素、纠偏方向输出的图片质量方差变小。换句话说你是在用一个聪明的模型去驯服另一个不那么聪明的模型最终用户只感受到一个稳定的服务。6. 常见问题与排查实录6.1 Ollama下载慢、装完还老失效怎么办这是被问得最多的问题。ollama pull qwen2.5:7b半天不动第一件事别反复重试先看是不是存储路径问题。Ollama默认把模型放在用户目录的.ollama/models下如果你C盘快满了下载会先卡住。按前面说的设置OLLAMA_MODELS环境变量后重启服务再重新pull。如果网络确实慢最稳的办法是绕过pull去HuggingFace找对应模型的GGUF文件用工具下载到本地再写一个Modelfile内容就是一行FROM /path/to/model.gguf然后ollama create mymodel -f Modelfile。这个方案在离线环境里几乎是唯一解也方便你统一管理模型文件。另外Ollama对GPU的使用是自动的想确认模型真的用了显卡执行ollama ps看模型对应的显存占用如果显示CPU跑检查驱动和CUDA版本。6.2 显存爆了CUDA out of memoryDiffusers最常见的报错就是显存不足。三步排查第一步确认torch.cuda.is_available()为True很多人用的是CPU版torch第二步把StableDiffusionXLPipeline的torch_dtype设为torch.float16这一步能省接近一半显存第三步开启enable_model_cpu_offload()代价是速度慢一点。如果还不够降分辨率、换SD 1.5模型、减少steps这三个是降显存的铁三角。Ollama和Diffusers同时跑显存打架的问题也需要留意。两个模型的常驻显存是叠加的建议给Ollama设置OLLAMA_MAX_LOADED_MODELS1并且用OLLAMA_NUM_PARALLEL1限制同时处理的请求数给Diffusers留足空间。6.3 Diffusers模型下载和缓存的老大难第一次运行from_pretrained会从HuggingFace拉权重国内网络慢是常态而且模型动不动几个GB。解决办法是设置环境变量HF_ENDPOINThttps://hf-mirror.com再指定HF_HOME把缓存放到一个有足够空间的目录。模型下载一次后会缓存在本地以后走local_files_onlyTrue就能离线加载。如果你已经有一套下载好的模型目录也可以通过from_pretrained(/本地路径)直接加载不一定要走HuggingFace的缓存逻辑。版本兼容问题也常碰到diffusers版本太旧SDXL的variantfp16参数不识别transformers版本太新和某个底模的tokenizer不兼容。我的建议是不要盲目追新锁定一个大版本内的最新小版本跑通之后不要随意升级。我把requirements里的关键包用和锁了范围避免某天队友pip install把环境搞坏。6.4 接口层面的三个隐蔽坑第一个坑是请求超时。默认的requests超时很短图片生成动辄几十秒客户端早断开了。要在调用侧设置合理的超时参数至少给足两分钟或者改用异步任务轮询。第二个坑是FastAPI里误用async def。前面反复强调重计算任务用同步def让FastAPI走线程池这个顺序千万别搞反。判断标准很简单函数里有没有真实的异步IO有才用async def纯计算一律用普通def。第三个坑是返回图片时的响应头。如果你直接return bytesFastAPI会当成JSON处理导致报错必须用Response(contentbuf.getvalue(), media_typeimage/png)。要额外传种子信息记得加自定义Header但注意Header值必须是字符串。6.5 一些零散但救过命的小技巧最后分享几个散装经验不按逻辑排序都是踩过坑之后才记住的启动时先做一次模型自检比如初始化管线后跑一张8x8的小图有问题立刻报错退出别等用户第一个请求才炸。记录生成日志时把输入提示词、最终提示词、种子、耗时都打出来排查用户问题全靠它。接口加一个/v1/health健康检查接口返回模型加载状态和显存余量部署到Docker后探活就靠它。用Docker部署的话给Ollama和FastAPI各起一个容器通过docker-compose管理GPU透传记得加gpusall配置。想接一个Web前端时FastAPI直接托管静态文件把HTML丢进去出一个简易版绘画页面整个服务就完整了。这套链路跑通之后我又陆续往里加了LoRA切换、ControlNet边缘控制甚至把Ollama换成了更强的大模型做更复杂的提示词规划。底子打好了后面每一步都是锦上添花。关于听话这件事我最后的体会是模型不会真正理解你的意图它只理解你给的指令。你的工作就是搭好一条把意图翻译成指令的流水线而Ollama加Diffusers加FastAPI这三件套恰好能把这件事做得足够稳、足够顺。