個人開発で運用しているアプリの、夜間に走らせている問い合わせ分類のジョブが、ある朝から先月比で請求を倍近く押し上げていました。ログを追うと、モデルを Opus 4.8 に上げたついでに budget_tokens を「余裕を持たせて」32,000 に固定したのが効いていました。精度指標はほとんど動いていません。深く考えさせた分だけ請求が増え、答えは変わらない。思考に払ったお金の大半が、結論に届いていなかったわけです。
この記事は、その後 budget を根拠のある値に落とし込むまでに手元で組んだ計測の記録です。要点は一つで、budget は「大きいほど安全」でも「小さいほど得」でもありません。1段上げたときに正解率がどれだけ伸びるかを測れば、払うべき額はほぼ自動的に決まります。
budget_tokens は上限であって固定費ではない
最初に誤解を解いておきます。budget_tokens はモデルが内部思考に使ってよいトークンの上限であり、毎回この値を消費するわけではありません。簡単なタスクなら数百トークンで思考を切り上げ、難しいタスクのときだけ上限近くまで伸びます。
| 設定 | 誤解 | 実際の挙動 |
| budget=32,000 | 毎回32,000トークン分課金される | 難タスクに遭遇したときだけ深く使う。平易な入力では数百で止まる |
| budget=4,000 | 常に安く済む | 難タスクで思考を打ち切り、浅い結論を返すことがある |
つまり budget を上げる行為は「難タスクに遭遇したときに深く考えてよい余裕を渡すこと」です。ここを取り違えると、「請求が怖いから一律で小さくする」判断で、本来解けるはずの問題を解けないモデルに変えてしまいます。逆に、私のように「怖いから一律で大きく」しても、平易なタスクでは余裕が使われず、難タスクでだけじわじわ効いてくる。どちらの固定値も、計測なしでは外します。
from anthropic import Anthropic
client = Anthropic()
resp = client.messages.create(
model="claude-opus-4-8",
max_tokens=8000,
thinking={"type": "enabled", "budget_tokens": 10000}, # 思考に使ってよい上限
messages=[{"role": "user", "content": "次の SQL を最適化してください: ..."}],
)
# 実際に消費された思考トークンを必ず記録する
usage = resp.usage
print(usage.thinking_tokens, usage.output_tokens)
response.usage.thinking_tokens が実消費量です。ここを毎リクエスト残すかどうかで、後のチューニングが「計測」になるか「勘」になるかが分かれます。
まず思考トークンを台帳に残す
budget を触る前に、本番の思考消費を台帳化します。平均だけを見るとロングテールを見落とすので、p50・p95・到達率の3つを残してください。到達率とは、thinking_tokens が budget の 95% 以上に達したリクエストの割合です。ここが高いほど「思考を途中で打ち切られている」疑いが強まります。
import json, time, statistics
from pathlib import Path
LEDGER = Path("thinking_ledger.jsonl")
def call_and_log(client, *, model, budget, messages, task_type):
resp = client.messages.create(
model=model, max_tokens=8000,
thinking={"type": "enabled", "budget_tokens": budget},
messages=messages,
)
u = resp.usage
rec = {
"ts": time.time(), "task_type": task_type,
"budget": budget,
"thinking_tokens": u.thinking_tokens,
"output_tokens": u.output_tokens,
"hit": u.thinking_tokens >= budget * 0.95, # 上限に張り付いたか
}
with LEDGER.open("a") as f:
f.write(json.dumps(rec) + "\n")
return resp
def summarize(task_type):
rows = [json.loads(l) for l in LEDGER.read_text().splitlines()]
rows = [r for r in rows if r["task_type"] == task_type]
tt = sorted(r["thinking_tokens"] for r in rows)
if not tt:
return None
p50 = statistics.median(tt)
p95 = tt[min(len(tt) - 1, int(len(tt) * 0.95))]
hit_rate = sum(r["hit"] for r in rows) / len(rows)
return {"n": len(rows), "p50": p50, "p95": p95,
"max": tt[-1], "hit_rate": round(hit_rate, 3)}
# 例: {"n": 214, "p50": 1180, "p95": 5200, "max": 9900, "hit_rate": 0.02}
台帳が100件を超えたあたりから、タスク種別ごとの輪郭が見えてきます。分類タスクで budget=32,000 を設定していたのに p95 が 5,200 だった、というのはよくある光景です。残りの 26,800 は「使われないかもしれない上限」として抱えているだけで、平易な入力では課金もされません。問題は、難タスクが混じったときにだけ深く使い、その深さが答えに効いていない場合です。それを次に測ります。
budget を1段上げたときの「限界効用」を測る
到達率と分布だけでは、「深く考えた結果、答えが良くなったか」は分かりません。これは正解率の差でしか判定できません。そこで、評価用データセットを固定し、budget を段階的に変えて同じタスクを流し、1,000 思考トークンあたり正解率が何ポイント伸びたかを限界効用として出します。
def evaluate(client, dataset, *, model, budget, judge):
"""dataset: [{"messages": [...], "gold": ...}], judge(pred, gold)->bool"""
correct = 0
thinking_sum = 0
for ex in dataset:
resp = client.messages.create(
model=model, max_tokens=4000,
thinking={"type": "enabled", "budget_tokens": budget},
messages=ex["messages"],
)
pred = resp.content[-1].text
thinking_sum += resp.usage.thinking_tokens
correct += judge(pred, ex["gold"])
n = len(dataset)
return {"budget": budget, "acc": correct / n,
"avg_thinking": thinking_sum / n}
def marginal_utility(results):
"""budget 昇順の結果から、1000思考トークンあたりの正解率の伸びを出す"""
out = []
for prev, cur in zip(results, results[1:]):
d_acc = cur["acc"] - prev["acc"]
d_think = max(1.0, cur["avg_thinking"] - prev["avg_thinking"])
out.append({
"from": prev["budget"], "to": cur["budget"],
"d_acc_pts": round(d_acc * 100, 2),
"per_1k": round(d_acc / d_think * 1000 * 100, 3), # pt / 1k think tok
})
return out
grid = [2000, 4000, 8000, 16000, 32000]
results = [evaluate(client, dataset, model="claude-opus-4-8", budget=b, judge=judge)
for b in grid]
for step in marginal_utility(results):
print(step)
手元の分類ワークロードで出た値は、たとえばこうでした。
| 区間 | 正解率の伸び | 1,000思考トークンあたり |
| 2,000 → 4,000 | +4.1 pt | +2.3 pt |
| 4,000 → 8,000 | +1.2 pt | +0.5 pt |
| 8,000 → 16,000 | +0.3 pt | +0.08 pt |
| 16,000 → 32,000 | -0.1 pt | ほぼ 0 |
伸びは 4,000 を境に急速に平らになり、16,000 から上はノイズの範囲でした。32,000 で払っていた思考トークンは、正解率に対して何も返していなかったわけです。ここまで測れれば、budget を決めるのは判断ではなく読み取りになります。限界効用が実運用で許容できる下限(私は「1,000思考トークンあたり +0.2 pt」を目安に置きました)を割り込む一つ手前の budget を採用すれば、払いすぎも絞りすぎも避けられます。この基準では 4,000 を選ぶことになりました。
ログから budget を推薦する小さなスコアラー
評価は一度組めば使い回せますが、日々のジョブでは台帳から機械的に候補を出せると楽です。分布(p95)と到達率を組み合わせ、限界効用の判定結果を上書きに使う、という順で推薦します。
def recommend_budget(summary, mu_floor_budget=None):
"""summary: summarize() の出力, mu_floor_budget: 限界効用評価で決めた上限"""
p95 = summary["p95"]
hit = summary["hit_rate"]
# 基本は p95 の 1.3 倍。到達率が高いなら思考が詰まっているので広げる
base = p95 * 1.3
if hit > 0.10:
base = p95 * 1.8 # 打ち切りが多い → 余裕を足す
rec = int(round(base / 1000) * 1000)
# 限界効用が頭打ちする budget が分かっているなら、それで蓋をする
if mu_floor_budget is not None:
rec = min(rec, mu_floor_budget)
return max(1000, rec)
# 例: summarize→{"p95":5200,"hit_rate":0.02}, mu_floor_budget=4000 → 推薦 4000
分布から出した推薦(p95×1.3 ≒ 6,800)と、限界効用から出した上限(4,000)がぶつかったときは、限界効用を優先します。分布は「どこまで思考が伸びうるか」を示すだけで、「その思考が答えに効いたか」までは語らないからです。到達率が 10% を超えるワークロードだけは例外で、思考の打ち切りが精度を削っている可能性があるため、蓋をかける前にもう一段上の budget で限界効用を測り直します。
到達率は高いのに精度が伸びないとき
やっかいなのは、到達率が高い(思考が上限に張り付いている)のに、budget を上げても正解率が伸びない場合です。これは budget 不足ではなく、タスクがそのモデルにとって難しすぎるサインのことがあります。切り分けは次の手順で行いました。
- budget を2倍にして評価データを流し直す
- 正解率が有意に伸びる → budget 不足。推薦値を引き上げる
- 正解率が横ばい、かつ到達率が高いまま → タスク過負荷。budget ではなく入力設計を疑う
3 に該当したときは、budget をいくら積んでも改善しません。入力を構造化する、選択肢を明示する、長い文脈を関連箇所だけに絞る、上位モデルに切り替える、といった別の打ち手に移ります。思考の上限を上げることは、考える余地がある問題にしか効かない、という当たり前のことを、到達率と正解率の組み合わせが教えてくれます。
コストは難タスクにだけ課金される — 帰属を1リクエスト単位で
過剰な budget のコストは固定ではなく、難タスクに遭遇したときだけ発生します。だからこそ「平均思考トークン × 単価」で概算すると実態を外します。1リクエストごとに実消費で帰属させておくと、どのタスク種別が請求を押し上げているかが見えます。
def request_cost(usage, *, in_rate, out_rate):
"""rate は 100万トークンあたりの単価(各自の契約値に置き換える)。
思考トークンは出力側として課金される点に注意。"""
input_cost = usage.input_tokens / 1_000_000 * in_rate
output_cost = (usage.output_tokens + usage.thinking_tokens) / 1_000_000 * out_rate
return round(input_cost + output_cost, 6)
# 月10万リクエスト、budget 32,000→4,000 で平均思考が 3,000 トークン減った場合、
# 出力単価を仮に $75/Mtok とすると:
# 10万 * 3,000 / 1e6 * 75 = 月 $22,500 の削減
# この額が限界効用の評価で失った正解率(この例では実質ゼロ)に見合うかで判断する
単価は契約やモデルで変わるので、必ずご自身の値に置き換えてください。大事なのは絶対額ではなく、削減額と限界効用の評価で失う正解率を天秤にかける構図です。個人開発の小さな運用でも、この帰属だけは早めに入れておくと後が楽になります。私の分類ジョブでは、失う正解率が測定誤差の範囲だったため、削減はそのまま利益になりました。
budget を上げるならタイムアウトとセットで
限界効用が明確に立っていて budget を上げる場合は、レスポンス時間も必ず一緒に設計します。深い思考には数十秒かかることがあり、HTTP クライアントの既定タイムアウトに引っかかります。クライアント側のタイムアウトを 90 秒以上に延ばし、長い思考には Streaming を使ってください。オフラインの大量処理なら、Batch API のコスト最適化ガイド と組み合わせると、高めの budget でも実コストを抑えられます。リアルタイム性が要らない処理は、ほぼすべて Batch に寄せる価値があります。
まずやること
次に budget_tokens に触れるときは、値を動かす前に本番ログの thinking_tokens を100件集め、p50・p95・到達率を出してください。そのうえで固定の評価データで budget を段階的に振り、1,000思考トークンあたりの正解率の伸びを1枚の表にする。この2つがそろえば、budget は決める対象ではなく読み取る対象に変わります。私自身、この計測を組むまでは「怖いから大きめ」で払い続けていました。測ってみて初めて、払っていた思考の大半が答えに届いていなかったと分かった次第です。
同じように請求と精度の関係で迷っている方の、手を動かす起点になれば幸いです。お読みいただきありがとうございました。