GEMINI LABEN
CLASSROOM — 本日8月10日から、web で Gemini in Classroom が全年齢の学生へ広がります。モバイルは8月17日からですSTUDY — 学生は授業のコースマテリアルを選び、その授業に合わせたフラッシュカードや練習クイズを Gemini で作れるようになりますPROMPT — 従来のスターター プロンプトが授業内容に紐づいた体験へ置き換わり、Classroom と Gemini を行き来する手間が減りますSUNSET — 画像生成モデルの停止は8月17日で残り7日です。Grok 4.1 ファミリーは8月20日、gemini-robotics-er-1.6-preview は8月31日ですFLASH — 7月21日に GA となった gemini-3.6-flash は、トークン効率とコード・エージェント計画が改善し、3.5 Flash より低い価格帯ですPARAMS — サンプリングパラメータの temperature・top_p・top_k は非推奨になりました。既存の呼び出しを見直す時期ですCLASSROOM — 本日8月10日から、web で Gemini in Classroom が全年齢の学生へ広がります。モバイルは8月17日からですSTUDY — 学生は授業のコースマテリアルを選び、その授業に合わせたフラッシュカードや練習クイズを Gemini で作れるようになりますPROMPT — 従来のスターター プロンプトが授業内容に紐づいた体験へ置き換わり、Classroom と Gemini を行き来する手間が減りますSUNSET — 画像生成モデルの停止は8月17日で残り7日です。Grok 4.1 ファミリーは8月20日、gemini-robotics-er-1.6-preview は8月31日ですFLASH — 7月21日に GA となった gemini-3.6-flash は、トークン効率とコード・エージェント計画が改善し、3.5 Flash より低い価格帯ですPARAMS — サンプリングパラメータの temperature・top_p・top_k は非推奨になりました。既存の呼び出しを見直す時期です
記事一覧/API / SDK
API / SDK/2026-06-25上級

Gemini API の構造化出力が本番で静かにスキーマから外れるとき — 検証と再試行を計測する運用メモ

response_schema を指定しても、Gemini の構造化出力は本番で時折スキーマからずれます。失敗を握りつぶさず計測し、finish_reason で原因を切り分け、エラーを差し戻して再試行する。実運用で安定させた検証パイプラインのメモです。

gemini-api281structured-output21pydanticvalidation3production105reliability4

プレミアム記事

response_schema を指定したのに、本番のログに ValidationError が点々と残る。再現させようとローカルで同じプロンプトを何十回叩いても、きれいに通る。けれど数千リクエストに一度くらい、ratingint のはずなのに "9点" という文字列で返り、後段の集計が落ちている——。

構造化出力は「ほぼ守られる」けれど「必ず守られる」わけではありません。やっかいなのは、この逸脱が静かに起きることです。例外で派手に落ちてくれれば気づけますが、try/except で握りつぶしていると、失敗が null や既定値にすり替わって、データだけが少しずつ濁っていきます。

ここでは、構造化出力を本番で安定させるために私が落ち着いた考え方を、動くコードとともに整理します。要点は3つです。失敗を例外ではなく「失敗率」として計測すること。原因を finish_reason で切り分けること。そして、ただ再試行するのではなく、エラーをモデルに差し戻して直させること。公式ドキュメントが「対応しています」と書く機能を、運用に耐える形にするまでの距離を埋めるメモだと思ってください。

「動くコード」と「落ちないコード」は別物

まず、基本形は素直です。Pydantic v2 のモデルを response_schema に渡し、返ってきた JSON を model_validate_json でパースします。

import os
from pydantic import BaseModel, Field
from google import genai
from google.genai import types
 
client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
 
class ReviewSummary(BaseModel):
    product_name: str = Field(description="製品名")
    rating: int = Field(description="1〜5の整数評価", ge=1, le=5)
    summary: str = Field(description="120文字以内の要約")
 
resp = client.models.generate_content(
    model="gemini-2.5-flash",
    contents="このレビューを要約してください: ……",
    config=types.GenerateContentConfig(
        response_mime_type="application/json",
        response_schema=ReviewSummary,
    ),
)
review = ReviewSummary.model_validate_json(resp.text)

これはデモとしては完璧で、社内でも「ちゃんと型で返ってくる」と評判が良いはずです。問題は、このコードが「正常系しか想定していない」ことにあります。resp.textNone になる経路、スキーマからわずかにずれた JSON が返る経路、安全フィルターが途中で止める経路——どれも本番では現実に起きます。デモで一度動いたことと、無人で何万回も回って落ちないことの間には、はっきりした距離があります。

失敗を例外ではなく「率」で持つ

最初にやるべきは、ハンドリングの精緻化ではなく計測です。構造化出力の失敗率がそもそも 0.1% なのか 5% なのかで、打つ手はまったく変わります。私はまず、成功・失敗を素通りで記録するだけの薄い計装を入れます。

from dataclasses import dataclass, field
from collections import Counter
import time
 
@dataclass
class StructuredOutputMetrics:
    total: int = 0
    success: int = 0
    failures: Counter = field(default_factory=Counter)  # 原因別
 
    def record_success(self):
        self.total += 1
        self.success += 1
 
    def record_failure(self, reason: str):
        self.total += 1
        self.failures[reason] += 1
 
    @property
    def failure_rate(self) -> float:
        return 0.0 if self.total == 0 else 1 - self.success / self.total
 
    def report(self) -> str:
        top = ", ".join(f"{k}={v}" for k, v in self.failures.most_common(5))
        return f"rate={self.failure_rate:.3%} n={self.total} [{top}]"
 
METRICS = StructuredOutputMetrics()

ポイントは、失敗を一括りにせず原因別の Counter で持つことです。「finish_reasonMAX_TOKENS で切れた」のと「JSON は完全だが rating が範囲外だった」のとでは、まったく別の対処になります。これを混ぜて「失敗 3%」とだけ記録すると、どこを直せばいいのか永遠に分かりません。私は1時間ごとにこの report() をログへ吐き、failure_rate が普段の3倍を超えたら通知が飛ぶようにしています。モデルのバージョンが切り替わった日や、プロンプトをいじった直後に、この数字が静かに跳ねることがあるからです。

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

この記事の続きを読む

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

この記事で得られること
構造化出力の失敗を「例外」ではなく「失敗率」として計測し、しきい値で警告を出す計装の入れ方
finish_reason・空テキスト・スキーマ逸脱を切り分け、原因別にリトライ方針を変える分岐の実装
エラー内容をプロンプトに差し戻す『修正付き再試行』と、Union/strict/深いネストで踏んだ地雷の回避策
Stripe による安全な決済 · いつでもキャンセル可能

この記事を購入する

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

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

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

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

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

関連記事

API / SDK2026-07-05
使っているモデルの廃止予告だけを拾う — url-context に公式チェンジログを読ませる仕組み
画像モデルの停止を公開3日前に知って肝を冷やしました。url-context に公式チェンジログを直接読ませ、自分が実際に使っているモデルの廃止予告だけを構造化して差分通知する仕組みを、そのまま動く Python と運用で削った過検知の調整まで含めて組み立てます。
API / SDK2026-06-25
Gemini API × TypeScript 型安全AIアプリケーション設計ガイド — Zodスキーマ・Structured Output・ストリーミングの統合アーキテクチャ
Gemini APIとTypeScriptで型安全なAIアプリケーションを構築する方法を解説。Zodバリデーション、Structured Output、ストリーミング、エラーハンドリングを統合した本番アーキテクチャを実装します。
API / SDK2026-06-13
Gemini の生成は通ったのに公開が落ちた朝 — 高価な出力を取りこぼさない『生成アウトボックス』の設計
生成は成功したのに、公開処理の直前でプロセスが落ちる。すると高価な出力が消え、もう一度同じ生成に課金することになります。生成結果を先に永続化し、公開を冪等な後続処理に分離する『生成アウトボックス』の実装と、6月の障害下での運用記録をまとめました。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →