跳转到主要内容

Nano Banana 2 503 模型过载:有界重试、暂停与出图验收

Nano Banana 2 返回 503 时,先确认接口和 HTTP 状态,再在次数与总耗时预算内退避重试;预算耗尽就暂停或排队,收到图片数据并完成解码才算出图成功。

LaoZhang AI Team发布于更新于 16 分钟阅读
文章目录
Nano Banana 2 遇到 503 后有限重试、延期与实际图片验收的主题图。

Nano Banana 2 返回 503、UNAVAILABLE 或“模型过载”时,先保留实际接口、模型 ID 和完整 HTTP 响应,再做有次数上限、总耗时上限和随机抖动的退避重试。预算用完后暂停或排队,不让用户一直转圈。最后必须检查响应里的图片数据,并确认文件能解码、能打开;HTTP 200 或一段文字都不等于出图成功。

当前 Nano Banana 2 模型文档对应的图片模型是 gemini-3.1-flash-image。下文针对 Gemini Developer API 原生 generateContent 的生成和编辑流程。Gemini 应用、Vertex AI、第三方供应商的接口与错误约定需要分别确认,不能直接套用。

先分清:503、429、没图片,还是请求超时

按明确 503、429、其他 HTTP 错误、200 无图和未知超时分流的排查示意。

先查看 HTTP 状态码,再读结构化错误字段。不要用 "503" in str(error) 或搜索“overloaded”来决定重试:错误说明里可能引用别的错误码,代理也可能返回自己的错误页。

实际信号此时能判断什么下一动作
HTTP 503,收到明确错误响应当前请求所经服务暂时不可用;可能是过载或维护在预算内退避重试;保存响应来源
HTTP 429需要区分速率限制、配额耗尽及当前限制维度速率限制可按提示等待;额度耗尽先停止,不进入本页的 503 循环
HTTP 400、402、403、404请求、支付前提、权限或资源存在其他问题按完整错误修正原因,不原样循环重试
HTTP 200,但没有可用图片传输成功,图片任务仍未完成查看候选内容、结束原因、阻止信息与解析逻辑
客户端超时、连接断开,没有确定结果不知道服务是否已经完成生成暂停并核对日志或已有任务结果,不自动重放

Google 的排错文档建议对瞬态错误采用退避、抖动和有限重试。但“200 无图”不能一律归因于安全拦截:输出配置、模型选择、候选结束原因和代码只读取文本,都可能改变判断。先看真实响应,不急着改提示词。

503 也不能单独证明“全球所有用户都受影响”。记录时间、接口域名、API 版本、模型、HTTP 状态、脱敏错误和 request ID(如果有)。第三方入口返回 503 时,要区分其网关错误与 Google 上游响应;影响范围不清楚,可先按 Nano Banana 2 宕机判断指南核对对应入口和状态页。

用当前图片请求恢复生成或编辑

图片请求应保持任务本身:生成时提供文字,编辑时提供文字和原图,输出仍要求图片。不能把 gemini-2.5-flash 等文本模型换上来,然后把返回文字当作恢复成功。当前原生请求与图片响应字段见 Google 图片生成文档。

最小文字生成请求体如下。它使用 contents[].parts 输入与 generationConfig.responseModalities 输出意图,不涉及 Vertex 的项目、地区和 IAM 配置:

json
{
  "contents": [{
    "role": "user",
    "parts": [{"text": "生成一张白色背景上的蓝色陶瓷杯图片。"}]
  }],
  "generationConfig": {"responseModalities": ["TEXT", "IMAGE"]}
}

编辑图片时,在同一条用户消息的 parts 中加入 inlineData,提供真实 MIME 类型和原图的 Base64 数据,并把文字改成具体编辑要求。例如“保留杯子形状,把杯身改成绿色”。原图与编辑要求都要保留到重试中;只把文字送给备用模型并不能完成原来的编辑任务。

可复制的 Node.js 有界重试函数

下面使用支持原生 fetch 的 Node.js,直接调用 REST,不依赖 SDK 的默认重试。示例配置是最多 4 次尝试,包括首次;总预算 60 秒;单次最多 20 秒;完全抖动从 1 秒窗口开始增长,窗口上限 8 秒。这些是演示参数,不是 Google 的容量、平均出图时间或恢复承诺。根据你自己的交付时限调整;长任务若经常超过单次时限,应改为合适的异步流程,而不是反复制造未知结果。

保存为 nano503.mjs。函数只在明确收到 HTTP 503 后再次发送;其他状态返回给对应排错分支。Retry-After 可以是秒数或 HTTP 日期,等待时间不会短于服务端提示,超过总预算就延期。HTTP 503 与 Retry-After 标准允许服务端给出提示,但不保证每次都会返回该头。

javascript
import { performance } from "node:perf_hooks";

export function retryAfterMs(value, wallNow) {
  if (!value) return 0;
  const s = value.trim();
  if (/^\d+$/.test(s)) return Number(s) * 1000;
  const date = Date.parse(s);
  return Number.isFinite(date) ? Math.max(0, date - wallNow) : 0;
}

export function imageParts(body) {
  const images = [];
  const candidates = Array.isArray(body?.candidates) ? body.candidates : [];
  for (const c of candidates) {
    const parts = Array.isArray(c?.content?.parts) ? c.content.parts : [];
    for (const p of parts) {
      if (p?.thought) continue;
      const d = p?.inlineData;
      if (typeof d?.data !== "string" || !["image/png", "image/jpeg", "image/webp"].includes(d.mimeType)) continue;
      if (!/^[A-Za-z0-9+/]+={0,2}$/.test(d.data)) continue;
      const bytes = Buffer.from(d.data, "base64");
      if (bytes.toString("base64").replace(/=+$/, "") !== d.data.replace(/=+$/, "")) continue;
      const png = bytes.subarray(0, 8).equals(Buffer.from("89504e470d0a1a0a", "hex"));
      const jpg = bytes.length > 4 && bytes[0] === 255 && bytes[1] === 216 &&
        bytes[2] === 255 && bytes.at(-2) === 255 && bytes.at(-1) === 217;
      const webp = bytes.length > 12 && bytes.toString("ascii", 0, 4) === "RIFF" &&
        bytes.toString("ascii", 8, 12) === "WEBP";
      if ((d.mimeType === "image/png" && png) || (d.mimeType === "image/jpeg" && jpg) ||
          (d.mimeType === "image/webp" && webp)) images.push({ mime: d.mimeType, bytes });
    }
  }
  return images;
}

export async function postOnce({ url, apiKey, payload, timeoutMs }) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);
  try {
    const r = await fetch(url, {
      method: "POST", signal: controller.signal,
      headers: { "Content-Type": "application/json", "x-goog-api-key": apiKey },
      body: JSON.stringify(payload),
    });
    return { status: r.status, retryAfter: r.headers.get("retry-after"), text: await r.text() };
  } finally {
    clearTimeout(timer);
  }
}

export async function generateBounded({ apiKey, payload, maxAttempts = 4,
  budgetMs = 60000, perCallMs = 20000, send = postOnce,
  now = () => performance.now(), wallNow = () => Date.now(),
  sleep = ms => new Promise(resolve => setTimeout(resolve, ms)), random = Math.random }) {
  if (!Number.isInteger(maxAttempts) || maxAttempts < 1 ||
      !Number.isFinite(budgetMs) || budgetMs <= 0 || !Number.isFinite(perCallMs) || perCallMs <= 0) {
    throw new Error("invalid retry budget");
  }
  const start = now(), trace = [];
  const done = (state, extra = {}) => ({ state, elapsedMs: now() - start, trace, ...extra });
  const url = "https://generativelanguage.googleapis.com/v1/models/gemini-3.1-flash-image:generateContent";
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    const remaining = budgetMs - (now() - start);
    if (remaining <= 0) return done("deferred", { reason: "elapsed_budget" });
    let r;
    try {
      r = await send({ url, apiKey, payload, timeoutMs: Math.min(perCallMs, remaining) });
    } catch {
      // 请求可能已经执行:未知传输错误不自动重放 POST。
      return done("paused", { reason: "transport_result_unknown", attempt });
    }
    trace.push({ attempt, http: r.status });
    let body;
    try { body = JSON.parse(r.text); } catch { body = null; }
    if (r.status >= 200 && r.status < 300) {
      const images = imageParts(body);
      return images.length ? done("image_received", { images }) :
        done("stopped", { reason: "no_usable_image", body });
    }
    if (r.status !== 503) return done("stopped", { reason: "non_503", http: r.status, body });
    if (attempt === maxAttempts) return done("deferred", { reason: "attempt_budget", body });
    const jitter = random() * Math.min(8000, 1000 * 2 ** (attempt - 1));
    const delay = Math.max(jitter, retryAfterMs(r.retryAfter, wallNow()));
    if (delay >= budgetMs - (now() - start)) {
      return done("deferred", { reason: "retry_after_or_elapsed_budget", waitMs: delay });
    }
    await sleep(delay);
  }
}

下面的调用函数完成输入读取、生成或编辑请求,以及图片文件保存。保存为同目录的 example.mjs;将 createOrEdit 接入已有应用,apiKey 由你现有的安全凭证管理提供。未传 inputPath 时生成新图;传入本地 JPG 路径时编辑原图。其他输入格式要同步修改 MIME 类型,不能只改文件扩展名。

javascript
import { readFile, writeFile } from "node:fs/promises";
import { generateBounded } from "./nano503.mjs";

export async function createOrEdit(apiKey, inputPath = null) {
  const parts = [{ text: inputPath
    ? "保留杯子形状,把杯身改成绿色。"
    : "生成一张白色背景上的蓝色陶瓷杯图片。" }];
  if (inputPath) {
    parts.push({ inlineData: {
      mimeType: "image/jpeg",
      data: (await readFile(inputPath)).toString("base64"),
    } });
  }
  const result = await generateBounded({ apiKey, payload: {
    contents: [{ role: "user", parts }],
    generationConfig: { responseModalities: ["TEXT", "IMAGE"] },
  } });
  if (result.state !== "image_received") return result;
  const ext = { "image/png": "png", "image/jpeg": "jpg", "image/webp": "webp" };
  const paths = [];
  for (const [i, image] of result.images.entries()) {
    const path = `output-${i + 1}.${ext[image.mime]}`;
    await writeFile(path, image.bytes);
    paths.push(path);
  }
  return { ...result, paths };
}

这份代码已经通过假传输和假时钟的离线检查,覆盖 503 后恢复、非 503 停止、次数耗尽、请求耗时挤占预算、秒数/日期形式的 Retry-After、未知超时暂停,以及无图片和错误图片数据。没有调用真实 Gemini API,也没有测量当前成功率或延迟。

别让应用、SDK、网关同时重试

选一个地方负责重试,并确认其他层不会偷偷增加尝试数。例如应用重试 4 次,每次 SDK 再尝试 5 次,就可能变成 20 次上游调用,还没有计入网关。

Google 排错文档描述 Python SDK 自动重试 4 次;而本次查看的 python-genai main 实现在 retry_options=None 时只尝试一次。源码分支不是你已安装版本的保证,因此不要凭默认值估算预算。若采用当前 google-genai、from google import genai 且由应用负责重试,可显式使用 types.HttpOptions(retry_options=types.HttpRetryOptions(attempts=1), timeout=20000),并确认安装版本支持这些字段。这里 attempts=1 表示只做首次调用;timeout=20000 的单位是毫秒,即 20 秒,字段定义见 SDK types.py。

Python 原生 SDK 异常应按 errors.APIError.code == 503 判断,code 是整数,见 APIError 定义。另一个 Interactions 错误参考使用字符串错误码,不能把其中的 service_unavailable 当作原生 generateContent SDK 的字段约定。

怎样确认真的出图,预算用完又怎样处理

上面的 image_received 只表示取到了图片候选字节,完成了 Base64、MIME 类型与文件头的初步检查。它还不是最终交付状态:用应用现有图片解码器完整解码,检查宽高,再打开文件确认内容符合生成或编辑要求。文件头正确也可能是截断文件;要求编辑原图时,还需检查所要求的修改是否实际发生。

验收顺序是:

  1. 遍历 candidates[].content.parts[],找到非思考部分的 inlineData 图片,而不是只取第一段文本。
  2. 确认 MIME 类型、Base64 和非空字节匹配。使用 SDK 时注意其字段可能是 inline_data,不要把 REST 与 SDK 对象混用。
  3. 保存并完整解码,确认尺寸和可读性;然后检查输出内容。到这里才能将业务任务标为完成。
  4. 没有图片时记录实际候选结束原因、阻止信息与响应摘要;不要把空输出直接返回成成功,也不要自动再跑整个 503 循环。

其他返回状态需要有明确去向:

函数状态用户界面与后台动作
stopped给出与错误分支对应的处理结果;不自动重新提交相同任务
paused标为“结果待核对”,保留本地任务 ID 和日志;先查是否已有输出,再决定是否补发
deferred告知暂未完成;能接受延迟就保存到队列,否则结束本次交互
image_received进入完整解码和内容验收,通过后交付

队列里保存同一个业务任务 ID、原始请求的受保护引用、当前状态、累计尝试数、任务到期时间和下次可尝试时间。一个任务同时只允许一个工作进程持有执行租约;用户连续点击也应复用待处理任务,不创建一串副本。本地去重是你的业务控制,不代表 Google 会按这个 ID 对生成 POST 去重。

同步预算耗尽后,不要让后台自动获得无限新预算。后台仍需限制总尝试数和任务寿命,到期就结束或交给用户重新选择。若 Retry-After 超过当前交互预算,按提示延期;不能为了赶上 60 秒窗口,把服务端要求的 120 秒压成 8 秒后立即发送。

超时尤其要谨慎:中止客户端等待不等于撤销服务端生成。HTTP 标准对非幂等请求的自动重试要求额外依据;这里没有假设图片 POST 具备去重保证。确认结果之前盲目重放,可能产生重复图片与额外工作。

降低并发,恢复时只放一个半开探针

断路器关闭、打开与半开状态之间的转换示意图

沿用旧版断路器示意图;实际阈值、等待时间与探针许可按下面的流程设计。

如果很多任务同时遇到 503,先暂停新任务进入执行区,降低同一入口、同一模型的活动请求数,让其余任务排队。不要给每个任务都增加重试次数,这会放大瞬时压力。并发限制应覆盖正在请求、等待退避后准备再次请求的同一任务,而不只是某一次 HTTP 调用。

持续失败时可以加断路器,但旧式“进入半开后所有请求都能执行”的写法会在恢复窗口重新制造拥堵。可按下面的执行顺序实现:

  • 正常状态:在并发上限内执行,记录实际成功图片与瞬态失败。
  • 失败达到你定义的阈值:进入打开状态,把新任务留在队列,记录下次检查时间。阈值和冷却时长应依据自己的日志与交付要求设置。
  • 冷却结束:用锁或原子比较交换领取唯一的半开许可。其他工作进程没有许可就继续等待;分布式服务要使用共享许可,不能各实例放一个探针。
  • 探针只用一个允许执行的实际业务任务,最多一次请求,不套多次重试。收到并验收图片后,再逐步增加并发;明确 503 则重新打开,未知超时则保留待核对状态并继续限制流量。

已有在途请求的迟到结果不能把新的打开状态随意改回正常:给状态转换增加代数标记,只允许持有当前半开许可的任务完成该次转换。这个控制能减少自身流量造成的冲击,但不能保证 Google 在某个固定分钟数内恢复。也不需要用定时付费生图来“刷健康状态”。

什么时候排队、换图片模型或用 Batch

选择取决于交付时限与图片任务要求。能等待的任务优先保留原模型和原始输入排队;必须尽快交付的任务,才考虑已经接入、允许使用并且满足要求的备用图片模型。

备用模型应提前核对四件事:是否接受原图进行编辑、输出尺寸或比例能否满足要求、数据处理约定是否适用、总预算是否容纳再次生成。切换后照样验收图片,不把文本解释当成替代结果。没有经过这类核对的备用路线,就明确暂停或排队,不能临时换协议、换到 Vertex IAM 流程,或承诺另一个模型一定更稳定。

Nano Banana 2 的模型文档列出支持 Batch。Gemini Batch API适合非紧急 generateContent 任务,目标完成时间为 24 小时,但这是处理目标,不是交付 SLA,也不是“没有任何 503 风险”的保证。图片任务仍可能失败、取消或过期。

已有 Batch 作业时保存并查询同一个作业名,不因暂时没有结果就反复创建整个批次。作业成功后,也要逐项读取错误和图片数据;作业成功不等于每条图片请求成功。只重新处理确认失败且仍需要交付的条目,避免重复生成已完成图片。

常见问题

Nano Banana 2 的 503 失败请求一定不收费吗?

不能这样保证。Google 计费 FAQ明确提到失败的 400 或 500 请求不计 token 费用,但仍计入配额;该说明没有明确覆盖所有 503、第三方封装退款或未知网络超时。核对你实际入口的用量、账单与请求记录。重试、备用模型的新尝试以及超时后已经完成的生成,都不能预先视为免费。

升级付费层级或轮换 API key 能解决过载吗?

付费或配额变更不能保证修复 503。若实际错误是 429,应先确认当前受限维度,再按对应规则处理;不能用“升级一定解决”代替诊断。503 分支先减压、有限退避和延期,不通过轮换 key 或项目规避限制。

应该等多久再试,有统一的低峰时间吗?

本页没有当前成功率、统一低峰时间或固定恢复时长的实测依据。有 Retry-After 时尊重提示;没有时按自己的交付预算退避,超预算就延期。安排后台任务时,可利用自己同入口、同模型、相同请求形态的日志观察趋势,不能把旧社区比例当作今天的调度承诺。

503 消失、只返回文字,算恢复了吗?

对图片任务而言还不算。确认请求仍使用图片模型和图片输出配置,检查候选部分是否有可用图片数据,再完成解码和内容验收。若只回文字,应进入无图片分支检查真实响应,而不是宣布服务已恢复或继续盲目重试。

官方文档

接入参数、价格与扣费规则以老张API开发文档为准:图片生成计费与失败处理。

说明:laozhang.ai 是我们自己的服务,其价格是平台报价,不是独立比较的结论。

参考来源10

本文引用的外部页面,按正文出现顺序排列。最后更新于 2026年10月7日。

  1. 1.Nano Banana 2 模型文档ai.google.dev/gemini-api/docs/models/gemini-3.1-flash-image
  2. 2.Google 的排错文档ai.google.dev/gemini-api/docs/troubleshooting
  3. 3.Google 图片生成文档ai.google.dev/gemini-api/docs/generate-content/image-generation
  4. 4.HTTP 503 与 Retry-After 标准rfc-editor.org/rfc/rfc9110.html
  5. 5.python-genai main 实现github.com/googleapis/python-genai/blob/main/google/genai/_api_client.py
  6. 6.SDK types.pygithub.com/googleapis/python-genai/blob/main/google/genai/types.py
  7. 7.APIError 定义github.com/googleapis/python-genai/blob/main/google/genai/errors.py
  8. 8.Interactions 错误参考ai.google.dev/gemini-api/docs/api-errors
  9. 9.Gemini Batch APIai.google.dev/gemini-api/docs/batch-api
  10. 10.Google 计费 FAQai.google.dev/gemini-api/docs/billing
Nano Banana 2 图片体验入口与应用、开发者服务的区别示意
AI 图像生成

Nano Banana 2 免费试用:入口、出图步骤与当前额度

不想先订阅,可以从 Gemini 的免费图片入口开始,完成生成、按需修改和下载。不过免费账号已默认使用 Auto,出图成功不代表一定调用了 Nano Banana 2;指定模型的第三方试用和 API 另有条件。

16 分钟