GEMINI LABEN
OUTAGE — Geminiが過去最大級の障害(error 1076/1099)から回復へ。エンジニアリングチームの緩和策で影響は縮小中DAILY-BRIEF — 新エージェント「Daily Brief」が登場。夜間にinbox・カレンダー・タスクを分析し、朝のパーソナルダイジェストを生成GEMINI-OMNI — Geminiと生成メディアモデルを統合した動画AI「Gemini Omni」。プロンプトから一貫性のある高品質動画を生成ENTERPRISE — Gemini Enterpriseで3.5 Flashが6/8からデフォルト固定に。機能管理トグルは廃止され全ユーザーで有効DEPRECATION — 画像previewモデル(3.1-flash-image/3-pro-image)は6/25に停止。GA版への移行はお早めにFILE-SEARCH — File Searchがマルチモーダル検索に対応。gemini-embedding-2で画像をネイティブに埋め込み・検索OUTAGE — Geminiが過去最大級の障害(error 1076/1099)から回復へ。エンジニアリングチームの緩和策で影響は縮小中DAILY-BRIEF — 新エージェント「Daily Brief」が登場。夜間にinbox・カレンダー・タスクを分析し、朝のパーソナルダイジェストを生成GEMINI-OMNI — Geminiと生成メディアモデルを統合した動画AI「Gemini Omni」。プロンプトから一貫性のある高品質動画を生成ENTERPRISE — Gemini Enterpriseで3.5 Flashが6/8からデフォルト固定に。機能管理トグルは廃止され全ユーザーで有効DEPRECATION — 画像previewモデル(3.1-flash-image/3-pro-image)は6/25に停止。GA版への移行はお早めにFILE-SEARCH — File Searchがマルチモーダル検索に対応。gemini-embedding-2で画像をネイティブに埋め込み・検索
記事一覧/API / SDK
API / SDK/2026-06-12中級

Gemini API の空応答を finish_reason から逆引きする — 診断フロー・再試行分類・監視の実務

response.text が空になる問題は candidates・prompt_feedback・finish_reason の3層で診断できます。思考トークンによる出力枯渇の検出、再試行可否の分類器、空応答率の監視まで、本番で運用している実装をまとめました。

gemini-api223finish-reason2troubleshooting55error-handling7python87typescript14

プレミアム記事

今月上旬、Gemini が大規模な障害に見舞われた朝のことです。私のパイプラインでも呼び出しが立て続けに失敗し、復旧後にログを読み返していて、ひとつ気づいたことがありました。

例外で落ちた呼び出しはすぐに分かります。アラートが鳴り、スタックトレースが残るからです。検知が遅れたのは、その隣にいた「HTTP 200 で成功したのに response.text が空」という呼び出しのほうでした。空文字は静かにパイプラインを通り抜けます。後続の処理は何事もなかったように走り、ユーザーには白い画面だけが届く。例外より、こちらのほうが質が悪いのです。

空応答は Gemini API の故障ではありません。モデルが「なぜそこで生成を止めたか」を必ずシグナルとして残しており、読み落としているのは受け取る側です。その読み方を、診断フロー・再試行の分類・監視の3段階に分けて整理します。コードは Python(google-genai)を中心に、Node/TypeScript(@google/genai)も併記します。

「空応答」には3つの層がある — response.text の前に見る場所

response.text は便利なヘルパーですが、内部では candidates[0].content.parts からテキストパーツを拾って連結しているだけです。だから「空」と一口に言っても、実際には壊れている場所が3つに分かれます。

  1. candidates 自体が空 — 生成が始まる前に、入力がブロックされています。見るべきは prompt_feedback.block_reason です
  2. candidates はあるが parts が空 — 生成が途中で打ち切られています。見るべきは finish_reason です
  3. parts はあるがテキストがない — 実は壊れていません。function_callinline_data、思考パーツなど、テキスト以外のパーツが返っています

この3層を順に見分ける関数を、私は本番のすべての呼び出し直後に挟んでいます。

from google import genai
 
client = genai.Client(api_key="YOUR_API_KEY")
 
def triage(resp) -> str:
    """空応答の層を特定する。戻り値は後段の分岐とログのキーになる。"""
    # 層1: candidates がない → 入力段階でブロック済み
    if not resp.candidates:
        block = getattr(resp.prompt_feedback, "block_reason", None)
        return f"input_blocked:{block}"
 
    cand = resp.candidates[0]
    finish = str(getattr(cand, "finish_reason", "UNKNOWN"))
 
    # 層2: parts が空 → finish_reason が打ち切り理由を持っている
    parts = getattr(getattr(cand, "content", None), "parts", None) or []
    if not parts:
        return f"no_parts:{finish}"
 
    # 層3: parts はあるがテキストがない → 別種のパーツが返っている
    text = "".join((getattr(p, "text", "") or "") for p in parts)
    if not text:
        kinds = []
        for p in parts:
            if getattr(p, "function_call", None):
                kinds.append("function_call")
            elif getattr(p, "inline_data", None):
                kinds.append("inline_data")
            elif getattr(p, "thought", False):
                kinds.append("thought")
            else:
                kinds.append("unknown")
        return f"non_text_parts:{finish}:{'+'.join(kinds)}"
 
    return "ok"

response.text に触る前にこの戻り値をログへ流すだけで、「空に見える応答」が届いた瞬間に原因の層が割れます。障害対応の最中、この1関数があるかないかで調査時間は大きく変わりました。切り分けに迷っていた時間が、ログを1行読む時間になるからです。

finish_reason 逆引き表 — そのまま再試行してよい値・無駄な値

層2に該当した場合、finish_reason(Node SDK では finishReason)の値が打ち切りの理由を教えてくれます。実務で重要なのは意味の暗記ではなく、「その値はそのまま再試行して意味があるか」という1点です。再試行しても同じ結果になる値を叩き続けると、クォータを消費するだけで何も得られません。

主な原因そのまま再試行
STOP正常終了。空ならテキスト以外のパーツを疑う不要(層3を確認)
MAX_TOKENS出力上限に到達。思考トークンの枯渇含む無意味(設定を直してから)
SAFETY出力がセーフティフィルターに接触無意味(設定か入力を直す)
RECITATION学習データとの過度な一致無意味(プロンプトを直す)
LANGUAGE非対応言語の入出力無意味
BLOCKLIST禁止語リストに接触無意味
PROHIBITED_CONTENT禁止コンテンツ判定無意味
SPII機微な個人情報の検出無意味
MALFORMED_FUNCTION_CALLツール呼び出しの生成不全条件付き(スキーマを直す)
OTHER / UNSPECIFIED内部エラー・未分類有効(バックオフ付きで)

眺めると分かるとおり、再試行が素直に効くのは実質 OTHER 系だけです。残りは「設定か入力を直してから出直す」グループと「直しても無駄なので即座に失敗させる」グループに分かれます。この3分類が、後半で実装する再試行分類器の骨格になります。

ちなみに STOP なのに空、というケースを初めて見ると戸惑いますが、ほとんどは層3の取り違えです。Function Calling を有効にした呼び出しでモデルがツール実行を選ぶと、parts には function_call だけが入り、text は空になります。これは正常な応答です。

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
candidates・prompt_feedback・finish_reason を順に見る3層診断フローと、主要9値の原因・再試行可否をまとめた逆引き表
思考トークンが出力予算を使い切って本文が消える 2.5 系特有の事故を usage_metadata から検出し、設定を直して自動復帰するコード
SAFETY や RECITATION を無駄に再試行しない3分岐リトライ分類器と、空応答率を常時監視する最小ログ構成
Stripe による安全な決済 · いつでもキャンセル可能

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Gemini Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

API / SDK2026-05-12
Gemini File APIのファイルが「PROCESSING」のまま止まる:タイムアウト処理とリトライ設計
Gemini File APIでアップロードしたファイルがPROCESSING状態のまま進まない問題を解説。ポーリングの正しい実装・タイムアウト設計・失敗時のクリーンアップまで、実際のコードで解決します。
API / SDK2026-05-03
Gemini APIで突然の「RECITATION」エラー — 著作権ブロックの正体と回避法
Gemini APIが応答を途中でカットして finish_reason に RECITATION を返すエラーの原因と、再現性のある回避策を実装パターン付きで解説します。
API / SDK2026-06-01
Gemini 2.5/3 で本文が空なのに finish_reason が MAX_TOKENS になるときの原因と対処
プロンプトはほんの数行なのに、maxOutputTokens を絞った gemini-2.5-flash が空文字を返し finish_reason が MAX_TOKENS になる — 犯人は思考トークンです。原因と3通りの対処を実装コードで整理します。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →