5 月の中旬、Gemini 2.0 Flash の廃止スケジュールが 6 月に近づいているのを意識し始めて、自分が使っている個人開発のジョブを 1 件ずつ点検することにしました。年単位で動いている運用ジョブと、最近書いたばかりの実験ジョブが混在しており、廃止が来てから慌てて差し替えると確実にどこかが壊れます。
対象は 2 種類のバッチです。壁紙画像のメタデータを生成するジョブと、App Store の口コミを日次で要約するジョブ。どちらも人の目が毎日入るわけではないので、静かに品質が落ちると気付くのが遅れます。以下は、その 2 本を廃止スケジュールから切り離すためにやったことの記録です。
廃止アナウンスを「6 月に来るイベント」として扱わない
私が最初に変えたのは、廃止アナウンスを「6 月のイベント」として扱う発想を捨てたことです。アナウンスは確かに 6 月のどこかに区切りがありますが、本当に向き合うべきは「廃止前後でレスポンスのクセが変わってジョブの精度がぶれる」ほうです。
私の経験では、Gemini はモデル世代が変わると次のような部分でクセが移ります。
日本語の敬体/常体のゆらぎ
JSON 出力時の null の扱い(フィールド省略か明示 null か)
長文要約での句読点配分
ツール呼び出しの引数フォーマットの微差
これらは API のレスポンスコードでは検出できないので、廃止日だけ見ていると本番運用の中で静かに壊れます。しかも壊れ方が例外ではなく品質の劣化なので、監視のアラートも鳴りません。カレンダーではなく差分を見る。ここが出発点でした。
新旧モデルの差分を機械的に取る比較バッチ
「クセが変わる」と書いても、人間の目で 2 つの出力を並べて見比べるのは 10 件が限界です。私は 40 行ほどの比較バッチを書いて、代表的な入力 30 件を毎晩両モデルに流し、構造の差分だけを機械的に拾うようにしました。
import json
from difflib import SequenceMatcher
from google import genai
client = genai.Client( api_key = API_KEY )
OLD , NEW = "gemini-2.0-flash" , "gemini-2.5-flash"
def run (model: str , prompt: str ) -> str :
res = client.models.generate_content(
model = model,
contents = prompt,
config = { "response_mime_type" : "application/json" , "temperature" : 0 },
)
return res.text
def shape (payload: str ) -> dict :
try :
obj = json.loads(payload)
except json.JSONDecodeError:
return { "parsable" : False , "keys" : [], "empty" : []}
return {
"parsable" : True ,
"keys" : sorted (obj.keys()),
"empty" : sorted (k for k, v in obj.items() if v in ([], "" , None )),
"chars" : len (payload),
}
def compare (prompt: str ) -> dict :
a, b = run( OLD , prompt), run( NEW , prompt)
sa, sb = shape(a), shape(b)
return {
"dropped_keys" : sorted ( set (sa[ "keys" ]) - set (sb[ "keys" ])),
"added_keys" : sorted ( set (sb[ "keys" ]) - set (sa[ "keys" ])),
"similarity" : round (SequenceMatcher( None , a, b).ratio(), 3 ),
"old" : sa,
"new" : sb,
}
temperature を 0 に固定しているのは、モデル差ではなくサンプリングのゆらぎを拾ってしまうのを避けるためです。ここを既定値のままにすると、同じモデル同士でも similarity が 0.7 前後まで落ちて、何を見ているのか分からなくなります。
私が閾値にしたのは 2 つだけです。dropped_keys が空でなければその日のうちに parse 側を確認する。similarity が 0.80 を下回った入力は、翌朝に人間の目で読む。この 2 つに絞ったことで、毎晩の確認が 3 分で終わる作業になりました。差分バッチは精緻に作り込むほど見なくなるので、判断が 2 つで済む形に削ったのが結果的に良かったと感じています。
私の点検チェックリスト 4 つ
5 月時点で私が回しているチェックリストは 4 つに整理してあります。
1. プロバイダ抽象の有無
Gemini API を直叩きしている呼び出しと、自前の薄い抽象越しに呼んでいる呼び出しが混在していると、廃止対応の作業量が指数的に増えます。私は次のような最小抽象を 30 行ほど書いて、すべての呼び出しをこれに通すよう統一しました。
from dataclasses import dataclass
from typing import Optional
from google import genai
@dataclass
class GeminiRequest :
prompt: str
model: Optional[ str ] = None # None なら provider の default
json_schema: Optional[ dict ] = None
class GeminiProvider :
DEFAULT_MODEL = "gemini-2.5-flash" # 5 月から default を移行
FALLBACK_MODEL = "gemini-2.0-flash" # 廃止までは残す
def __init__ (self, api_key: str ):
self ._client = genai.Client( api_key = api_key)
def call (self, req: GeminiRequest) -> str :
model = req.model or self . DEFAULT_MODEL
cfg = { "response_mime_type" : "application/json" } if req.json_schema else {}
try :
res = self ._client.models.generate_content(
model = model, contents = req.prompt, config = cfg
)
return res.text
except Exception :
if model != self . FALLBACK_MODEL :
return self .call(GeminiRequest(req.prompt, self . FALLBACK_MODEL , req.json_schema))
raise
ここで意図的にやっているのは「default を 2.5 Flash に倒し、フォールバックとして 2.0 Flash をしばらく残す」運用です。廃止日が来る前に default 側で 100% 運用される実績を作っておきたいので、フォールバックを呼び出した回数が日次でカウントできるよう、別途メトリクスも入れてあります。
2. JSON スキーマ運用の差分
Gemini 2.0 Flash と 2.5 Flash で JSON 出力の挙動が体感で違うのは、空配列を返すときの形と、null を許容するフィールドの扱いです。2.0 では "items": [] を出していた場面で、2.5 ではフィールド自体を省略してくる頻度が増えました。私は壁紙アプリのレビュー要約スキーマで "keywords": [] が前提だったので、廃止前にスキーマと parse 側を両対応に直しました。
修正そのものは地味です。ただ、この形に揃えておくと以後のモデル移行で毎回同じ場所を触らずに済みます。
from typing import Any
REVIEW_DEFAULTS : dict[ str , Any] = {
"sentiment" : "unknown" ,
"keywords" : [],
"issues" : [],
"summary" : "" ,
}
def coerce_review_summary (raw: dict[ str , Any]) -> dict[ str , Any]:
"""フィールド省略・明示 null・空配列のいずれで来ても同じ形に潰す。"""
out = dict ( REVIEW_DEFAULTS )
for key, default in REVIEW_DEFAULTS .items():
value = raw.get(key)
if value is None :
continue
if isinstance (default, list ) and not isinstance (value, list ):
value = [value]
out[key] = value
out[ "summary" ] = str (out[ "summary" ]).strip()
return out
raw.get(key) or default と 1 行で書かなかったのには理由があります。sentiment が "0" のような偽値めいた文字列で返ってきたときに既定値へ潰れてしまい、それに気付くのに私は半日を使いました。省略と空値を分けて扱う、というのがこの関数の全てです。
3. ログのモデル名タグ付け
すべての呼び出しログにモデル名を入れておきます。これは廃止後のトラブルシュート時にどのジョブが何モデルで動いていたかを後から辿るためです。私は構造化ログにしていなかったジョブが 2 つあって、5 月の点検で全てに model_id フィールドを追加しました。
4. コスト試算と推論時間の実測
私の実測値で言うと、Gemini 2.5 Flash は 2.0 Flash と比べて、入力 1,000 トークンあたりの推論時間が 5 〜 15% ほど増える傾向がありました。月の処理量が多いジョブだとここがそのまま広告由来の収益に対する原価率に乗ってくるので、レイテンシ要件のあるジョブだけは 2.5 Pro よりも 2.5 Flash の方が個人開発の経済性として現実的だと判断しました。
同じ判断を迫られている方には、Pro への格上げを検討する前に出力トークンの上限を絞ってみることをお勧めします。私の場合は max_output_tokens を 320 に固定しただけで、体感の遅さがほぼ消えました。要約タスクは放っておくと必要以上に長く書くので、遅さの正体がモデルではなく出力長だったという回避可能なケースは珍しくありません。
フォールバック率を日次で見るための最小メトリクス
チェックリストの 1 番で触れたフォールバック回数は、抽象の内側で数えています。専用の監視基盤は持っていないので、既存のログ集約先に 1 行流すだけの形にしました。
from collections import Counter
from datetime import date
_calls: Counter = Counter()
def record (day: date, model: str , fell_back: bool ) -> None :
_calls[(day, model, fell_back)] += 1
def fallback_rate (day: date) -> float :
total = sum (v for (d, _, _), v in _calls.items() if d == day)
if total == 0 :
return 0.0
fb = sum (v for (d, _, f), v in _calls.items() if d == day and f)
return fb / total
見る値はフォールバック率ひとつです。5 月の第 3 週は 0.4% 前後、第 4 週に入って 0.0% が続くようになりました。ここで大事なのは絶対値ではなく「ゼロが何日続いたか」で、私は 7 日連続ゼロを移行完了の合図に決めました。数字そのものより、判断の条件を先に決めておくほうが後で迷いません。
コスト試算の最小フォーマット
私が壁紙アプリのバッチで使っているコスト試算は、次のようなシンプルな形に揃えています。
def estimate_monthly_cost_yen (
input_tokens_per_call: int ,
output_tokens_per_call: int ,
calls_per_day: int ,
in_price_per_1m: float , # 円/1M トークン
out_price_per_1m: float ,
) -> float :
daily_in = input_tokens_per_call * calls_per_day
daily_out = output_tokens_per_call * calls_per_day
daily_cost = (daily_in / 1_000_000 ) * in_price_per_1m + \
(daily_out / 1_000_000 ) * out_price_per_1m
return daily_cost * 30
5 月時点での実測値を入れた結果、私のレビュー要約バッチ(1 日 8,000 件、平均入力 1,200 トークン、出力 250 トークン)は、Gemini 2.0 Flash 換算で月 7,800 円程度、2.5 Flash 換算で月 9,400 円程度でした。差は約 20%。これを「許容する」と早めに決めて、メンタル的に廃止対応を引きずらないようにしました。
廃止前にやっておくべき「リハーサル日」を 1 日決める
5 月から 6 月までの間に、私は「リハーサル日」を 1 日だけ設定しました。当日は全ジョブの default モデルを 2.5 Flash に倒し、フォールバックを意図的に無効化します。これで丸 1 日 2.5 Flash だけで運用したログを取り、その日にだけ起きるエラーパターンを抽出します。
リハーサル日に出た問題は次の通りでした。
レビュー要約で「日本語の敬語が一段階強くなる」傾向
JSON 出力でフィールド省略の頻度が上がる
長文(4,000 字以上の入力)でレスポンスが 0.5 〜 1 秒遅くなる
これらは廃止日に向けてプロンプトを微調整するか、parse 側で吸収するかの判断材料になりました。
リハーサル日に気付いた「ジョブ間で挙動が違う」現象
リハーサル日の結果でもう一つ書き残しておきたい発見があります。同じ Gemini 2.5 Flash を使っていても、ジョブごとにレスポンスの体感が違うのです。具体的には、画像メタデータ生成のジョブは 2.0 と 2.5 で実質的に同じ品質に見えたのに対し、レビュー要約のジョブでは敬語の格上げと言い回しの硬化が明確に出ました。
私の推測では、これは入力の長さとプロンプトのトーン指定の組み合わせで挙動が変わっているからです。短い入力(300 トークン以下)ではモデル間の差が見えにくく、長い入力(1,000 トークン超)でしっかり差が出ます。壁紙アプリのレビュー要約は平均で 1,200 トークンの入力を扱うので、ちょうど差が現れる帯に乗っていました。
この経験から、リハーサル日に確認するジョブの優先順位は「入力長が長いものから順」にした方が効率的だ、という現場の感触を得ました。短いジョブは廃止日まで触らなくても被害が出にくく、長いジョブほど事前の調整が必要、という整理です。
【8 月追記】廃止日を過ぎて実際に起きたこと
ここから先は、廃止日を挟んで 2 ヶ月半運用した後の答え合わせです。5 月に書いた見立てがどこで当たり、どこで外れたかを並べておきます。
項目 5 月時点の見立て 8 月時点の実際
フォールバック率 7 日連続ゼロを移行完了の合図にする 6 月第 1 週に達成。廃止日の作業はモデル名を一本化するコミット 1 本で完了
月額コスト 2.5 Flash で月 9,400 円 実測は月 8,900 円。出力トークンが平均 250 → 約 210 に減ったぶん見立てより安い
JSON フィールド省略 parse 側で吸収する 6 〜 8 月で parse 起因の失敗はゼロ
敬語の格上げ プロンプトの微調整で収める トーン指定を 1 行足して収束。要約の読み味は 5 月時点に戻った
バッチ実行時間 入力 1,000 トークンあたり 5 〜 15% 増 定常時はほぼ想定どおり。ただし切替直後の 1 日だけ 12% 余計に伸びた
外れたのは最後の行です。切替直後だけバッチ実行時間が伸びた原因は、モデルを変えるとコンテキストキャッシュが別物になることでした。画像メタデータ生成のジョブでは共通の長いシステムプロンプトをキャッシュに載せていたのですが、キャッシュはモデル単位で管理されるため、default を切り替えた瞬間にヒット率がゼロから積み上がり直します。
これは 5 月の私の点検リストから完全に抜け落ちていた観点でした。廃止対応というと呼び出し側のコードばかり見てしまいますが、モデル名を鍵に持っている状態は他にもあります。キャッシュ、プロンプトのバージョン管理、評価用のゴールデンデータ。移行時に「温め直しが必要なもの」を先に列挙しておけば、切替日の 12% は事前に予測できたはずです。
リハーサル日を半日ではなく丸 1 日にしておいたのは、結果的に正解でした。キャッシュのヒット率が戻るまでには数時間かかるので、半日で切り上げていたら「遅くなった」という印象だけが残り、原因まで辿り着けなかったと思います。
廃止スケジュールとの距離の取り方
3 ヶ月分の記録を通して残った結論は、廃止対応を「廃止日まで何もしない」と「今すぐ全部書き換える」の二択にしないことです。フォールバック付きの薄い抽象を先に入れ、差分バッチとフォールバック率で状態を見えるようにし、リハーサル日を 1 日だけ取る。この 3 つが揃っていれば、廃止日そのものはコミット 1 本の作業に縮みます。
次の廃止告知が来たときに私が最初にやるのは、コードを開くことではなく「モデル名を鍵にしている場所」を紙に書き出すことだと思います。キャッシュの件で学んだのはそれでした。廃止告知のメールを開くときの、あの少し胃が縮む感覚は何度経験しても慣れませんが、手順が決まっているだけでずいぶん扱いやすくなります。ここまでお付き合いいただき、ありがとうございました。