GEMINI LABEN
VIDEO — 9月1日、エージェント型の動画理解が 3.7 Flash・3.6 Flash・3.5 Flash-Lite に届きました。モデルが動画のタイムラインを自分で辿りますTOKENS — 必要なときだけ字幕・フレーム・音声を取りに行く方式で、長尺では従来処理に比べて最大88%のトークン削減とされていますSCOPE — 対応は Interactions API と GenerateContent API の両方です。長い動画を扱う見積もりは、この変更で前提が変わりますMUSIC — 9月3日、Lyria 3.5 がパブリックプレビューへ。フルレングスの楽曲生成に対応し、44.1kHz ステレオを出力しますCONTROL — Lyria 3.5 はテキストと画像を入力に取り、音楽的な一貫性、自然なボーカル、尺と構成の細かい制御が改善されましたROBOTICS — gemini-robotics-er-2-streaming-preview は Live API 上のリアルタイム配信向けで、物理動作に対するブロッキング挙動つきの function calling に対応しますVIDEO — 9月1日、エージェント型の動画理解が 3.7 Flash・3.6 Flash・3.5 Flash-Lite に届きました。モデルが動画のタイムラインを自分で辿りますTOKENS — 必要なときだけ字幕・フレーム・音声を取りに行く方式で、長尺では従来処理に比べて最大88%のトークン削減とされていますSCOPE — 対応は Interactions API と GenerateContent API の両方です。長い動画を扱う見積もりは、この変更で前提が変わりますMUSIC — 9月3日、Lyria 3.5 がパブリックプレビューへ。フルレングスの楽曲生成に対応し、44.1kHz ステレオを出力しますCONTROL — Lyria 3.5 はテキストと画像を入力に取り、音楽的な一貫性、自然なボーカル、尺と構成の細かい制御が改善されましたROBOTICS — gemini-robotics-er-2-streaming-preview は Live API 上のリアルタイム配信向けで、物理動作に対するブロッキング挙動つきの function calling に対応します
記事一覧/開発ツール
開発ツール/2026-08-22中級

段階公開を止める判断だけは、Gemini に渡さないことにしました

段階公開の初日に見えるクラッシュ率には、良し悪しを判定できるだけの母数がありません。止める・進める・まだ言えないの3状態に分け、Gemini にはレビュー本文の帰属判定だけを任せた配線を残します。

段階公開Gemini API233個人開発114リリース運用2responseSchema6

配信率 1% の初日、管理画面のクラッシュ件数は 2 件でした。前の版と並べても増えているようには見えなかったので、私は翌日の引き上げをそのまま承認しました。

5% まで広がった三日目、同じ症状を訴えるレビューが並び始めました。初日のあの 2 件は、いま思えば「問題がない」を意味していませんでした。単に、何も言えないだけの数だったのです。

個人開発でアプリを並行して出していると、段階公開の各日にかけられる時間は数分しかありません。その数分で判断を誤らないために、どこを機械に任せ、どこを言語モデルに任せ、どこを自分の手元に残すかを引き直しました。結論から書くと、止めるかどうかの判断そのものはモデルに渡さない、という線引きになりました。

初日の「静かでした」は、静かさの証拠ではありません

App Store の段階的リリースは 7 日間かけて 1% / 2% / 5% / 10% / 20% / 50% / 100% と配信率が上がります。ここで見落としやすいのは、初日の母数が最終日の 200 分の 1 しかないという点です。

1 日あたりの新規セッションが 100% 配信時に 20,000 件あるアプリを例にします。真のクラッシュ発生率が 0.8% で一定だと仮定して、各日に観測される件数と、その観測から言える範囲(Wilson の 95% 信頼区間)を計算しました。

配信率母数観測件数95%信頼区間区間の幅
Day 11%20020.27% 〜 3.57%3.30pt
Day 22%40030.26% 〜 2.18%1.93pt
Day 35%1,00080.41% 〜 1.57%1.16pt
Day 410%2,000160.49% 〜 1.30%0.80pt
Day 520%4,000320.57% 〜 1.13%0.56pt
Day 650%10,000800.64% 〜 0.99%0.35pt
Day 7100%20,0001600.69% 〜 0.93%0.25pt

Day 1 の行を見てください。観測値は 1.00% ですが、真の値は 0.27% かもしれないし 3.57% かもしれません。平常時の 4 倍に悪化していても、初日の数字は同じ顔をして並びます

私が三日目に取り逃がしたのは、まさにこの幅でした。数字が小さいことを安全と読み替えていたわけです。

「進める・止める」の二択をやめて、「まだ言えない」を状態にする

判断を誤った原因は、判断の選択肢が 2 つしかなかったことにあります。人は 2 択を渡されると、判断できない状況でもどちらかを選んでしまいます。そこで状態を 3 つに増やしました。

import math
 
def wilson(k, n, z=1.96):
    """k 件 / n 件 の観測から、真の発生率がありうる範囲を返します。"""
    if n == 0:
        return (0.0, 1.0)
    p = k / n
    d = 1 + z * z / n
    center = (p + z * z / (2 * n)) / d
    half = z * math.sqrt(p * (1 - p) / n + z * z / (4 * n * n)) / d
    return (max(0.0, center - half), min(1.0, center + half))
 
def verdict(n, k, baseline, ceiling):
    """baseline = 平常時の発生率 / ceiling = ここを超えたら止めるという上限。"""
    lo, hi = wilson(k, n)
    if lo > ceiling:
        return "STOP"        # 上限を超えていることが、母数の少なさを差し引いても言える
    if hi <= ceiling:
        return "GO"          # 上限を超えていないことが言える
    return "UNDECIDED"       # まだどちらとも言えない
 
def required_users(baseline, ceiling, z=1.96, step=100):
    """平常どおりの発生率が続いた場合に、GO と言えるようになる母数。"""
    n = step
    while n <= 2_000_000:
        if wilson(round(n * baseline), n, z)[1] <= ceiling:
            return n
        n += step
    return None

平常時 0.8%、上限を倍の 1.6% に置いて実行すると、次の結果になりました。

母数観測件数観測値信頼区間判定
20021.00%0.27% 〜 3.57%UNDECIDED
20084.00%2.04% 〜 7.69%STOP
1,00080.80%0.41% 〜 1.57%GO
1,000303.00%2.11% 〜 4.25%STOP
4,000902.25%1.83% 〜 2.76%STOP

required_users(0.008, 0.016)900 を返します。つまり平常どおりの数字が並んでいても、900 人に届くまでは「大丈夫でした」と言う根拠がありません。冒頭の 200 人はその 4 分の 1 以下でした。

一方で母数が少なくても STOP は出ます。200 人中 8 件なら、少ない母数を差し引いてもなお上限を超えています。壊れているときは早く分かり、壊れていないことは遅れて分かるという非対称は、そのまま受け入れてよいと考えています。

レビュー本文だけは、数えるだけでは足りません

クラッシュ率は機械で判定できます。困るのはレビューです。「アップデートしてから写真が保存できません」と「広告が多すぎます」は、どちらも星 1 として同じ列に積まれます。片方は今回の版の話で、もう片方は前から言われ続けている話です。

この帰属の判定こそ、言語モデルに向いた仕事だと感じています。ここでは responseSchema で出力の形を固定して、1 件ずつ分類させています。

from google import genai
from google.genai import types
 
client = genai.Client(api_key="YOUR_API_KEY")
 
SCHEMA = {
    "type": "object",
    "properties": {
        "attributable_to_update": {"type": "boolean"},
        "symptom": {"type": "string"},
        "severity": {"type": "string", "enum": ["blocking", "major", "minor", "none"]},
    },
    "required": ["attributable_to_update", "symptom", "severity"],
}
 
PROMPT = """次のアプリレビューを分類してください。
判定するのは「この更新のあとに起きた不具合を述べているか」だけです。
アプリの良し悪しや、公開を続けるべきかどうかは判定しないでください。
価格や広告量など、更新前から存在する不満は attributable_to_update を false にします。
 
レビュー本文:
{body}"""
 
def classify(body: str) -> dict:
    res = client.models.generate_content(
        model="gemini-3.7-flash",
        contents=PROMPT.format(body=body),
        config=types.GenerateContentConfig(
            response_mime_type="application/json",
            response_schema=SCHEMA,
        ),
    )
    return res.parsed

プロンプトの 2 行目と 3 行目は、書き足したのではなく削り出した部分です。最初は「このレビューは公開停止に値しますか」まで聞いていました。返ってくる文章はもっともらしく、しかも母数が少ない日ほど「様子を見るのが妥当です」と穏当な方向へ寄りました。

穏当な答えは反論しにくく、根拠らしい文章が添えられていると、そのまま承認してしまいます。判断をモデルに聞いた瞬間、自分の 3 状態は 2 択に戻っていました。だから聞くのをやめました。

レビュー返信そのものを Gemini に書かせる場合の注意点は、App Store / Google Play のレビュー返信を Gemini API で自動化したときにはまった「8秒ルール」 に別途まとめています。

モデルの出力を、しきい値の分子に混ぜない

分類結果を得たあと、それをクラッシュ率の計算に足したくなります。実際に一度そうしました。結果として、モデルが 1 件を取りこぼすたびに判定が揺れる仕組みになりました。

いまは 2 つの系統を分けたまま合流させています。合流点で行うのは、どちらかが赤なら止める、判断が割れたら人に回すという単純な突き合わせだけです。

def route(judgements, crash_state):
    """judgements: モデルの分類結果 / crash_state: 決定的に算出した3状態。"""
    regressions = [
        j for j in judgements
        if j["attributable_to_update"] and j["severity"] in ("blocking", "major")
    ]
 
    if crash_state == "STOP":
        return ("STOP", "クラッシュ率の下限が上限しきい値を超えました")
    if len(regressions) >= 3:
        return ("HUMAN_REVIEW", f"更新起因の重大レビューが {len(regressions)} 件あります")
    if crash_state == "UNDECIDED":
        return ("HOLD", "母数が不足しています。配信率を上げずに翌日まで待ちます")
    return ("GO", "しきい値内です。次の配信率へ進みます")

HUMAN_REVIEW を独立した戻り値にしたことが、この配線で一番効きました。モデルが 3 件を拾ったとき、システムは止めもしませんし進めもしません。私に本文を読ませます。読むのは 3 件ですから、朝の数分で足ります。

HOLDGO の差も意識して分けました。HOLD は「昨日と同じ配信率でもう一日置く」という積極的な行動です。何もしないことと、待つと決めることは別のものだと考えています。

運用に落とす前に決めた 4 つ

数値を後から動かすと、都合の良い方へ動かしてしまいます。リリース前に固定して、リリース中は触らないと決めました。

決めごと私の設定そう決めた理由
baseline(平常時の発生率)直前 4 週間の中央値平均だと過去の悪い版に引っ張られます
ceiling(止める上限)baseline の 2 倍1.5 倍だと UNDECIDED が最終日まで解けませんでした
最小母数required_users の戻り値この数に届く前の GO は、根拠のない GO です
モデル出力の扱い人へ回す判断のみに使用しきい値の分子に入れると、分類漏れが判定を揺らします

ceiling を 2 倍にした経緯だけ補足します。1.5 倍で試したところ、平常どおりの数字が続いても required_users が 2,800 まで上がり、Day 4 を過ぎるまで UNDECIDED が解けませんでした。厳しくすると安全になるのではなく、7 日間ずっと何も言えないだけになります。しきい値の厳しさと、判断できるようになる速さは交換関係にあります。

次に段階公開を始めるときは、まず自分のアプリの required_users を計算してみてください。その数を配信率で割れば、判断が意味を持ち始めるのが何日目からなのかが分かります。私の場合は Day 3 でした。それより前の数字を眺めていた時間は、安心を買っていただけだったことになります。

リリース直後に届く通知やストア文言のように、生成した文字列を配信前に検査する話は 通知文の長さ指示が英語版だけ効かなかった理由と、生成後に幅で詰める実装 で扱っています。あわせて読んでいただけると、リリース当日の手順がもう少し繋がるはずです。

数字の読み違えは、私自身がつい先日やったばかりの失敗です。同じところで足を止める方が一人でも減れば嬉しく思います。お読みいただきありがとうございました。

シェア

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

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

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

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

開発ツール2026-08-30
モデル廃止日をまたぐ古い端末に、読み替えではなく拒否を返す設計
モデルが止まっても、配布済みのアプリは止まりません。後継へ黙って差し替えるか、明示的に断るか。出力契約を軸に廃止台帳を作り、残存曲線から自分の締切日を逆算するまでの実装と計測をまとめました。
開発ツール2026-08-24
Gemini の訳文で %1$s が消えても、レビューでは気づけません。文字列リソースへ戻す前の検査
Gemini に訳させたアプリの文字列から書式指定子が抜けたり全角になったりする事象を、実運用の中で拾い続けています。壊れ方を4種類に整理し、生成時に守らせる書き方と、受け取った直後に弾く短い検査を紹介します。
開発ツール2026-08-20
Gemini に書かせたレビュー返信を、送信前に止めるための最小の検査
ストアのレビュー返信は下書きより送信後が怖い作業です。守れない約束・連絡先の混入・文体崩れを送信前に機械で止める Python の検査を、実際に走らせた結果と誤検出の修正まで含めて置いておきます。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます