Nano Banana 图片 API 失败时,先保存原始证据,再决定是否重试。最关键的不是产品名,而是信号来自哪一层:400/429/503 是请求级 HTTP 结果,NO_IMAGE 是当前 Google API 参考中的生成终止原因,timeout 则可能只是客户端或网关先停止等待。这三类信号不能放进同一个 retry 分支。
| 你看到的现象 | 先保留什么证据 | 第一个动作 | 原样重试? | 修复成功的证据 |
|---|---|---|---|---|
400 INVALID_ARGUMENT | URL、API version、model、脱敏 body、错误详情 | 对照当前 API 参考修正字段与参数 | 否 | 同一最小请求不再返回 400 |
429 RESOURCE_EXHAUSTED | status、details、quota 指标、attempt、时间戳 | 查实际限制并削峰 | 条件性、有界 | 流量平滑后请求成功且错误率回落 |
503 UNAVAILABLE | request ID、model、route、开始/结束时间 | 查服务状态并退避 | 条件性、有界 | 同一路径的最小请求恢复 |
finishReason=NO_IMAGE | 完整 candidates、finishMessage、parts、response ID | 核对输出模态与最小请求 | 先分类再决定 | 新响应出现可解码的 image part |
timeout / 504 | 每层 timeout、总耗时、是否收到 body、request ID | 找到最先关闭连接的一层,并查未知结果 | 只对暂态分支 | 同一路径完成且没有重复产物 |
Google 的 Gemini API 故障排查文档建议只对 408、429 和 5xx 等暂态错误做带上限的指数退避,不要盲目重试 400/403。当前 Gemini API 错误参考把 no_image 列为生成错误;GenerateContent 参考同时把 NO_IMAGE 定义为 FinishReason,含义是预期生成图片但没有生成。它是官方生成结果,但仍然不是 HTTP 状态,也不能单凭这一项推断安全过滤、服务过载或计费结果。
先保存原始响应,别让适配器抹掉诊断信号
排障最容易失败的地方不是代码,而是证据在第一层异常处理里被压成一句“生成失败”。至少记录这些字段,并对提示词、图片与密钥做脱敏:
texttimestamp_utc, provider, base_url, api_version, endpoint, model http_status, canonical_status, request_id, upstream_request_id attempt, started_at, latency_ms, client_timeout_ms candidate_count, prompt_block_reason, finish_reason part_mime_types, has_text_part, has_image_part, error_body_summary
同时保存“实际调用的 route”,不要只写产品名。Gemini Developer API、Vertex AI、第三方网关和你自己的反向代理可能给相似症状赋予不同语义。本文的 HTTP 代码解释以 Gemini Developer API 为主;如果你明确使用 Vertex AI,还要按其 project、region 和 quota 语境单独判断。
下面是一个不固定模型名的最小原生请求。它使用 Gemini native 的 models/{model}:generateContent 路径,并明确只请求图片。把 GEMINI_MODEL 设为你在当前官方模型页确认可用的图片模型,不要从旧教程复制 preview 名称。
bashAPI_VERSION="${GEMINI_API_VERSION:-v1beta}" MODEL="${GEMINI_MODEL:?set GEMINI_MODEL}" curl --fail-with-body --silent --show-error \ -X POST \ "https://generativelanguage.googleapis.com/${API_VERSION}/models/${MODEL}:generateContent" \ -H "x-goog-api-key: ${GEMINI_API_KEY:?set GEMINI_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "contents": [{"parts": [{"text": "生成一张白色背景上的蓝色陶瓷杯产品图"}]}], "generationConfig": {"responseModalities": ["IMAGE"]} }'
这个 probe 的价值不在于“换个提示词试试”,而在于固定路径、账户、model 和输出模态,只把复杂输入换成无害、单目标的图片请求。它成功而原请求失败,说明范围缩到了输入或上下文;它也失败,才继续看账户、容量、路由或平台状态。

400:修请求,不要把语法错误交给退避
400 INVALID_ARGUMENT 表示请求体格式错误、字段缺失、参数不适用于当前 API version,或所选 model 不支持这项能力。重复发送完全相同的请求不会让它自行恢复,还会制造噪声。
按下面的差分顺序缩小问题:
- 记录最终请求 URL,确认没有把 Gemini Developer API 的 native 路径与 Vertex project 路径混在一起。
- 在当前 图片生成文档中确认 model、API version、
responseModalities与图片参数。 - 删除可选字段,只保留一个文字 part 与
IMAGE输出;若最小请求成功,再一次加回一个字段。 - 若包含输入图片,记录 MIME type、编码方式和实际字节大小,避免只检查文件扩展名。
- 保存 Google 返回的完整 error details;不要只把 400 显示给用户。
“参数看起来对”不是成功判据。成功必须是同一个 endpoint 上的最小请求不再返回 400,且响应结构里出现图片 part。
429 与 503:都能退避,但不是同一个根因
429 RESOURCE_EXHAUSTED 表示你超过了某个速率或使用限制。限制可能涉及 RPM、TPM、RPD、支出或当前模型/层级的其他维度;实际值会变化,应在 Google AI Studio 的 rate limits界面查看,而不是依赖文章中的固定数字。
先把 burst 拉平,再重试。多个 worker 同时遇到 429 时,如果都按相同秒数醒来,会形成 retry storm。应用应使用指数退避加 jitter,并限制总尝试次数;批量任务还要把并发与队列长度分开监控。
503 UNAVAILABLE 则表示服务可能过载或暂时中断。它不证明请求有语法问题,也没有一个适用于所有 model 和地区的固定恢复时间。检查官方状态、同账户同 model 的最小 probe 和相邻 model 是否也失败;持续失败时停止自动重试,把 request ID 与时间窗口交给 provider 支持。
下面的实现只对 transport timeout、408、429 和 5xx 进行有界重试。它同时限制单次 timeout、总尝试次数和整个调用的时间预算;400 会立即暴露给调用方。若 SDK 已自动重试,应先确认其策略,避免外层乘法放大尝试次数。
pythonimport random import time import requests TRANSIENT_STATUS = {408, 429, 500, 502, 503, 504} def post_with_bounded_retry( url, headers, payload, *, max_attempts=4, total_budget_s=120 ): deadline = time.monotonic() + total_budget_s for attempt in range(1, max_attempts + 1): remaining = deadline - time.monotonic() if remaining <= 0: raise TimeoutError("retry budget exhausted") try: response = requests.post( url, headers=headers, json=payload, timeout=(10, min(60, remaining)), ) except (requests.Timeout, requests.ConnectionError): if attempt == max_attempts: raise else: if response.status_code == 400: raise ValueError(f"fix request before retry: {response.text[:500]}") if response.status_code not in TRANSIENT_STATUS: response.raise_for_status() return response.json() if attempt == max_attempts: response.raise_for_status() base = min(2 ** (attempt - 1), 30) delay = base + random.uniform(0, base * 0.5) if delay >= deadline - time.monotonic(): raise TimeoutError("no retry fits inside total budget") time.sleep(delay) raise RuntimeError("unreachable")
NO_IMAGE:它是官方生成结果,但不是 HTTP 错误
NO_IMAGE 与“应用自己报没有图片”不能混为一谈。当前 Google 参考中的 NO_IMAGE 位于 candidate 的 finishReason;某些网关还可能把它改写成小写 no_image 错误。先保存上游原始 body,再从外到内检查:
- 没有
candidates:先看promptFeedback,确认提示词是否在产生候选前被拦截。 finishReason=NO_IMAGE:这是“预期出图但未生成”的明确结果。记录finishMessage、model version 与 response ID,用相同 route 跑最小安全请求;不要擅自改写成 safety 或 503。finishReason是IMAGE_SAFETY、IMAGE_PROHIBITED_CONTENT或其他政策原因:按返回原因检查输入或接受边界,不把自动重试当作规避手段。- 有 candidate 和 text part,却没有 image part:确认输出模态,再阅读文字内容;“只有文字”与官方
NO_IMAGE不是同一个证据。 - 有
inlineData/image part,但应用仍显示“没有图片”:这才优先检查解析、MIME、base64 解码、对象存储或网关映射。
一个防御式检查器应把成功图片、官方 NO_IMAGE、政策停止和其他无图候选分开:
pythondef inspect_generate_content(body): prompt_feedback = body.get("promptFeedback") candidates = body.get("candidates") or [] if not candidates: return {"kind": "no_candidates", "promptFeedback": prompt_feedback} candidate = candidates[0] finish_reason = candidate.get("finishReason") parts = ((candidate.get("content") or {}).get("parts") or []) images = [p["inlineData"] for p in parts if p.get("inlineData")] texts = [p["text"] for p in parts if p.get("text")] if images: kind = "image" elif finish_reason == "NO_IMAGE": kind = "no_image" elif finish_reason in { "SAFETY", "IMAGE_SAFETY", "PROHIBITED_CONTENT", "IMAGE_PROHIBITED_CONTENT", "IMAGE_RECITATION", }: kind = "blocked_or_policy" else: kind = "candidate_without_image" return { "kind": kind, "finishReason": finish_reason, "finishMessage": candidate.get("finishMessage"), "imageCount": len(images), "textCount": len(texts), "responseId": body.get("responseId"), "modelVersion": body.get("modelVersion"), }
如果你的故障只涉及安全 finish reason 或纯文字结果,可以继续参考站内的 Gemini 不出图与 IMAGE_SAFETY 专项指南。若同时出现多种 Gemini API HTTP 错误,则用 Gemini API 通用错误排查核对认证与通用路由问题。
超时:找到最先结束请求的一层
“超时”不是一个足够精确的根因。常见调用链是:
textclient/SDK → application server → reverse proxy/API gateway → provider → model
任何一层的 deadline 或 idle timeout 都可能先到。客户端主动取消时,你可能根本拿不到 Google 的 HTTP body;上游明确返回 504 时,才可以把它记录为上游响应。网关生成的 504 也不能冒充 provider 504。反过来,客户端没等到响应也不证明服务端已经停止:若请求可能产生图片或下游写入,重试前先用 request/response ID、任务记录或产物存储查询结果,避免重复生成。
逐层记录开始时间、首字节、结束时间和关闭方。然后做两组同路径对照:一组使用最小单图请求,另一组保留原始复杂输入。若最小请求稳定成功而复杂请求在固定的客户端时限结束,调整正确层的 timeout 或拆小请求;若两者都在 503/504 上失败,继续重发复杂请求没有诊断价值。
不要一开始就把所有 timeout 都调大。更长的 client timeout 可能只会让用户多等,而不会修复上游容量、代理 idle timeout 或死循环。先找到最先关闭连接的一层,再修改那个契约。

何时停止重试,以及交给支持什么
满足以下任一条件时,停止自动重试:400 或其他明确不可重试错误;NO_IMAGE 尚未完成最小请求与响应解析;安全/政策 finish reason;达到应用设定的总尝试或总时长预算;相同 request fingerprint 持续 5xx;或者重试已超过用户请求的有效生命周期。
升级给 provider 或平台团队时,提交一个脱敏包,而不是截图一句“还是不行”:
- UTC 时间范围、provider/base URL、API version、endpoint 与 model;
- HTTP code、canonical status、完整但脱敏的 error details;
- request ID 与 upstream request ID;
- 每次 attempt 的延迟、总耗时与 timeout 配置;
- 最小安全 prompt、是否含输入图片、MIME 与输出模态;
candidate_count、promptFeedback、finishReason、finishMessage、part MIME 列表;responseId、modelVersion,以及 timeout 后是否查到已完成产物;- 同账户、同路径、同 model 的最小 probe 是否成功。
最终验证只有一个:在你真正使用的同一路径上,最小请求返回了可解码的 image part;随后原始工作负载在受控并发下也能完成。只看到 HTTP 200、队列任务变成 completed,或适配器不再显示 NO_IMAGE,都还不足以证明图片链路已经恢复。
