モデル ID を gemini-3.7-flash から gemini-3.8-flash へ書き換えたのは、Lab のカテゴリ説明文を整えるだけの小さな呼び出しでした。差分は一行です。夜のうちに流して、翌朝に出力を眺めましたら、同じ入力に対して前日とは違う組み立ての文章が並んでおりました。
どちらがよいのかを、その場で決められませんでした。並べてみれば新しいほうが丁寧に見えます。けれども丁寧さと、私がほしかった短さは別のものです。
——そして何より、前日の出力を私はもう持っておりませんでした。比べる材料を残していなかった、というだけの話でした。
公式は「3.7 Flash も引き続き使えます」と書いています
最初にお伝えしたいのは、新しい世代へ上げることが義務ではない、という一点です。
Gemini 3.8 Flash の What's new ページには、3.8 Flash が長期にわたるソフトウェア開発・自律エージェント・複雑な企業ワークフローに向けて設計されている旨が書かれています。難しい課題に対して推論の刻みを細かくし、ツールを繰り返し呼び、自分の出した答えを途中で検証する——そのぶんトークンを多く使うことがある、という説明が続きます。
そのうえで、すべてのワークフローにこの水準の検証が要るわけではない、と明記されています。日常的な処理では推論の労力を下げてよく、あるいは 3.7 Flash のままでも構わない、と。
| 項目 | gemini-3.8-flash |
|---|---|
| 既定の thinking level | medium |
| 指定できる値 | low / medium / high |
minimal | 非対応(指定するとエラーが返ります) |
| コンテキスト | 1M トークン |
| 最大出力 | 64k トークン |
| 導入価格の期限 | 2026年12月31日まで |
つまり判断の材料は、公式が並べてくれている範囲では「速さと賢さのつまみが増えた」ところまでです。自分の仕事でどちらが良いかは、手元で確かめるほかにありません。
比べる前に、土俵を揃えます
ここで私は一度つまずきました。書き残しておきます。
3.8 Flash への移行チェックリストには、temperature と top_p、top_k を生成設定から外すこと、thinking_budget を文字列の thinking_level に置き換えること、candidate_count を削除することが並んでいます。私は言われたとおりに 3.8 側のコードを直しました。そして 3.7 側は、既存の設定のまま走らせたのです。
結果は芳しくありませんでした。差は確かに出たのですが、それが世代の違いによるものなのか、外した temperature=0.2 のせいなのか、切り分けられなくなったのです。
いま思えば当たり前のことでした。比較の土俵は、新しいほうではなく、古いほうに合わせて作り直します。 3.7 側からもサンプリングパラメータを抜いて、両方を素の状態にしてから並べます。ここを省くと、あとの数字がすべて読めなくなります。
質問集は、間違えられた記録から作ります
質問集を新しく考えてはいけない、というのが二つめの線引きです。
思いついた「よい質問」は、たいてい自分が答えを知っている質問になります。そうではなく、過去に出力を直した箇所を集めます。レビューで書き戻した文、公開前に手で削った段落、アプリのストア説明文で語調が合わずに書き換えた行。私の場合は、それだけで20問がすぐに埋まりました。
形式は1行1問の JSONL にしております。判定を機械に任せたいので、期待する語と、出てほしくない語を添えます。
{"id": "cat-desc-01", "input": "壁紙アプリのカテゴリ「和風」の説明文を1文で書いてください。", "must_include": ["和"], "must_not_include": ["最高", "ぜひ"]}
{"id": "cat-desc-02", "input": "同じ説明文を、句点を1つだけ使って書き直してください。", "must_include": ["。"], "must_not_include": ["!"]}must_not_include のほうが効きます。世代を上げたときに増えるのは、間違いよりも、頼んでいない装飾のほうだからです。
同じ質問を3回ずつ、二つの世代へ流します
1回ずつ比べても、答えが割れているのか自分が揺らしているのかは分かりません。そこで各問を3回繰り返し、判定が3回とも揃った問だけを「割れていない」と数えます。
import json
import statistics
import time
from google import genai
client = genai.Client()
MODELS = [
{"name": "gemini-3.7-flash", "config": {}},
{"name": "gemini-3.8-flash", "config": {"thinking_level": "medium"}},
]
REPEAT = 3
SYSTEM = "日本語で、箇条書きを使わず200字以内で答えてください。"
def ask(model, config, prompt):
started = time.time()
interaction = client.interactions.create(
model=model,
input=prompt,
system_instruction=SYSTEM,
generation_config=config,
)
usage = interaction.usage
return {
"text": interaction.output_text,
"out_tokens": usage.total_output_tokens or 0,
"thought_tokens": usage.total_thought_tokens or 0,
"seconds": round(time.time() - started, 2),
}
def judge(text, item):
hit = all(word in text for word in item.get("must_include", []))
miss = any(word in text for word in item.get("must_not_include", []))
return hit and not miss
def run(path):
with open(path, encoding="utf-8") as handle:
items = [json.loads(line) for line in handle if line.strip()]
for model in MODELS:
passed = agreed = 0
out_tokens, thoughts, seconds = [], [], []
for item in items:
runs = [ask(model["name"], model["config"], item["input"]) for _ in range(REPEAT)]
verdicts = [judge(run_result["text"], item) for run_result in runs]
passed += sum(verdicts)
agreed += 1 if len(set(verdicts)) == 1 else 0
out_tokens += [r["out_tokens"] for r in runs]
thoughts += [r["thought_tokens"] for r in runs]
seconds += [r["seconds"] for r in runs]
total = len(items) * REPEAT
print(
model["name"],
f"充足 {passed}/{total}",
f"一致 {agreed}/{len(items)}",
f"出力トークン中央値 {statistics.median(out_tokens):.0f}",
f"思考トークン中央値 {statistics.median(thoughts):.0f}",
f"秒の中央値 {statistics.median(seconds):.2f}",
)
if __name__ == "__main__":
run("eval_set.jsonl")usage から取っている4つの値には、それぞれ役割があります。total_output_tokens は請求に直結する数、total_thought_tokens はモデルが考えるために使った数、そして経過秒数は体感に効く数です。平均ではなく中央値を出しているのは、20問のうち1問が長引いただけで平均が動いてしまうためです。
実行前に、両方のモデルで thinking_level を変えた回も回しておくと、判断がもう一段はっきりします。3.8 Flash を low にしたときの数字は、私にとっていちばん実務に近い列でした。
表にすると、迷いどころが2列に収まります
出てきた数字は、この5行に落として眺めております。
| 指標 | 何を見ているか | 差が出たときの読み方 |
|---|---|---|
| 充足率 | 期待語を満たし、禁止語を含まない回の割合 | ここが落ちたなら、世代を上げる理由がありません |
| 自己一致率 | 3回の判定が揃った問の数 | 落ちていれば「答えが割れる」の正体は世代差です |
| 出力トークン中央値 | 請求に直結する量 | 充足率が同じでここだけ伸びたなら、費用の判断になります |
| 思考トークン中央値 | 考えるために使った量 | 伸びた分を thinking_level で戻せるか試します |
| 秒の中央値 | 待たされる体感 | 対話的な画面では、充足率より重いことがあります |
私の20問では、充足率はほぼ動かず、自己一致率も保たれたまま、出力トークンだけがはっきり伸びました。これは品質の問題ではなく費用の問題でしたので、thinking_level を low に下げて測り直したところ、伸びの大半が戻りました。単価が据え置きでも請求が増える仕組みについては、単価と請求は別の数字であるという話に分けて書いております。
一方で、自己一致率が落ちる問も2問だけありました。どちらも、こちらの指示が曖昧だった問です。世代を疑って測り始めたつもりが、自分の書いた指示の粗さを拾い直すことになりました。
私が引いた線引き
モデル ID は固定したままにして、上げるのは質問集が通った日だけ。 この順番だけは、急いでいる日でも崩さないようにしております。
新しい世代が出た日に上げたくなる気持ちは、私にもあります。けれども、その日に上げても得られるものは「新しい」という感触だけで、手元の仕事が良くなったかどうかは何も分かっておりません。20問を通してから上げれば、良くなった箇所と悪くなった箇所を、両方とも言葉にできます。
まずは5問だけ書き出してみてください。過去に自分で直した出力を思い出して、直す前の文を must_not_include に入れる——それだけで、次に世代が変わった日の朝が、かなり楽になります。
お読みいただきありがとうございました。私もまだ質問集を育てている途中で、20問はいまも増えたり減ったりしております。