那种“标题看着简单真上手一堆暗坑”的项目最近刚好把某套支持图像生成与编辑的 API 完整接入了一个 Node.js 后台服务整个流程跑通之后回头整理这份教程。这套接口在 2.5 版本里同时支持从文本描述直接生成图片、基于已有图片做二次编辑还允许用自然语言描述修改内容比如“把背景换成傍晚的橙色天空”这种指令接口会直接对原图做语义理解再输出结果。如果你正在做内容生产工具、设计辅助平台或者想给博客、小程序加一个“AI 配图”功能这份实操笔记可以直接照着抄每一步我都会说明参数为什么那样写、响应为什么要那样解析而不是只贴一段能跑的代码。我尽量把代码、参数、报错场景都拆开讲适合刚接触这类接口的开发者也适合已经调过基础版本、想升级到 2.5 能力的同学。1. 项目背景与能力边界先把这套 API 到底能做什么说清楚。2.5 版本的能力可以大致分成三块。1.1 文本生成图片给一句描述性的 prompt比如“一只戴牛仔帽的柴犬坐在复古皮卡后座窗外是荒漠夕阳”接口返回一张符合描述的图片。这个能力最直接的使用场景就是配图、封面、海报底图、素材占位。和早期版本相比2.5 对长文本描述的还原度明显更好尤其是包含多个物体、相对位置关系、光线方向的描述不再容易出现“元素堆在一起”的僵硬感。1.2 图片编辑与局部重绘输入一张原图、一段修改指令接口返回修改后的图片。这里要特别注意它并不是简单的滤镜叠加也不是画板式的涂改而是理解原图内容后再做生成。举个例子你给一张室内装修实拍照说“把沙发换成墨绿色天鹅绒材质保持其他家具不变”输出结果会在保留房间结构、其他物品的前提下更换沙发材质。这个能力是所有编辑功能里使用频率最高的适合电商产品图替换背景、设计稿换配色、摄影作品调整元素。1.3 图片理解与指令式修改同样支持“把图片里人物的笑容调整得更自然”“去掉背景里路过的人”“把光线改成清晨的柔光”这类需要先理解画面内容、再做局部修改的指令。本质上这依赖多模态理解能力接口内部会先分析画面中都有什么对象、它们的位置和关系再根据指令生成新的画面。从实际开发角度这三个能力对应三种不同的接口调用方式但请求结构高度相似无非是传的参数不同。所以下面我统一按“生成”和“编辑”两条主线来讲。2. 环境准备与 API 接入前置工作这部分主要解决三件事拿到调用凭证、装好 HTTP 请求库、确定输出格式。2.1 获取 API 凭证与安全策略无论你用的是哪家服务商的接口第一步永远是搞到 key。通常在服务商控制台创建一个应用得到一串 API Key有些平台还区分 Secret Key 和 Access Key 两个字段签名方式也略有不同。这一步没什么技术含量但有几个安全工作必须提前做key 绝对不要写死在代码仓库里。哪怕项目是私有的也不建议。我用的是dotenv加载.env文件并把.env加进.gitignore。服务端调用时把 key 放在后端不要在前端代码里暴露。因为接口费用是跟 key 走的一旦前端暴露 key等于把钱包敞开给所有人。如果服务商支持子 Key 或者 IP 白名单建议按环境分别配置测试环境一个 key生产环境一个 key出问题时方便按 key 排查。2.2 安装依赖与初始化项目既然标题明确使用 Node.js我就以 Node.js 环境为例。初始化一个项目并安装依赖mkdir image-api-demo cd image-api-demo npm init -y npm install dotenv # 如果习惯用官方 SDK 就装 SDK如果没有官方 SDK 或不想受限制直接装 axios 或 fetch npm install axiosNode.js 18 及以上版本自带全局 fetch如果不想引入 axios直接用内置 fetch 也完全没问题。不过我在实际项目里倾向用 axios因为它的超时配置、错误处理、请求拦截器比原生 fetch 顺手尤其当你要对多个接口做统一鉴权时axios 拦截器能省不少重复代码。2.3 理解请求与响应结构不管具体接口路径长什么样这类生成式图片 API 的请求结构基本是{ model: image-gen-2.5, prompt: 一只戴牛仔帽的柴犬坐在复古皮卡后座, n: 1, size: 1024x1024, response_format: b64_json }响应通常是{ created: 1731234567, data: [ { b64_json: 这里是base64编码的图片数据 } ] }如果你设置了response_format: url响应里的data会是一个图片临时链接有效期一般只有几分钟到一小时不等。这个细节很关键我在 4.3 节会展开讲为什么推荐直接拿 base64 而不是拿 url。3. 文本生成图片从请求到保存文件这是最简单的一条链路但很多新手会在“图片怎么落盘”这一步卡住。我不光写怎么调用还把参数选择的逻辑讲清楚。3.1 文生图核心代码import dotenv from dotenv; import axios from axios; import fs from node:fs/promises; dotenv.config(); const API_KEY process.env.IMAGE_API_KEY; const BASE_URL process.env.IMAGE_API_BASE_URL; async function generateImage({ prompt, size 1024x1024, n 1 }) { const response await axios.post( ${BASE_URL}/images/generations, { model: image-gen-2.5, prompt, n, size, response_format: b64_json, }, { headers: { Authorization: Bearer ${API_KEY}, Content-Type: application/json, }, timeout: 60000, } ); const results response.data.data; const saveTasks results.map(async (item, index) { const buffer Buffer.from(item.b64_json, base64); const filename output_${Date.now()}_${index}.png; await fs.writeFile(filename, buffer); return filename; }); const filenames await Promise.all(saveTasks); return filenames; } generateImage({ prompt: 一只戴牛仔帽的柴犬坐在复古皮卡后座窗外是荒漠夕阳电影感构图, }) .then((filenames) console.log(已保存:, filenames)) .catch((err) console.error(生成失败:, err.message));这段代码的核心只有三部分拼接请求体、发送 POST 请求、把返回的 base64 转成 Buffer 写入文件。Buffer.from(item.b64_json, base64)这一步是把 URL-safe 的 base64 字符串还原为二进制的标准做法。3.2 为什么 prompt 要写这么多细节很多人第一次调用时只写“狗、车、沙漠”这类简单词生成结果往往很普通。这跟接口能力没关系是输入的信息量不够。以我测试多次的经验来看prompt 至少应该包含这几个维度主体对象什么东西数量多少外观特征颜色、材质、穿着、状态环境与背景地点、时间、天气、光线风格参考摄影风格、构图方式、画风、镜头焦段画质要求高清、细节丰富、专业摄影等前面的示例 prompt 就包含了全部五个维度。“戴牛仔帽的柴犬”是主体加外观“复古皮卡后座”是环境“荒漠夕阳”是光线“电影感构图”是风格“专业摄影”这类词虽然笼统但确实会引导模型往高质量方向走。3.3 size 参数怎么选择size 直接影响生成图的宽高比不同服务商支持的分辨率集合不同常见的有1024x1024正方形适合头像、封面、通用素材1024x1792竖版 9:16 比例适合海报、小红书配图、手机壁纸1792x1024横版 16:9 比例适合公众号头图、视频封面、PPT背景512x512小尺寸生成速度快适合快速迭代预览我这里建议默认用1024x1024因为很多模型的细节表现力在这个尺寸下最稳定。等 prompt 调合适了再按最终使用场景换成对应的宽高比不要一开始就用竖版长图去调试 prompt会同时遇到构图和细节两个变量叠加的问题很难定位是描述的问题还是分辨率的问题。# 输出效果不理想时优先排查prompt 是否足够具体size 是否匹配场景 # 再用同一条 prompt 在不同 size 下对比观察差异3.4 一次生成多张图的取舍请求参数里的 n 表示一次返回几张候选图。部分服务商会把 n 上限限制在 1 到 4 之间而且 n 越大整体耗时越长、费用越高。我的习惯是调试阶段 n 设为 2 或 3挑一张满意的正式生产环境 n 设为 1 或 2避免浪费配额和等待时间。如果你要做批量生成比如一次生成 20 张不同描述的图片不要在一个请求里塞多个 prompt而是写循环分批请求每批控制在 2 到 3 个并发既能充分利用接口能力又不容易触发热点限制。4. 图片编辑基于原图的三种玩法图片编辑的接口调用跟生成接口很像但请求体里多了一个image字段用来传原图数据。这里要注意的是原图数据的格式以及编辑模式和生成模式的参数差异。4.1 基于原图的对象修改适用场景替换产品颜色、换背景、改人物服装。先把原图读成 base64再放进请求体。import fs from node:fs/promises; async function editImage({ imagePath, prompt, size 1024x1024 }) { const imageBuffer await fs.readFile(imagePath); const imageBase64 imageBuffer.toString(base64); const response await axios.post( ${BASE_URL}/images/edits, { model: image-gen-2.5, image: imageBase64, prompt, size, response_format: b64_json, }, { headers: { Authorization: Bearer ${API_KEY}, Content-Type: application/json, }, timeout: 120000, } ); const result response.data.data[0]; const outputBuffer Buffer.from(result.b64_json, base64); const filename edited_${Date.now()}.png; await fs.writeFile(filename, outputBuffer); return filename; } editImage({ imagePath: ./input/product.jpg, prompt: 把产品背景替换成干净的浅灰色摄影棚背景产品本身不变, }).then((filename) console.log(已保存:, filename));有几个细节值得展开。第一image字段传的是 base64 字符串不是文件路径。如果你用的是官方 SDK有些 SDK 会直接接受本地文件路径并帮你处理编码但手动请求时必须自己转。第二编辑接口的超时时间要比生成接口设得更长因为模型需要先分析原图内容再执行生成整个链路更耗时。4.2 局部重绘让模型只动某个区域部分接口支持用mask参数指定重绘区域。所谓 mask是一张与原图同尺寸的图片需要重绘的区域用白色标注其他区域用黑色覆盖。这个能力非常适合“只想改画面中的一小部分不破坏整体结构”的场景。const maskBuffer await fs.readFile(./mask.png); const maskBase64 maskBuffer.toString(base64); const response await axios.post( ${BASE_URL}/images/edits, { model: image-gen-2.5, image: imageBase64, mask: maskBase64, prompt: 把白色区域替换成一只橙色花纹猫, size: 1024x1024, response_format: b64_json, }, // headers 同前 );mask 必须由开发者自己准备常用做法是先用程序把原图中想要修改的区域标出来生成 mask。如果你不想手动做 mask可以用不带 mask 的编辑接口靠 prompt 指定要修改的内容。两者的区别在于带 mask模型只会在指定区域生成其他区域完全保持原样可控性更高不带 mask模型会根据 prompt 在整幅图上做调整修改范围可能超出预期。以我测试过的项目为例处理“把照片里的路人挪走”这个需求不带 mask 硬调很容易连带改变天空颜色或建筑细节后来改成先手动圈出路人所在区域生成 mask再用 mask 编辑效果就稳定多了。如果讲究工作流效率建议写一个简单的 Canvas 服务让用户在网页上框选需要重绘的区域后端根据框选坐标生成 mask然后调用接口。4.3 编辑结果返回 URL 还是 Base64这是很多人忽略的一个关键点。请求体里response_format设置为url接口会返回一个临时链接你可以直接把这个链接给前端展示看起来省事。但临时链接有效期通常只有几十分钟而且你在服务器上还要再额外向后端存储服务转发一次增加一道不稳定环节。设置为b64_json后接口直接把图片数据打包在 JSON 里返回虽然单个响应体积会变大一点但免掉了“拉取临时链接”的额外请求也不存在链接过期的问题。我的建议是如果图片需要长期保存一律用b64_json服务器直接落盘或者上传对象存储如果只是临时展示比如返回给前端预览几秒钟url模式也够用但要注意有效期。4.4 编辑时 prompt 的修辞策略编辑用的 prompt 跟生成用的 prompt 不一样重点是描述“差异”而不是描述完整画面。比如原图是一只柯基坐在草地上你想让它变成坐在雪地里两个写法普通写法“柯基坐在雪地里”高效写法“保持柯基的姿势和朝向不变把草地改为覆盖薄雪的冬季地面光线变柔和背景的树加上雪挂”第二种写法效果明显更好的原因是它明确告诉了模型哪些东西要保持不变。如果你只描述目标效果模型可能会把狗的形态、角度、甚至品种一起改了这不是你想要的。所以编辑场景的 prompt尽量带“保持某某不变”这样的限制性描述。5. 工程化落地文件处理、并发与容错到这里接口调用本身已经可以跑通了。但要真正用到生产环境还必须处理文件格式校验、重试机制、错误响应解析、并发控制等工程问题。5.1 图片格式与大小预处理不是所有图片传给编辑接口都能被正确处理。以我实际使用的接口来说支持的输入格式通常是 PNG、JPEG、WEBP并且图片不能太大有些平台要求小于 5MB。如果你接入的业务经常有用户上传大图必须在调用前做压缩处理。Node.js 里我用sharp做图片预处理它很成熟性能也好。npm install sharpimport sharp from sharp; async function preprocessImage(inputPath, outputPath) { await sharp(inputPath) .resize(1024, 1024, { fit: inside }) .jpeg({ quality: 85 }) .toFile(outputPath); }.resize(1024, 1024, { fit: inside })表示在保持宽高比的前提下把图片缩放并限制在 1024x1024 的边框内。为什么限制在这个尺寸因为大多数图像生成模型会把输入图片缩放到固定分辨率再处理如果你原图是 4000 像素宽模型内部照样会先压缩但压缩质量和直接传一张优化过的图是不同的提前在本地压缩能保证细节损失更可控。同时缩到 1024 的图片传输体积会小很多上传更快接口处理也更快。5.2 重试机制接口超时的兜底方案生成式接口的耗时波动很大高峰期可能 10 秒也可能 60 秒。网络抖动、服务端排队都会造成偶发失败。我建议不是简单捕获异常就退出而是写一层重试逻辑。async function callWithRetry(fn, retries 3, delayMs 1000) { for (let attempt 1; attempt retries; attempt) { try { return await fn(); } catch (err) { if (attempt retries) throw err; const isTimeout err.code ECONNABORTED || err.response?.status 500; if (!isTimeout) throw err; console.warn(请求失败第 ${attempt} 次重试: ${err.message}); await new Promise((r) setTimeout(r, delayMs * attempt)); } } }这里的关键判断是只有超时和 5xx 服务端错误才值得重试。如果是 4xx比如prompt被内容审核拦截或者image格式不对那重试多少次都没用反而会消耗配额。我的经验是重试次数设为 3 次就够了再多反而容易造成堆积。每次重试的间隔用指数退避第一次 1 秒第二次 2 秒第三次 4 秒避免服务端还没恢复就频繁重试。5.3 常见错误码速查与排查思路调这类接口错误响应一般会带一个error对象包含code和message。我把自己遇到过的典型错误整理成一个速查表错误场景典型错误信息排查方向Key 无效Invalid authentication检查环境变量是否加载key 是否过期是否有空格超出配额Quota exceeded看控制台用量是否达到调用次数上限请求内容违规Content policy violation修改 prompt 措辞避免敏感或限制级描述图片格式错误Invalid image format确认 base64 内容是否完整格式是否为 jpg/png/webp图片过大Image too large用 sharp 压缩图片或降低分辨率模型不存在Model not found确认 model 参数是否与你开通的服务一致接口限流Rate limit reached降低并发检查是否触达每分钟调用上限尤其要注意内容违规这个错误。它跟代码无关是 prompt 或图片触发服务商的内容审核机制。遇到这种问题不要尝试绕过而是修改描述方式。比如把带有特定品牌 logo 的图片改成“抽象的红色圆形标志”既保持生成需求又不触犯规则。5.4 并发控制与任务队列如果你要接入的是一个批量出图的后台系统并发问题必须提前设计。我最早做的时候图省事直接用Promise.all一次发 10 个请求结果触发了限流好几个请求返回 429。后来改成任务队列方案一次只跑 2 个并发每个请求完成后再从队列里取下一个任务。async function runConcurrent(tasks, limit 2) { const results []; const queue [...tasks]; const workers Array.from({ length: limit }, async () { while (queue.length) { const task queue.shift(); try { results.push(await task()); } catch (err) { results.push({ error: err.message }); } } }); await Promise.all(workers); return results; }队列的长度要根据接口的限流文档来定。有的接口允许每分钟 60 次请求那就把并发控制在 1 比 2 更安全给重试留出余量。不要满打满算地去压上限一旦有重试挤进来立马就会超出限制反而导致连环失败。5.5 图片后处理检查结果是否可用的方法接口返回图片后不要直接信任它。生成式模型偶尔会输出“看起来正常但仔细看有畸变”的图比如手指数不对、文字乱码、画面里出现奇怪的异物。对于半自动化流程我建议至少要做三层检查尺寸检查确保返回图片宽高比符合预期不出现莫名的拉变形文件大小检查生成结果如果只有几 KB大概率是一张纯色或模糊图不可用人工抽检批量流程里随机抽取 5% 的结果人工审核尤其在正式对外发布前如果是做自动化封面生成建议再加一道“文字拼写检测”因为很多模型画文字还是不强经常出现错字。这个可以利用 OCR 服务识别生成图里的文字内容检查有没有大段乱码有就直接重新生成一次。6. 完整示例把图片生成与编辑封装成一个服务前面几节都是拆开的片段这节给一个完整可运行的服务封装示例。实际项目里我会把生成和编辑统一封装成一个模块对外只暴露两个方法调用方不需要关心接口细节。// imageService.js import dotenv from dotenv; import axios from axios; import fs from node:fs/promises; import sharp from sharp; dotenv.config(); const API_KEY process.env.IMAGE_API_KEY; const BASE_URL process.env.IMAGE_API_BASE_URL; const TIMEOUT 120000; const client axios.create({ baseURL: BASE_URL, timeout: TIMEOUT, headers: { Authorization: Bearer ${API_KEY}, Content-Type: application/json, }, }); async function saveBase64Image(b64, prefix output) { const buffer Buffer.from(b64, base64); const filename ${prefix}_${Date.now()}.png; await fs.writeFile(filename, buffer); return filename; } export async function generateImage(prompt, options {}) { const { size 1024x1024, n 1 } options; const response await client.post(/images/generations, { model: image-gen-2.5, prompt, n, size, response_format: b64_json, }); const filenames []; for (const item of response.data.data) { const filename await saveBase64Image(item.b64_json, gen); filenames.push(filename); } return filenames; } export async function editImage(imagePath, prompt, options {}) { const { size 1024x1024 } options; const imageBuffer await fs.readFile(imagePath); let processedBuffer imageBuffer; // 如果图片太大压缩到 1024 以内 if (imageBuffer.length 4 * 1024 * 1024) { processedBuffer await sharp(imageBuffer) .resize(1024, 1024, { fit: inside }) .jpeg({ quality: 85 }) .toBuffer(); } const imageBase64 processedBuffer.toString(base64); const response await client.post(/images/edits, { model: image-gen-2.5, image: imageBase64, prompt, size, response_format: b64_json, }); const filename await saveBase64Image(response.data.data[0].b64_json, edit); return filename; }调用方只需要这样用import { generateImage, editImage } from ./imageService.js; const genFiles await generateImage(夏日海边沙滩上的白色遮阳伞阳光明媚高清摄影); console.log(genFiles); const editFile await editImage(./input/room.jpg, 把沙发的颜色改成墨绿色材质换成天鹅绒); console.log(editFile);这样一个封装调用方注意不到 base64 转换、图片压缩、响应解析这些细节对上层业务来说就是一个纯异步的“生图”和“改图”函数。后面想换别的服务商也只需要改这一个文件上层完全不用动。7. 常见问题与排查技巧实录做这个项目时我踩过不少坑这里挑几个最有代表性的记录一下供大家排查时参考。7.1 base64 图片损坏生成结果打不开表现接口返回成功代码也执行完了但生成的 PNG 文件打不开。排查后发现有的接口会对 base64 做 URL 编码字符串里的会被替换成空格直接拿来转 Buffer 就会损坏。解决办法保证拿到 base64 后先解码decodeURIComponent或者把字符串里的空格替换成。有些服务商还会在 response 里把\n保留在 base64 字符串中需要先去掉所有换行符。const cleanBase64 item.b64_json.replace(/\s/g, ); const buffer Buffer.from(cleanBase64, base64);7.2 背景被意外修改编辑接口不带 mask 时模型对“保持背景不变”的理解是有限的。避免方案有两个一是用 mask 精确控制可修改区域二是在 prompt 里反复强调“背景保持不变”“其他区域完全不变”。我用过多次之后发现后者只是降低概率不能完全杜绝要求高的话还是要做 mask。7.3 反复触发 429 限流触发限流时接口会返回 429 状态码。除了降低并发我建议记录请求时间戳在本地做滑动窗口计数。比如已知限制是每分钟 60 次那就写一个计数器每分钟重置一次在达到 55 次时主动等待几十秒再发送这样比依赖服务端的 429 响应更稳。7.4 n1 时 data 数组为空极少数情况下接口请求成功但data数组是空的。这通常不是网络问题而是内容审核阶段模型认为结果有风险直接把生成结果丢弃了。遇到这种情况调整 prompt 再试一次尤其是把描述改得更中性往往就能正常返回。7.5 长 prompt 反而效果变差很多新手以为 prompt 越长越好其实不是。当 prompt 超过 500 字以后模型对关键信息的抓取会明显变弱甚至会出现细节冲突。我的经验是控制在 100 到 300 字之间把最重要的特征放在前面比如“主体是什么、什么颜色、在什么环境、什么光线下”句首信息对结果的影响权重更高。8. 结束语再分享两个小技巧整套流程跑下来最大的体会是“接口接入只是第一步稳定地产出才是工程重点”。生成式接口天然带不确定性所以在代码架构上一定要把错误处理、重试、输出校验当成一等公民来设计而不是跑通就完事。最后再分享两个小技巧。第一个调试 prompt 时建议用同一句话跑 2 到 3 次观察结果是否存在明显差异因为这类模型本身有随机性一次结果不满意不代表 prompt 不行。第二个如果在做内容平台建议在生成图片后自动拼接一个“AI 生成”的水印文字标记既是合规需要也方便后续追踪图片来源。如果你在实际接入中遇到了上面没写到的奇葩问题欢迎和我交流我这边也还在持续更新对这个接口的踩坑记录。
阅读完成 · 觉得有帮助?