DALL·E与Gemini批量生图实战:API调用、并发限速与质量管控 1. 先想清楚再动手DALL·E 与 Gemini 的生图定位差异1.1 两者的接口形态与生成风格差异做批量生图之前最容易被忽略的一件事就是DALL·E 和 Gemini 根本不是同一类东西。虽然它们都叫“文生图模型”但接口形态、返回方式、适用场景差别很大。DALL·E 走的是 OpenAI 的 Images API核心就是images.generate这一组接口。你传入提示词、指定尺寸和数量它同步返回一张或一组图片的 URL 或者 base64 数据。生成质量稳定、语义理解细腻尤其擅长把长句子里的多个约束条件拆解到画面里。比如“一只穿红色雨衣的柴犬站在东京街头身后是霓虹灯招牌雨滴清晰可见”这种多层描述DALL·E 基本都能还原到位。它的短板也很明显单次请求最多生成 10 张SDK 频率限制严格量一大就得考虑排队和退避。Gemini 这边则更像是多模态大模型的“附带技能”。你调用的是 Gemini API 里的generateContent接口把提示词放进contents里同时指定responseModalities: [Image]模型会返回生成图像的内联 base64 数据。它的优势在于如果你本来就在用 Gemini 做文本生成、图片理解、甚至视频理解再顺手接入生图能力整个架构可以共用一套鉴权和调用链路不用为生图单独维护一套 SDK。Gemini 的图在写实风格和光影质感上有自己的特色某些场景下甚至比 DALL·E 更细腻但在复杂指令的还原度上没有 DALL·E 那么“较真”。我自己的感受是如果项目核心就是“把一段描述变成高质量图片”选 DALL·E 更省心如果项目本身是多模态应用比如图文问答、内容审核、配图生成一体化的内部工具那用 Gemini 可以把系统复杂度压得更低。1.2 选型矩阵什么时候用 DALL·E什么时候用 Gemini先给一个可以直接抄作业的判断表场景推荐选择理由电商商品图、营销海报对画面还原度要求极高DALL·E复杂的商品描述还原更准出图稳定大量不同风格插画、虚拟形象Gemini光影质感好批次之间风格一致性比较容易控制多模态应用中的配图模块Gemini与文本生成共用一套 API链路简单高并发、大批量日均千张以上DALL·E 排队机制官方限速虽有但退避机制成熟文档齐全成本敏感的小团队Gemini 免费额度 低档位免费额度初期够用试错成本低需要严格按固定比例出图如 1:1 头像、9:16 封面两者均可需实测不同尺寸支持能力不同实测为准选择的关键不是“哪个模型更强”而是“你的工程链路到底愿不愿意为某个模型做专门适配”。DALL·E 的优势是生态成熟Python SDK 里就能完成几乎所有事Gemini 的优势是统一如果你已有的项目里 Google 系服务占了很大比重硬塞一套 OpenAI SDK 反而显得臃肿。另外一个很实际的考量账号和网络的可得性。这一点我不想展开说太多但提醒大家在实际部署前先确认好当前网络环境下哪个服务能够稳定访问、鉴权畅通。这直接决定了后续调试的效率。我见过不少团队在架构上选了很理想的方案结果每次跑批处理都要先折腾十分钟环境最终被迫换方案。2. 环境准备与第一张图把步骤落实到能跑通2.1 API 密钥管理与环境变量无论选哪个模型第一步都是密钥管理。别把密钥写死在代码里哪怕项目不公开也尽量不要因为你永远不知道代码什么时候会被分享出去。正确做法是放到环境变量或者项目根目录的.env文件里并且保证.env不进 git。# .env OPENAI_API_KEYsk-xxxxxxxxxxxxxxxx GOOGLE_API_KEYAIzaxxxxxxxxxxxxxxxxPython 端用python-dotenv加载import os from dotenv import load_dotenv load_dotenv() openai_key os.getenv(OPENAI_API_KEY) google_key os.getenv(GOOGLE_API_KEY) if not openai_key and not google_key: raise RuntimeError(至少配置一个 API Key)这里有个经验之谈两个密钥都配上代码里做多后端支持。原因不是“两个模型都要用”而是开发阶段某个服务万一出现临时不可用你可以一键切到另一个不至于整个进度卡死。后面要讲的完整流水线本身就应该设计成“生成后端可配置”而不是硬编码死一个模型。2.2 单张生成的最小可运行代码DALL·E 的最小可运行代码是这么写的import os from openai import OpenAI client OpenAI(api_keyos.getenv(OPENAI_API_KEY)) resp client.images.generate( modeldall-e-3, prompt一只穿红色雨衣的柴犬站在东京街头霓虹灯倒映在湿润的路面上写实摄影风格, size1024x1024, qualityhd, n1, ) image_url resp.data[0].url print(image_url)Gemini 这边略有不同import os import google.generativeai as genai genai.configure(api_keyos.getenv(GOOGLE_API_KEY)) model genai.GenerativeModel(gemini-2.0-flash-exp) resp model.generate_content( 一只穿红色雨衣的柴犬站在东京街头霓虹灯倒映在湿润的路面上写实摄影风格, generation_config{response_modalities: [Image]}, ) # 返回的是内联 base64 image_data resp.candidates[0].content.parts[0].inline_data.data with open(sample.png, wb) as f: f.write(image_data)注意 Gemini 的输出是 base64需要自己写文件DALL·E 默认返回 URL你也可以设置response_formatb64_json让它直接返回 base64避免 URL 过期问题。两条链路拿到手之后建议先把这两份代码跑通再考虑批量化。基础不牢的批处理脚本后面出bug你都分不清是模型问题还是代码问题。2.3 参数详解size、quality、n 与 response_format批量场景下参数选择决定了你的单位成本和质量值得花点时间理解。size参数直接影响计费。DALL·E 3 的 1024x1024、1024x1792、1792x1024 三个档位价格不同宽幅图相对贵一些。Gemini 这边也有尺寸限制不同模型对输出分辨率有上限。我的建议是按最终用途选尺寸不要一律选最大。如果只是做内容配图web端展示1024x1024 足够了如果是做印刷物料或视频封面再看是否需要上高分版本。quality参数是 DALL·E 特有的。标准档生成速度快、成本低高清档细节更好但也更慢。实战里我发现如果是批量生成后还要人为筛选建议先用标准档跑全量筛出值得精修的图再用高清档重生成。这样能用最低成本覆盖最大样本量。当然这个策略不适用于“必须一步到位”的生产场景。n参数在 DALL·E 3 里最多10张Gemini 没有 n 参数但你可以通过并发多个请求来达到类似效果。说实话我一般不用n1的批量模式因为它在单次请求里一次性消耗多个配额一旦某张图出问题整个请求就要重来。相比之下用并发控制一个个生成单张失败只需要重试那一张错误隔离性更好。response_format建议直接设成b64_json。URL 看起来方便但拿去做流水线必须下载等于多一次 HTTP 往返还可能出现 URL 过期。base64 直接入库或写盘一步到位。当然要注意 base64 比二进制膨胀约 33%传参和写文件时稍微注意一下内存占用就行。两个模型都跑通、参数都理清楚之后你的“单张”能力就绪了。这时候再回头看批量需求思路会清晰很多批量不是把单张代码包个 for 循环那么简单它还涉及限速、错误处理、断点续跑这些工程问题。3. 批量化路上真正难啃的骨头限速、并发与重试3.1 速率限制的真实形态与应对策略很多人第一次跑批量脚本都栽在同一个地方前 20 张生成得好好的第 21 张突然报 429。原因很简单你触发了服务端的速率限制。这里的限制通常有两个维度每分钟请求数RPM和每分钟图片数IPM。DALL·E 的 RPM 限制在不同账号等级下差异很大免费档和付费档差距可以到数倍。你可以在服务端文档里查到具体数值但更好的做法是代码里不写死限速参数而是做成可配置项。这样账号升级或者换服务商时不用改代码逻辑。# config.py RATE_LIMIT { rpm: 60, # 每分钟最多请求数 ipm: 120, # 每分钟最多生成图片数 max_retries: 6, base_delay: 2.0, }有一个非常容易踩的坑RPM 限制看着够用但 IPM 先爆了。比如你每分钟发 60 个请求每个请求生成 3 张图图片数就是 180直接超了 IPM 限制。所以批量脚本里要同时跟踪两个维度的消耗。我自己比较推荐的是令牌桶思路维护一个计数器每次请求前根据已用请求数和已用图片数做双重判断超了任意一个就等待当前周期结束。实现不复杂但能把 429 的出现概率大幅降低。3.2 线程池与信号量实现可调控的并发Python 里做并发最高效的方式是用concurrent.futures.ThreadPoolExecutor然后通过信号量控制最大并发数。import threading import time from concurrent.futures import ThreadPoolExecutor, as_completed semaphore threading.Semaphore(5) # 同一时刻最多 5 个请求在飞 def generate_one(task): with semaphore: # 调用 DALL·E 或 Gemini 的单张生成函数 result generate_image(task[prompt], task[output_path]) return task, result with ThreadPoolExecutor(max_workers10) as executor: futures [executor.submit(generate_one, task) for task in tasks] for future in as_completed(futures): task, result future.result() print(f完成: {task[output_path]})这里有两个细节值得注意。第一max_workers和信号量的大小是两个概念max_workers决定线程池里最多多少线程信号量在函数内部再做一层闸门。你可以开 20 个线程同时只放行 5 个真实请求其余的排队等待。这样即使任务里混入了一些耗时操作比如图片后处理也不会挤占 API 请求的配额。第二不要只靠线程池控制并发因为线程池只限制线程数不感知 API 配额。两个脚本实例同时跑的时候线程池的限流形同虚设。把限速参数做成全局配置必要时用 Redis 或文件锁做跨进程限流。单机小批量可以忽略但如果你计划定时任务多进程跑迟早会遇到这个问题。3.3 退避重试与可观测性设计429 和 5xx 的错误处理是批量脚本的核心。不处理重试的批量脚本本质上是一次性赌运气。推荐指数退避策略第一次重试等 2 秒第二次等 4 秒第三次等 8 秒加上随机抖动防止所有任务同时重试导致又撞在一起。import random import time def generate_with_retry(task, max_retries6, base_delay2.0): for attempt in range(max_retries): try: return generate_image(task) except RateLimitError as e: delay base_delay * (2 ** attempt) random.uniform(0, 1) print(f请求被限流{delay:.1f}s 后重试 ({attempt1}/{max_retries})) time.sleep(delay) except APIStatusError as e: if e.status_code 500: delay base_delay * (2 ** attempt) random.uniform(0, 1) print(f服务端错误 {e.status_code}{delay:.1f}s 后重试) time.sleep(delay) else: raise # 4xx 错误一般不重试直接抛出去人工处理 raise RuntimeError(f任务重试 {max_retries} 次仍然失败: {task[prompt][:50]})除了重试可观测性同样重要。批量任务跑起来后你需要知道跑了多少成功多少失败多少失败的原因分布是什么当前处于什么阶段我习惯在每个任务的处理函数里记录状态到 SQLite 或 JSONL 文件格式大概是{task_id: t001, prompt: xxx, status: success, cost: 0.04, duration: 3.2, error: null}跑完一批之后直接按状态聚合成功率一目了然。这样做的成本很低但能帮你省下大量的排障时间。批量任务最怕的不是失败而是失败了不知道、不知道失败了多少、不知道失败的原因。4. 提示词工程让批量生成的风格保持稳定4.1 模板化提示词与变量注入批量生成和单张生成最大的区别在于你需要让几十上百张图片保持统一的风格基线而不是每张图自由发挥。做法就是模板化提示词。假设你要做一套 30 张的“城市夜景插画”系列提示词模板大概是这样的PROMPT_TEMPLATE ( 一幅城市夜景插画主题是{city}的{landmark} 画面以深蓝色和金色为主色调笔触细腻 氛围宁静且带有一点科幻感适合作为竖版手机壁纸尺寸 9:16。 ) cities [ {city: 上海, landmark: 外滩建筑群}, {city: 东京, landmark: 涩谷十字路口}, {city: 巴黎, landmark: 埃菲尔铁塔}, # ... 更多 ]每个城市只替换{city}和{landmark}两个变量其余所有描述词固定不变。这样一来不同图片之间天然具备了风格一致性你后续筛选也只需要关注主题差异不用费劲比对风格漂移。模板化还有一个隐藏的好处方便批量修改。如果客户看完前 5 张说“色调太深了要明亮一点”你只需要改模板里关于色调的描述然后重新生成全量耗时和成本都可控。这才是批量的工程化价值。4.2 负面提示词与风格锚点DALL·E 没有专门的负面提示词参数但 Gemini 的部分接口支持system_instruction那里可以做风格约束。更通用的做法是把“不要什么”直接写进提示词里。我自己常用的表述方式“不要文字、不要水印、不要边框”“不要歪斜的建筑透视”“不要过度锐化”“保持自然光影不要 HDR 效果”写提示词时建议把“风格锚点”放在开头把“负面约束”放在中段把“技术参数”尺寸、构图、光线放在末尾。顺序对生成结果有影响模型通常更关注靠前的描述。说到风格锚点我强烈推荐给批量任务建一个“风格词汇表”风格方向推荐锚点词汇写实摄影85mm 镜头、f/1.8 光圈、自然肤色、浅景深赛博朋克霓虹灯、雨夜、全息投影、高对比、冷色调青色与洋红国风插画水墨质感、留白、淡彩、轮廓线柔和卡通 3D皮克斯风格渲染、柔软材质、圆润造型、柔和全局光照极简海报大量留白、单一主色、几何构图、无衬线字体的感觉一旦确定风格所有提示词统一挂上锚点词汇不要每次凭感觉增删。4.3 批量场景下的提示词版本管理这个细节很多人会忽略但实际上是批量项目中最大的隐性成本之一提示词改动了批量结果的一致性立刻被打破。你昨天用模板 A 跑出 100 张今天觉得某个形容词不够好随手改成模板 B 又跑了 100 张这两批图混在一起就会出现明显的风格断层。解决思路其实很简单给提示词模板加上版本号PROMPT_TEMPLATES { v1: ( 一幅城市夜景插画主题是{city}的{landmark} 画面以深蓝色和金色为主色调... ), v2: ( 一幅城市夜景插画主题是{city}的{landmark} 画面以深蓝色和金色为主色调增加了月光反射效果... ), }然后在任务清单里记录用的模板版本{task_id: t001, prompt_version: v2, prompt: ...}以后你看到任何一张图都能追溯到它是由哪个版本的提示词生成的。如果某个版本的结果整批被弃用你也知道应该重建哪些任务。这本质上就是数据管理思维图像生成项目跑到后期瓶颈往往不在“模型能力”而在“管理和追溯能力”。5. 从脚本到流水线任务队列、断点续跑与产物管理5.1 任务清单与状态管理批量脚本跑一次容易但真实生产里你会遇到无数需要断点续跑的场景跑到一半 API 挂了、磁盘满了、提示词需要调整、某几张图超时。如果脚本设计成“从头跑”前面的产出全废时间和成本双重浪费。正确的做法是引入任务状态机。每个任务有四个状态待处理、处理中、成功、失败。状态记录在 JSONL 或 SQLite 里脚本每次启动时读取状态文件跳过已成功的任务只处理待处理和失败的任务。import json from pathlib import Path STATE_FILE Path(tasks_state.json) def load_state(): if STATE_FILE.exists(): with open(STATE_FILE, r, encodingutf-8) as f: return {t[task_id]: t for t in map(json.loads, f)} return {} def save_result(task, status, errorNone): with open(STATE_FILE, a, encodingutf-8) as f: f.write(json.dumps({**task, status: status, error: error}, ensure_asciiFalse) \n)实际可以做得更轻不需要每次成功都写文件可以攒 5~10 条批量 flush 一次减少磁盘 IO。但状态文件一定要有否则重跑全量任务的时间损失不是线性增长而是结合限速的指数级增长。5.2 结果落盘与文件命名规范产物管理是另一个容易被忽视的点。批量生成的图片如果没有规范的命名几天后你打开文件夹看到的是一堆 hash 文件名完全对不上任务。我建议的文件命名规则{series_name}/{task_id}_{version}_{style_tag}_{timestamp}.png例如city_night_series/t001_v2_shanghai_bund_20250114.png这个命名包含了系列名、任务 ID、提示词版本、主题标识和时间戳。配合状态文件里的完整提示词你可以在任何时候从图片反推生成信息。另外图片本身建议同时保留原始 base64 和 final PNG 两个版本。base64 可以存到状态文件里做备份溯源PNG 用于实际展示和使用。两个都留一份不大但恢复成本完全不同。5.3 失败任务的恢复与重新生成失败任务怎么处理我的策略是先尝试自动重试重试耗尽后把任务标记为失败跑完一批后统一人工检查失败原因。常见的失败原因失败错误常见原因处理方式429 限流并发过大或配额不足增加退避时间或降低并发重试400 无效请求提示词包含敏感词或非法字符修改提示词后人工重跑执行超时网络波动或服务端异常单独挑出重试图片内容被拒触发内容审核修改提示词重新生成密钥配额耗尽本月用量超限换密钥或等额度恢复这里要特别提醒内容审核拦截是批量项目里最容易被低估的失败因素。某些提示词单看没问题但批量跑几天后就会撞上审核系统的高敏感模式。这时候别硬怼换个措辞就能解决。比如“穿着性感”改成“穿着时尚”“身材曲线”改成“姿态优雅”表述安全也能让生成更稳定。恢复操作建议写成独立函数def recover_failed_tasks(state_file): failed_tasks [] for record in load_state(): if record[status] failed: failed_tasks.append(record) # 重新跑失败的 run_batch(failed_tasks)这样你随时可以在不触碰成功任务的前提下只补跑失败的那几条。对于批量流水线这种“精确惩戒粒度”很重要。整批重跑是最后的选项而不是默认选项。6. 实测数据与踩坑总结6.1 一次批量生产的实测数据拿一个实际模拟项目举例生成 120 张不同城市夜景插画DALL·E 31024x1024 标准质量并发 5单价约 0.04 美元一张。实测数据大概是这样的指标数值总耗时约 34 分钟成功116 张失败4 张失败原因3 张触发内容审核1 张超时总费用约 4.8 美元平均单张耗时约 17 秒429 触发次数2 次均已自动恢复失败率 3.3%在可接受范围内。但如果失败率超过 10%说明提示词模板或参数配置有问题不建议直接加并发硬跑先停下来分析失败模式。盲目增加重试次数和并发数只会让成本更快消耗。另外我注意到一个规律内容审核失败往往集中在某些特定主题上。比如涉及“夜景人像”的任务失败率远高于“纯建筑夜景”。如果项目允许尽量在模板里避开高危主题组合。6.2 我踩过的几个坑和解决方案第一个坑直接使用默认 network 超时。OpenAI SDK 的默认超时配置在很多网络环境下不够用批量任务里单张请求耗时超过 30 秒并不罕见。建议手动设置 timeout否则会出现客户端超时但服务端还在处理的状况任务被标记失败但实际已经生成重试会造成重复扣费。from openai import OpenAI client OpenAI( api_keyos.getenv(OPENAI_API_KEY), timeout60.0, # 给足时间不要用默认值 )第二个坑把 URL 下载和生成混在同一个事务里。用 URL 模式时生成成功并不代表下载成功。稳妥的做法是生成时直接用b64_json把下载环节彻底省掉。第三个坑任务清单里的提示词没有做长度校验。某些模型对提示词长度有限制超长提示词会在中间被截断导致生成结果偏离预期。批量脚本里统一加一个截断或预检逻辑长度超限直接报错避免稀里糊涂跑几百张废图。第四个坑也是最容易忽略的本地磁盘空间。1000 张 1024x1024 的 PNG 大约占用 1.5~2GB如果还保留 JSONL 状态文件内存和磁盘都要提前规划。跑之前检查一次磁盘剩余空间不费什么事但能避免跑到一半磁盘写满的尴尬。6.3 成本优化与质量抽检建议最后说说成本和质量管理。批量场景下成本控制和质量保障是两条线成本控制方面最有效的策略是“先小批量验证再全量铺开”。先拿 5~10 条典型任务跑一遍人工看结果质量确认提示词没问题后再放全量。这个策略下哪怕是全量失败浪费的也只有 10 张图的钱。另外可以监控每次运行的总费用设置一个月度预算阈值脚本跑之前先判断预算是否足够。质量抽检方面我建议每一批任务产出后至少随机抽 10% 的图片做人工检查。检查维度包括是否与提示词匹配、风格是否一致、有没有明显畸变或瑕疵。抽检结果如果合格率低于 80%不要继续跑下一批先回头调提示词。关于成本还有一个容易被忽略的点模型版本升级后的价格差异。同一家服务商的不同模型版本可能有不同价格代码里硬编码的版本号会在不知情的情况下悄悄增加成本。建议在配置中心统一管理模型版本每次升级先跑小批量对比效果和成本。至于 DALL·E 和 Gemini 的日常分工我的实测体会是DALL·E 更适合“描述复杂、需要精确还原”的任务Gemini 更适合“风格感强、需要氛围渲染”的插画类任务。如果一个项目两者都涉及建议分成两条流水线分支各跑各的而不是在同一个循环里无缝切换。理由很简单两者的限速、错误类型、出图风格差异都不同混在一个循环里会让问题定位变得非常困难。最后想补充一点私人体会批量生图这类项目技术层面其实没有什么神秘的API 调用、并发、重试都是常规工程手段。真正拉开差距的是对任务的管理方式——你有没有能力在 500 张图里快速定位到那一张异常图能不能在三天后准确复现某批图的生成条件这些数据管理和工程化的细节往往比模型本身的选型更决定项目成败。我自己在跑过几个批量项目后的经验是把前面说的任务状态机、提示词版本、产物命名规范都搭好之后生成图片本身的代码几乎不用改项目推进的速度主要取决于你筛图、改词、复跑的反馈循环够不够快。这套基础设施值得在你第一次跑批量前就搭好否则越到后面越难补。