Claude Codeのトークン消費とプロンプトキャッシュを理解する

ブログ

claude-code-token-usage.avif

Claude Codeのトークン消費とプロンプトキャッシュを理解する


こちらの要点を簡潔にまとめます。

前提:単位はすべてトークン

API課金でもPro/Max/Team/Enterpriseの5時間・週次ウィンドウでも、消費しているのは常にトークンです。目的は「トークンを減らす」ことではなく、使うトークンを本当に必要な作業に向けることです。

リクエストの中身はプロンプトだけではない

Claude Codeにメッセージを送ると、実際には以下がすべて一緒に送信されます。

  • システムプロンプト、組み込みツール定義(read, edit, bashなど)
  • 作業ディレクトリやGit状態などの環境情報
  • CLAUDE.md
  • 設定されているスキルやMCPサーバー(名前と説明)
  • ユーザーのメッセージ

これらがトークナイザーを通ってトークン化され、モデルに渡されます。

3種類のトークン

  1. 入力トークン:モデルに送るすべてです。並列処理できるため単価は安くなっています。
  2. 出力トークン:モデルが生成するすべてです(要約だけでなく、ツール呼び出し・編集・思考過程も含みます)。1トークンずつ逐次生成するため計算コストが高く、単価も最も高くなっています。
  3. キャッシュトークン:リクエストの先頭部分(プレフィックス)で、前回と同一の部分です。サーバー側が内部状態を保持しているため再計算不要で、非常に安く済みます。

健全なセッションでは、入力の大半がこのキャッシュ由来になります。

コストを左右する4つのレバー

① モデル選択

大きいモデル(Opus、Fableなど)はトークンごとの計算量が多く単価が高くなります。Sonnetは優秀なジェネラリスト、Opusは専門家、Fableはニッチな問題に強い専門家、Haikuは明確に指示できる機械的作業向けです。

Fable → Opus → Sonnet → Haiku

② コンテキストの与え方

  • ファイル名をプロンプトに含めると、Claudeが自分でreadツールを呼ぶ往復を省けます
  • /contextでセッション開始前に何が読み込まれているか確認しましょう
  • CLAUDE.mdには「すべてのタスクに共通する内容」だけを置き、特定作業向けの指示はスキルに移します(スキルは必要な時だけ名前と説明から判断されて読み込まれます)
  • 使っていないMCPサーバーはオフにする、またはCLI(GitHub CLI、AWS CLIなど)で代替しましょう
  • コマンド出力のノイズに注意が必要です。大きすぎる出力は自動でファイル化されますが、閾値以下(例:テストの全行出力)はそのまま会話に残り続けます。CLAUDE.mdに「静かなフラグ付きの実行コマンド」を明記しておくと良いでしょう

③ Effortレベル

高いeffortほど出力トークンが増え、それに伴い入力トークンも増えます。結果が「知識不足」で失敗したならモデルを上げる、「やる気不足(手を抜いた)」で失敗したならeffortを上げる、という判断基準が使えます。

④ セッションの長さとキャッシュ管理

モデルはコンテキストに追記するだけで、勝手に削除しません。長いセッションほど過去のタスクを引きずり続けます。

  • /compact:会話全体を要約に置き換えます(ヒントも指定可能です)
  • /rewind:直近のターンを削除します
  • /clear:新しいタスクの前にリセットします

プロンプトキャッシュの仕組み

キャッシュは「先頭から前回と全く同じ部分(プレフィックス)」にのみ適用されます。1トークンでも変わると、そこから後ろは全部新規扱いになりフルプライスで処理されます。

キャッシュを壊す(invalidateする)アクション:

  • モデルの切り替え(/model)
  • effortレベルの変更(/effort)(Fable 5.1はeffort変更でキャッシュが切れない例外です)
  • fast modeのON切り替え
  • MCPサーバーの接続/切断(ツール定義がプレフィックスに含まれる場合)
  • ツールのdenyルール追加
  • /compact実行
  • Claude Codeのアップグレード

逆にキャッシュを壊さないアクション:

  • リポジトリ内のファイル編集
  • CLAUDE.mdの編集(ただし次の/clearか/compactまで反映されません)
  • output styleの変更
  • permission modeの変更
  • スキルやコマンドの呼び出し
  • /recap
  • /rewind
  • サブエージェントの起動

キャッシュの保持時間(TTL)

キャッシュは最後のリクエスト/レスポンスからの経過時間(非アクティブ時間)で判定されます。会話開始からの経過時間でも、キャッシュ作成からの固定時間でもありません。5分以内に次のリクエストを送り続けている限り、会話が何時間続いてもキャッシュは切れません。

リクエストの種類Claudeサブスクリプション(プラン内使用量)従量課金(Usage credits/APIキー/クラウドプロバイダ)
メイン会話1時間5分
それ以外(サブエージェント等)5分(一部サーバー制御ヘルパーは1時間)5分

Amazon Bedrock経由の場合はデフォルトで5分側に該当します。promptCacheTtl設定(または環境変数CLAUDE_CODE_PROMPT_CACHE_TTL)で5m/1hを選択できますが、1時間TTLはキャッシュ書き込み単価が高いため、こまめに作業していて5分以上の空白がほとんどない働き方であれば5分のままの方が総コストは安くなります。

/compactのタイミングが重要です

compactは会話全体を要約に置き換える処理ですが、要約を作るためにモデルが会話全体をもう一度読む必要があります。キャッシュが温かいうちにcompactすれば、その読み込みは安く済みます。キャッシュが切れた後(例:1時間放置後)にcompactすると、その同じ読み込みがフル入力価格になってしまいます。

翌日また同じセッションに戻ることが分かっているなら、離席前にcompactしておくと、翌日の再開時は短い要約だけで済みます。逆に、キャッシュが切れた古いセッションをresumeすると、最初のメッセージは会話全体を送り直すことになり一切キャッシュされません。

セッションが長くなるとヒット率は下がるのでしょうか?

直感に反しますが、答えは逆です。ターンが長くなるほど、通常はヒット率は上がっていきます。

  • ターン1:ほぼ全部が新規(cache write)です
  • ターン2:ターン1の内容はキャッシュ済みで、新規は直近のやり取りのみです
  • ターン10:新規部分の割合はさらに小さくなります

会話が積み重なるほど、入力全体に対する新規(未キャッシュ)部分の比率は小さくなるため、ヒット率は上昇する傾向にあります。ヒット率が下がる原因は「ターンの長さ」自体ではなく、上記の「キャッシュを壊すアクション」が起きた時です。

ただし絶対コストは増え続ける点に注意が必要です。ヒット率(比率)が上がっても、cache read自体もコストがかかるため、会話が長くなるほどcache read分の絶対コストは積み上がっていきます。

サブエージェントの活用

大きなログやリポジトリ全体、テスト結果などを調査させたいものの、その過程(読み込んだ大量のファイルなど)はメインの会話に残したくない場合、サブエージェントに調査を任せると、結論だけがメイン会話に返り、詳細はサブエージェント側のコンテキストに留まります。サブエージェント自体にも入出力・キャッシュコストはかかりますが(TTLは常に5分です)、メインセッションを小さく保てるというメリットがあります。モデルやeffortもサブエージェントごとに個別設定できます。

/usage(/cost)でキャッシュ状態を確認する

セッション内で/usageまたは/cost(エイリアス)を実行すると、「Session」ブロックにPrompt cache (main)という行が表示されます(Claude Code v2.1.251以降)。

Prompt cache (main):   20 requests · 95% of input tokens from cache · no misses · warm (1h TTL, last activity 8m 39s ago)

表示される内容は以下の通りです。

  • リクエスト数
  • キャッシュ由来の入力トークン比率(ヒット率)
  • ミス回数
  • 現在キャッシュが温かい(warm)か切れている(cold)か、TTLと最終アクティビティからの経過時間

v2.1.260以降ではミスの原因(例:tool definitions changed)まで表示されるようになっています。

判断基準は以下の通りです。

  • ヒット率90%台以上が健全な目安です
  • ミス回数はできれば0が望ましいです。複数回出ている場合はモデル切替・effort変更・MCPサーバーの接続断/再接続・/compactなどが起きたことを意味します
  • warm状態であることが重要です。coldの場合は次のリクエストがフル入力価格になってしまいます

チーム管理者向け(Managed Settings)

チーム全体でClaude Codeを運用する場合、以下を一箇所(managed settings)で設定できます。

  • デフォルトモデル・選択可能なモデル:高コストの原因は大抵「大きいモデルがデフォルトのまま放置」されていることです
  • デフォルトeffortレベル(Enterpriseでは上限設定も可能です)
  • プロンプトキャッシュTTL:チームの働き方(断続的 vs 長時間離席が多い)に応じて設定できます
  • 許可するMCPサーバーの制限、組織共有のCLAUDE.md
  • 分析ダッシュボード:ユーザー・モデル別の支出、OpenTelemetryによるキャッシュ利用率の可視化
  • 利用上限:5時間/週次ウィンドウの上限、Usage Creditsによる追加の支出上限設定

以下ビデオでは紹介されていませんでしたが、有効なtipsです。

ステータスラインでキャッシュ失効タイマーを表示する

/usageは都度開いて確認する必要がありますが、ステータスラインに常時表示しておけば、キャッシュが切れる前に気づくことができます。

Claude Codeのステータスラインスクリプトはstdinで以下のようなJSONを受け取ります(prompt_cacheオブジェクト、v2.1.251以降)。

"prompt_cache": {
  "warm": true,
  "caching_observed": true,
  "ttl": "1h",
  "expires_at": 1738429200,
  "requests": 14,
  "misses": 2,
  "hit_ratio": 0.91,
  ...
}

expires_at(エポック秒)と現在時刻の差分を計算すれば、キャッシュが失効するまでの残り秒数が分かります。これをcache_ttl_timerという名前のカウントダウンタイマーとしてステータスラインに実装しました。

#!/bin/bash
# cache_ttl_timer
input=$(cat)

MODEL=$(echo "$input" | jq -r '.model.display_name')
WARM=$(echo "$input" | jq -r '.prompt_cache.warm // false')
TTL=$(echo "$input" | jq -r '.prompt_cache.ttl // empty')
EXPIRES_AT=$(echo "$input" | jq -r '.prompt_cache.expires_at // empty')
CACHING_OBSERVED=$(echo "$input" | jq -r '.prompt_cache.caching_observed // false')

YELLOW='\033[33m'
GREEN='\033[32m'
GRAY='\033[90m'
RESET='\033[0m'

NOW=$(date +%s)

if [ "$CACHING_OBSERVED" != "true" ] && [ -z "$EXPIRES_AT" ]; then
    echo -e "[$MODEL] ${GRAY}⏳キャッシュ --${RESET}"
    exit 0
fi

if [ "$WARM" = "true" ] && [ -n "$EXPIRES_AT" ]; then
    REMAINING=$((EXPIRES_AT - NOW))

    if [ "$REMAINING" -gt 0 ]; then
        MINS=$((REMAINING / 60))
        SECS=$((REMAINING % 60))
        TIMER=$(printf "%02d:%02d" "$MINS" "$SECS")

        if [ "$REMAINING" -lt 60 ]; then
            echo -e "[$MODEL] ${YELLOW}⏳キャッシュ ${TIMER} (${TTL})${RESET}"
        else
            echo -e "[$MODEL] ${GREEN}⏳キャッシュ ${TIMER} (${TTL})${RESET}"
        fi
    else
        echo -e "[$MODEL] ${GRAY}⏳キャッシュ cold${RESET}"
    fi
else
    echo -e "[$MODEL] ${GRAY}⏳キャッシュ cold${RESET}"
fi

~/.claude/settings.json側の設定は以下のようになります。

{
  "statusLine": {
    "type": "command",
    "command": "~/.claude/cache-ttl-timer.sh",
    "refreshInterval": 1
  }
}

ステータスラインは通常イベント駆動(新しいメッセージが届いた時など)でしか更新されないため、会話が止まっている間もカウントダウンを動かし続けるにはrefreshInterval: 1の設定が必須です。残り60秒を切ると黄色に変わり、warmがfalseになるかexpires_atを過ぎると自動で「cold」表示に切り替わります。

なかなか有効なのでぜひ使ってみてください

一覧へ戻る