Gemini 2.5 Pro を個人開発のアプリに試験導入したとき、最初の週で想定の3倍以上の費用が発生しました。原因を調べてみると、thinking_budget を設定していなかったことによる thinking tokens の暴走でした。パラメータの存在は知っていましたが、「デフォルトで適当にやってくれる」と高をくくっていたのです。
個人開発では、API コスト管理の失敗がサービスの継続を直接脅かします。机上の話ではなく、毎月の請求書として返ってきます。
同じ轍を踏まないために、thinking_budget の設計と、個人開発・小規模プロダクション環境での実践的な制御パターンをまとめます。動的な分類器の作り方、実際の数値で見たコスト削減効果、暴走を未然に防ぐ監視の仕組みまで、本番投入できる形で扱います。
thinking_budget を未設定のままにすると、なぜ危険なのか
Gemini 2.5 Pro は、最終回答を出す前に内部で推論ステップを踏みます。この推論プロセスに使われるトークンが thinking tokens で、thinking_budget はそれに使える最大トークン数を指定するパラメータです。
公式ドキュメントには「0に設定すると思考モードをオフにできる」と書かれていますが、省略した場合の挙動についての記述は曖昧です。私自身が試したところ、省略すると最大24,576トークンを自動的に使用しようとする ことが判明しました(2026年5月時点)。
簡単な感情分類(「このレビューは肯定的か否定的か」レベル)でも、デフォルト設定なら thinking tokens が数千トークン発生します。Input/Output に加えて thinking tokens にも課金される Gemini 2.5 Pro では、これが日々数千リクエスト積み重なると費用が数倍に膨らみます。
thinking tokens の課金体系(2026年5月現在)
Gemini 2.5 Pro の料金は大まかに3区分です。
Input tokens : こちらが送る入力テキスト
Output tokens : モデルが生成した最終回答
Thinking tokens : 推論プロセス中に内部で使われたトークン(出力には含まれない)
Thinking tokens は Output tokens と同等の単価で課金されます。つまり、最終回答が150トークンでも、その裏で4,000トークンの thinking が走っていれば、実質的なコストは最終回答だけを見たときの約27倍になります。
まず現状の消費量を確認する
何かを変える前に、いまのリクエストが実際に何トークンの thinking を使っているかを測ります。
import google.generativeai as genai
genai.configure( api_key = "YOUR_GEMINI_API_KEY" )
model = genai.GenerativeModel( "gemini-2.5-pro" )
# thinking_budget を設定せずに代表的なリクエストを1本流す
response = model.generate_content(
"次のレビューの感情を分類してください: 「配送は遅れましたが、製品の品質は素晴らしいです。」"
)
if hasattr (response, "usage_metadata" ):
meta = response.usage_metadata
thinking_tokens = getattr (meta, "thoughts_token_count" , 0 ) or 0
output_tokens = meta.candidates_token_count or 0
print ( f "Output tokens: { output_tokens } " )
print ( f "Thinking tokens: { thinking_tokens } " )
print ( f "Ratio: { thinking_tokens / max (output_tokens, 1 ) :.1f } x" )
期待する出力例(未制御):
Output tokens: 32
Thinking tokens: 4817
Ratio: 150.5x
単純なリクエストで比率が10倍を超えているなら、大きな最適化余地があるサインです。
タスク複雑度と budget レベルを対応づける
全てのリクエストに同じ budget を設定するのは非効率です。「2+2は?」という計算と「複雑な法律文書の矛盾点を洗い出して」では、必要な思考量がまったく異なります。
個人開発での実運用を通じて、私は以下の4段階に分類するようになりました。
Level 0(思考オフ・budget=0) : 機械的な変換タスク。JSON整形、定型翻訳、テンプレート差し込み、単純検索
Level 1(軽量思考・budget=1,024〜4,096) : 軽い判断。感情分析、カテゴリ分類、短い要約
Level 2(中量思考・budget=8,192〜16,384) : 複数ステップの推論。コードのバグ分析、複数条件が絡むビジネスロジック設計
Level 3(フル思考・budget=16,384〜24,576) : 複雑な問題解決。アーキテクチャ判断、ニュアンスを含む文書分析、創造的な計画立案
基本的な thinking_budget の指定方法
import google.generativeai as genai
genai.configure( api_key = "YOUR_GEMINI_API_KEY" )
model = genai.GenerativeModel(
model_name = "gemini-2.5-pro" ,
generation_config = genai.GenerationConfig(
thinking_config = genai.types.ThinkingConfig(
thinking_budget = 4096 # Level 1: 軽量思考
)
)
)
response = model.generate_content(
"このレビューの感情を分類してください: 「良い製品です。発送も早かったです。」"
)
if hasattr (response, "usage_metadata" ):
meta = response.usage_metadata
thinking_tokens = getattr (meta, "thoughts_token_count" , 0 ) or 0
print ( f "Input tokens: { meta.prompt_token_count } " )
print ( f "Output tokens: { meta.candidates_token_count } " )
print ( f "Thinking tokens: { thinking_tokens } " )
期待する出力例:
Input tokens: 28
Output tokens: 9
Thinking tokens: 211
ここで重要なのは、thinking_budget は「上限」であって「目標値」ではない点です。budget=4096 と指定しても、モデルが「211トークンで十分」と判断すれば、それ以上は使いません。必要以上に高い値を設定してもトークンを無駄にするわけではなく、モデルに余地を与えるだけ、という安全な性質を持っています。
動的タスク分類器を作る
実際のプロダクションでは、毎回手動で budget を割り当てるのは現実的ではありません。以下は、私が個人開発の API サーバーで使っている分類器です。
import google.generativeai as genai
import re
from dataclasses import dataclass
from typing import Literal
BudgetLevel = Literal[ "off" , "light" , "medium" , "full" ]
@dataclass
class TaskConfig :
level: BudgetLevel
thinking_budget: int
def classify_task (prompt: str ) -> TaskConfig:
"""
プロンプトの内容と長さから thinking budget レベルを自動判定する
ヒューリスティック分類器。
"""
word_count = len (prompt.split())
# Level 3: フル思考が必要なキーワード
full_thinking_keywords = [
"設計" , "アーキテクチャ" , "最適化" , "比較して分析" , "矛盾" , "トレードオフ" ,
"design" , "architecture" , "optimize" , "analyze" , "evaluate" , "complex"
]
# Level 2: 中量思考が必要なキーワード
medium_thinking_keywords = [
"バグ" , "エラー" , "なぜ" , "理由" , "計画" , "デバッグ" ,
"bug" , "error" , "why" , "reason" , "plan" , "strategy"
]
# Level 0: 思考不要な単純タスクのパターン
simple_task_patterns = [
r " ^ 翻訳 [ :: ] " ,
r "以下のJSONを整形" ,
r " ^ 要約 [ :: ]. {0,20} $ " ,
r " ^\d + \s * [ \+\-\*\/ ]\s * \d + " ,
]
for pattern in simple_task_patterns:
if re.search(pattern, prompt):
return TaskConfig( level = "off" , thinking_budget = 0 )
if any (kw in prompt for kw in full_thinking_keywords) or word_count > 300 :
return TaskConfig( level = "full" , thinking_budget = 20480 )
if any (kw in prompt for kw in medium_thinking_keywords) or word_count > 100 :
return TaskConfig( level = "medium" , thinking_budget = 8192 )
return TaskConfig( level = "light" , thinking_budget = 2048 )
def generate_with_adaptive_budget (prompt: str , verbose: bool = False ) -> str :
"""タスク複雑度に応じた thinking budget で生成する。"""
genai.configure( api_key = "YOUR_GEMINI_API_KEY" )
task_config = classify_task(prompt)
if verbose:
print ( f "Task level: { task_config.level } (budget: { task_config.thinking_budget } )" )
model = genai.GenerativeModel( "gemini-2.5-pro" )
if task_config.thinking_budget > 0 :
gen_config = genai.GenerationConfig(
thinking_config = genai.types.ThinkingConfig(
thinking_budget = task_config.thinking_budget
)
)
else :
gen_config = genai.GenerationConfig()
response = model.generate_content(prompt, generation_config = gen_config)
if verbose and hasattr (response, "usage_metadata" ):
meta = response.usage_metadata
thinking = getattr (meta, "thoughts_token_count" , 0 ) or 0
print ( f "Tokens: { meta.prompt_token_count } in / "
f " { meta.candidates_token_count } out / { thinking } think" )
return response.text
分類器を複雑度ごとに検証する:
test_cases = [
( "翻訳:Hello, world!" , "off" ),
( "PythonのGILがなぜ真の並列性を妨げるのか教えてください" , "medium" ),
( "高書き込みのSaaSでイベントソーシングとCQRSのアーキテクチャのトレードオフを比較して分析してください" , "full" ),
]
for prompt, expected_level in test_cases:
config = classify_task(prompt)
match = "OK" if config.level == expected_level else "MISMATCH"
print ( f "[ { match } ] Expected= { expected_level } , Got= { config.level } | { prompt[: 30 ] } " )
Before/After — 実際の数値でコスト差を比較する
個人開発のアプリで、月間10,000リクエストを処理するケースで試算します。
Before(thinking_budget 未設定)
デフォルト挙動で平均8,000 thinking tokens/リクエストと仮定します。
平均Output: 200 tokens/リクエスト → 2,000,000 tokens/月
平均Thinking(未制御): 約8,000 tokens/リクエスト → 80,000,000 tokens/月
Thinking tokens が Output tokens の40倍 — コストに対して非常に大きな乗数です。
After(動的 budget 分類器を適用)
汎用アシスタント的なリクエスト分布を仮定します。
Level 0(全体の40%): 平均Thinking 0 tokens
Level 1(全体の35%): 平均Thinking 約600 tokens
Level 2(全体の20%): 平均Thinking 約2,800 tokens
Level 3(全体の5%): 平均Thinking 約11,000 tokens
加重平均 = (0.40×0) + (0.35×600) + (0.20×2,800) + (0.05×11,000) = 1,320 thinking tokens/リクエスト
月間Thinking tokens は 13,200,000 — 未制御の 80,000,000 と比較して 約84%削減 です。
この数値はあくまで試算ですが、私のアプリでも API コストが約60〜70%削減されたことを確認しています。ユーザーの実際のクエリ分布によって効果は変わりますが、方向性としての改善は一貫しています。
ストリーミングと thinking_budget の組み合わせ
応答を逐次返すインターフェースでは、ストリーミングを使いながら budget も制御したいケースがあります。
import google.generativeai as genai
genai.configure( api_key = "YOUR_GEMINI_API_KEY" )
def stream_with_budget (prompt: str , thinking_budget: int = 4096 ) -> str :
"""
thinking_budget を指定しながらストリーミング生成する。
thinking tokens はストリームに含まれない。
使用量は resolve() 後に取得する。
"""
model = genai.GenerativeModel( "gemini-2.5-pro" )
gen_config = genai.GenerationConfig(
thinking_config = genai.types.ThinkingConfig(
thinking_budget = thinking_budget
)
)
stream = model.generate_content(prompt, generation_config = gen_config, stream = True )
collected = []
for chunk in stream:
if chunk.text:
print (chunk.text, end = "" , flush = True )
collected.append(chunk.text)
print ()
try :
stream.resolve()
thinking = getattr (stream.usage_metadata, "thoughts_token_count" , 0 ) or 0
print ( f "Thinking tokens used: { thinking :, } " )
except Exception :
pass
return "" .join(collected)
stream_with_budget(
"セッションストレージにおけるRedisとMemcachedのトレードオフを教えてください" ,
thinking_budget = 8192
)
知っておくべき挙動 : thinking tokens はストリーム開始前に処理されます。ユーザーから見ると、思考なしのリクエストに比べて最初のトークンまでの待ち時間がわずかに長くなります。レイテンシに敏感なUIでは、この間「考えています...」のインジケータを出すと体験が安定します。
本番環境での監視 — thinking tokens の暴走を未然に防ぐ
どんな分類器も全てのエッジケースは捌けません。以下は、私が本番で動かしている監視レイヤーです。
import time
from collections import defaultdict
from typing import Optional
import google.generativeai as genai
class ThinkingTokenMonitor :
"""
thinking token の消費を追跡し、異常を検知するモニター。
個人開発でも回せるよう軽量に保つ。
"""
def __init__ (
self,
hourly_limit: int = 500_000 ,
single_request_limit: int = 20_000 ,
alert_callback = None
):
self .hourly_limit = hourly_limit
self .single_request_limit = single_request_limit
self .alert_callback = alert_callback or self ._log_alert
self .hourly_counts = defaultdict( int )
self .request_log = []
def _log_alert (self, message: str ):
# 実際は Slack Webhook やメール等に置き換える
print ( f "ALERT [ { time.strftime( '%Y-%m- %d %H:%M:%S' ) } ]: { message } " )
def record (
self,
thinking_tokens: int ,
output_tokens: int ,
budget_level: str ,
prompt_snippet: Optional[ str ] = None
):
hour_key = int (time.time() // 3600 )
self .hourly_counts[hour_key] += thinking_tokens
self .request_log.append({
"ts" : time.time(),
"thinking" : thinking_tokens,
"output" : output_tokens,
"level" : budget_level
})
if self .hourly_counts[hour_key] > self .hourly_limit:
self .alert_callback(
f "Hourly thinking token limit exceeded. "
f "Used: { self .hourly_counts[hour_key] :, } / Limit: { self .hourly_limit :, } . "
f "Last prompt: { prompt_snippet or 'N/A' } "
)
if thinking_tokens > self .single_request_limit:
self .alert_callback(
f "Single request used { thinking_tokens :, } thinking tokens. "
f "Prompt: { prompt_snippet or 'N/A' } "
)
def summary (self) -> dict :
if not self .request_log:
return { "status" : "no data" }
total_thinking = sum (r[ "thinking" ] for r in self .request_log)
total_output = sum (r[ "output" ] for r in self .request_log)
n = len ( self .request_log)
level_dist = defaultdict( int )
for r in self .request_log:
level_dist[r[ "level" ]] += 1
return {
"total_requests" : n,
"avg_thinking_tokens" : round (total_thinking / n, 1 ),
"avg_output_tokens" : round (total_output / n, 1 ),
"thinking_to_output_ratio" : round (total_thinking / max (total_output, 1 ), 2 ),
"level_distribution" : dict (level_dist)
}
# 分類器と組み合わせる
monitor = ThinkingTokenMonitor( hourly_limit = 200_000 )
def monitored_generate (prompt: str ) -> str :
task_config = classify_task(prompt)
genai.configure( api_key = "YOUR_GEMINI_API_KEY" )
model = genai.GenerativeModel( "gemini-2.5-pro" )
gen_config_kwargs = {}
if task_config.thinking_budget > 0 :
gen_config_kwargs[ "thinking_config" ] = genai.types.ThinkingConfig(
thinking_budget = task_config.thinking_budget
)
response = model.generate_content(
prompt,
generation_config = genai.GenerationConfig( ** gen_config_kwargs) if gen_config_kwargs else None
)
if hasattr (response, "usage_metadata" ):
meta = response.usage_metadata
thinking_tokens = getattr (meta, "thoughts_token_count" , 0 ) or 0
monitor.record(
thinking_tokens = thinking_tokens,
output_tokens = meta.candidates_token_count or 0 ,
budget_level = task_config.level,
prompt_snippet = prompt[: 80 ]
)
return response.text
分類器を運用データでチューニングする
キーワードベースの分類器は出発点としては機能しますが、あらゆるドメインで最適とは限りません。本番データをもとに改善する手順を示します。
まず、初日から構造化ログを残します。
import json
import time
def log_request (prompt: str , task_config, usage_metadata):
"""分類器チューニング用にリクエスト詳細をログ出力する。"""
thinking_tokens = getattr (usage_metadata, "thoughts_token_count" , 0 ) or 0
output_tokens = getattr (usage_metadata, "candidates_token_count" , 0 ) or 0
budget_utilization = thinking_tokens / max (task_config.thinking_budget, 1 )
log_entry = {
"ts" : time.time(),
"classified_level" : task_config.level,
"thinking_budget" : task_config.thinking_budget,
"thinking_tokens_used" : thinking_tokens,
"budget_utilization" : round (budget_utilization, 3 ),
"prompt_snippet" : prompt[: 100 ]
}
log_path = f "/tmp/budget_log_ { time.strftime( '%Y-%m- %d ' ) } .jsonl"
with open (log_path, "a" ) as f:
f.write(json.dumps(log_entry, ensure_ascii = False ) + " \n " )
2週間ほどデータを溜めたら、2つのパターンを探します。
過剰割り当てのリクエスト — Level 2 や Level 3 に分類されたのに、実際の thinking tokens が budget の20%未満だったもの。これらはより低いレベルで捌けたはずです。
過少割り当てのリクエスト — thinking tokens が budget 上限の95%以上に張り付いたもの。タスクが本来必要とする以上にモデルが制約されていた可能性があります。
両グループの prompt_snippet を眺めると、分類器が取り違えている言い回しのパターンが見えてきます。それに合わせてキーワードリストを更新します。この反復を月次で回すことが、机上のモデルではなく実際のユーザー行動に合った分類器を育てます。
モデルルーティング — thinking_budget だけでは足りない領域
thinking tokens をうまく管理できるようになると、その手前にもう一つの判断があることに気づきます。そもそもどのモデルを使うか、です。thinking_budget は Gemini 2.5 Pro 内部の推論の深さを制御しますが、最も単純なリクエストでは、budget をいくら下げても Gemini 2.5 Flash の方が適切な場合があります。
2つのモデルのルーティングを、私は次のように考えています。
Gemini 2.5 Flash を使う場面 : タスクが Level 0〜1 で、レイテンシとコストの最小化が最優先のとき。Flash は Pro よりトークン単価が明確に安く、Pro レベルの言語理解を要しないタスクなら品質差はほぼ無視できます。
Gemini 2.5 Pro を低 budget で使う場面 : Pro レベルの指示追従や言語品質は欲しいが、重い多段推論は不要なとき。Pro に thinking_budget=1024 を設定すると、Flash では出せない出力品質を、未制御の Pro よりはるかに低いコストで得られることが多いです。
Gemini 2.5 Pro を中〜フル budget で使う場面 : 複雑な推論の正確さが本当に重要で、コスト差が提供価値で正当化できるとき。
この3層ルーティング(単純は Flash・標準は Pro低budget・複雑は Pro高budget)を、いまは全アプリで使っています。初期設定は半日程度で、その後のコスト削減は恒久的に効きます。「どのモデルを使うべきか」から「このリクエストが実際に必要とする推論レベルは何か」へと、問いの立て方が変わるのが大きな収穫でした。
なお、Flash は Pro と同じ形では thinking_config を扱いません(2026年5月時点)。Flash では thinking 設定を省略するのが適切で、もともと低レイテンシ向けに最適化されています。機能は速く進化するので、ルーティング実装前に公式ドキュメントで最新のモデル能力を確認してください。
リクエスト種別ごとに budget の効き方は異なる
数週間の観察でようやく腑に落ちたのは、thinking_budget が全リクエストに等しく効くわけではない、という点です。budget と出力品質の関係は線形ではなく、タスク種別によって大きく変わります。
分類・感情分析タスク では、品質曲線が早い段階で平坦化します。budget=0 から budget=1024 への変化ははっきり効きますが、同じ分類タスクで budget=4096 から budget=16384 に上げてもほぼ差は出ません。単純分類に高い budget を設定するのは純粋な無駄です。
コードのデバッグ・分析タスク では、品質曲線がずっと急です。ある関数がなぜ誤った出力を返すかを budget=2048 で問うと、正しいが表層的な答えが返りがちです。同じ問いを budget=12288 にすると、コールスタックを辿ってエラーを追い、見落としていた相互作用まで指摘する深い分析になります。こうしたタスクでは、追加の thinking tokens が確かにコストに見合います。
創造的・自由記述タスク では、関係はより複雑です。budget が高いほど良い創作になるとは限らず、むしろ過剰に身構えた応答になることもあります。これらでは budget=4096 を上限の目安とし、プロンプトが明示的に構造化された分析を求める場合のみ budget=8192 を充てています。
検索・事実照会タスク では、thinking tokens はほぼ価値を生みません。答えが学習データ内に素直に存在する問いなら、budget=0 が正解です。事実の想起は thinking tokens を増やしても正確にはならず、関係のないエッジケースを考えてトークンを費やすだけです。
日本語・多言語リクエストの注意
日本語と英語の両方のユーザーを抱える私のアプリで、ひとつ興味深い挙動に気づきました。日本語のリクエストは、同じ budget レベルでも英語の同等リクエストよりわずかに多くの thinking tokens を消費する傾向があります。言語固有の処理に追加のトークンを費やしているのではないか、というのが私の仮説です。公式にどこにも記載は見当たりませんが、ログ上のパターンが一貫しているため、分類器では日本語リクエストに20%の budget バッファを上乗せしています。非英語ユーザーを抱える方は、ご自身の環境で検証してみる価値があると思います。
よくあるハマりどころと対処法
「budget を下げたら回答の質が落ちた」場合 — 多くは、分類器が必要以上に低いレベルを割り当てています。不完全・的外れに見える応答をログから抽出し、分類レベルまで遡って、キーワードリストが取りこぼしている言い回しを探します。また thinking_budget=0(思考オフ)は、本当に単純なタスクのみに限定してください。ニュアンスの複雑な文章は、軽い感情分析でも最低512〜1,024程度の budget を確保する方が安定します。
「usage_metadata が取得できない」場合 — SDK のバージョンによって属性名が異なることがあります。安全な取得方法を使ってください。
meta = response.usage_metadata
thinking_tokens = (
getattr (meta, "thoughts_token_count" , None ) or
getattr (meta, "thinking_token_count" , None ) or # 古いSDKの命名
0
)
実際に試したところ、google-generativeai 0.8 以降では thoughts_token_count が安定して使えますが、それ以前のバージョンでは属性が存在しないことがあります。SDK を最新に保つことを強くお勧めします。
「本番でモデルが見つからないエラー」が出る場合 — gemini-2.5-pro-latest ではなく gemini-2.5-pro を使ってください。-latest エイリアスは、モデルのバージョン移行時に一時的に 404 を返すことがあります(Gemini API のエイリアスと正しいバージョン指定参照)。
コストダッシュボードへ統合する
分類器と監視を入れたら、次に効くのは時系列でのコスト構造の可視化です。簡素なダッシュボードでも、ドリフトの早期警告になります。
私が毎日見ている指標はシンプルです。
平均 thinking-to-output 比 は、分類器が機能しているかを教えてくれます。動的 budget を導入したとき、メインアプリでこの比は約40:1から約6:1に下がりました。これが再び上がり始めたら、分類器が取り違える新しい種類のリクエストが現れたサインです。
レベル分布 は、ユーザー層の変化を示します。Level 3(フル思考)が日次トラフィックの5%から15%へ跳ねたら、何かが変わっています。複雑なユースケースを呼ぶ機能を足したか、誤分類を招くプロンプトパターンが出てきたかのいずれかです。
95パーセンタイルの thinking token 数 は、外れ値の検知において平均より有用です。少数の非常に高価なリクエストは平均を誤った方向に歪めます。私は95パーセンタイルに、時間合計より保守的な別アラート閾値を設けています。
JSONL ログから読み出す週次レポート生成器です。
import json
import time
import glob
from collections import defaultdict
def generate_weekly_report (log_dir: str = "/tmp" ) -> dict :
"""過去7日間の thinking token 使用量を集計する。"""
seven_days_ago = time.time() - ( 7 * 24 * 3600 )
all_entries = []
for log_file in glob.glob( f " { log_dir } /budget_log_*.jsonl" ):
with open (log_file) as f:
for line in f:
try :
entry = json.loads(line.strip())
if entry.get( "ts" , 0 ) > seven_days_ago:
all_entries.append(entry)
except json.JSONDecodeError:
continue
if not all_entries:
return { "status" : "no data in past 7 days" }
n = len (all_entries)
total_thinking = sum (e[ "thinking_tokens_used" ] for e in all_entries)
total_output = sum (e[ "output_tokens" ] for e in all_entries)
level_dist = defaultdict( int )
for e in all_entries:
level_dist[e[ "classified_level" ]] += 1
thinking_values = sorted (e[ "thinking_tokens_used" ] for e in all_entries)
p95_idx = int ( 0.95 * n)
p95_thinking = thinking_values[p95_idx] if thinking_values else 0
return {
"total_requests" : n,
"avg_thinking_tokens" : round (total_thinking / n, 1 ),
"thinking_to_output_ratio" : round (total_thinking / max (total_output, 1 ), 2 ),
"p95_thinking_tokens" : p95_thinking,
"level_distribution" : {level: count for level, count in sorted (level_dist.items())}
}
このレポートは10秒もあれば確認でき、コスト制御が効いているかが即座に分かります。私は毎週月曜の朝、アプリ関連で他の何を見るより先にこれを回しています。
budget を間違えると実際にどうなるか
設定を誤ったときの挙動を具体的に知っておくと役立ちます。失敗の出方が大きく違うからです。
複雑なリクエストに低すぎる budget は、正しく見えて重要なニュアンスを欠いた応答を生みます。コードのデバッグなら、一つのバグは正しく特定しつつ、関連する二つ目を見落とす — 実行経路を最後まで辿る思考の余地がなかったためです。応答は一見問題なく見えます。問題は、書かれていない部分にあります。
私はこれを直接経験しました。コストを切り詰めようと thinking_budget=2048 を全体の上限に設定していた時期です。あるユーザーから「メモリリークの修正が、最初は動いたが別のクラッシュを引き起こした」と報告を受けました。同じプロンプトを budget=12288 で流し直すと、モデルは目先のリークと、後で表面化する根本原因の両方を特定しました。2048トークンの応答は安かったものの、ユーザーに追加のデバッグ時間を強い、私にはサポート対応のコストとして返ってきました。
単純なリクエストに高すぎる budget は、誤答は生みません。生むのは「高価な正答」です。「どのJSONキーをリネームすべきか」に budget=20480 を充てれば、モデルは正しくリネームします — まったく無関係なエッジケースを数千トークンかけて考えた後で。出力は budget=0 と同一で、違うのは請求額だけです。
この非対称性は、不確実なときの構え方を決めます。過少割り当ては、すぐには気づかない品質問題を起こします。過剰割り当ては、使用量ダッシュボードに即座に現れるコスト問題しか起こしません。新種のリクエストで適正な budget が読めないときは、必要と思う値より少し高めに振り、観測した品質をもとに下げていく方が安全です。高すぎる budget で1週間過ごすコストは、過小な応答が招くサポート1件のコストより、ほぼ常に小さく済みます。
バッチ処理への応用
ここまではリアルタイムのリクエスト/レスポンスを前提にしてきました。大規模データセットをオフラインで回すバッチ処理は、少し違うアプローチが効きます。
バッチでは入力分布が事前に分かるため、コスト計算がより予測可能です。大きなバッチの前に、私は入力の1%をサンプリングして分類・生成を回し、本番投入前に想定トークンコストを外挿します。
import random
from typing import List
def estimate_batch_cost (
prompts: List[ str ],
sample_rate: float = 0.01 ,
cost_per_1k_output: float = 0.012 ,
cost_per_1k_thinking: float = 0.012
) -> dict :
"""
本番投入前にバッチ処理コストを見積もる。
プロンプトの一部をサンプリングし、分類して外挿する。
"""
sample_size = max ( 10 , int ( len (prompts) * sample_rate))
sample = random.sample(prompts, min (sample_size, len (prompts)))
level_counts = { "off" : 0 , "light" : 0 , "medium" : 0 , "full" : 0 }
for prompt in sample:
config = classify_task(prompt)
level_counts[config.level] += 1
# 各レベルの典型的な消費量から thinking tokens を見積もる
avg_thinking_by_level = { "off" : 0 , "light" : 600 , "medium" : 3000 , "full" : 12000 }
avg_output_per_request = 300 # ユースケースに応じて調整
total = len (sample)
weighted_thinking = sum (
(count / total) * avg_thinking_by_level[level]
for level, count in level_counts.items()
)
total_prompts = len (prompts)
estimated_thinking_tokens = weighted_thinking * total_prompts
estimated_output_tokens = avg_output_per_request * total_prompts
estimated_cost = (
(estimated_thinking_tokens / 1000 ) * cost_per_1k_thinking +
(estimated_output_tokens / 1000 ) * cost_per_1k_output
)
return {
"total_prompts" : total_prompts,
"sample_size" : len (sample),
"level_distribution" : {
level: round (count / total * 100 , 1 ) for level, count in level_counts.items()
},
"estimated_thinking_tokens" : int (estimated_thinking_tokens),
"estimated_output_tokens" : estimated_output_tokens,
"estimated_cost_usd" : round (estimated_cost, 4 )
}
# 例: 50,000件のアプリレビューを処理する前に見積もる
reviews = [ "素晴らしいアプリ!" , "起動時にクラッシュする" , "もっと速くなると良い" ] # 実データに置き換える
estimate = estimate_batch_cost(reviews * 1000 ) # 3,000件を模擬
print ( f "Estimated cost for { estimate[ 'total_prompts' ] :, } requests: " )
print ( f " Thinking tokens: { estimate[ 'estimated_thinking_tokens' ] :, } " )
print ( f " Cost: $ { estimate[ 'estimated_cost_usd' ] :.4f } USD" )
大きなバッチの前にこの事前見積りを回しておくことで、高額な驚きを何度も回避できました。50,000件のデータセット(1%サンプリング)で30秒ほど、続行するか・分類器を調整するか・そもそもアプローチを練り直すかを判断するのに十分な情報が得られます。
thinking_budget の設計で学んだこと
個人開発でコスト管理の失敗を何度も経験してきた身として、thinking_budget は「強力なデフォルトが、コスト安全なデフォルトとは限らない」という典型例だと感じています。モデルが1リクエストあたり24,576 thinking tokens を使える能力を持ち、何も指定しなければ、タスクに値すると判断したときにそれを使います。能力としては見事ですが、月末の請求書としては手強い相手です。
このパラメータが面白いのは、推論モデル一般について示してくれることです。同じモデル・同じプロンプトでも、考える余地をどれだけ与えるかで出力品質が有意に変わります。これは単なるコストのつまみではなく、品質のつまみでもあります。その関係を理解して向き合うことが、Gemini 2.5 Pro を「意図して使う」か「高くつかせる」かの分かれ目になります。
ここで紹介したパターンは、実トラフィックを持つ複数のアプリで検証したものです。まず4段階の分類器をそのまま起点とし、最初からリクエストを計測し、実際のユーザーの送信内容をもとにチューニングしてください。固定の設定より、データに基づく反復のほうがずっと頼りになります。監視コードはそのまま本番投入できる形です。最初の超過通知を待つのではなく、最初から組み込んでおくことをお勧めします。