「Prompt Caching」とやらのイメージをざっくりと掴みたいんダッ....!
はじめに
そもそもPrompt Cachingてなんなんだよ一体ッ.....!?
"プロンプト"を"キャッシング"するのかッ....!?
むずかしい....!...むずかしく感じるッ....!
むずかしく感じる俺がおかしいのかッ.....!?
だが、"イメージ"を.....掴みたいんダッ....!!
1. Prompt caching ってそもそもなんなんだよ一体ッ...!?
⚠ 1 章はざっくり仕組みを掘り下げます。何が起きてるかだけ掴めば OK な人は2章へジャンプしてください。
1.1 前提 その1
LLM API は基本的に 1 リクエスト 1 レスポンスの仕組み (= stateless)。サーバー側で会話履歴を覚えてくれるわけではない。
なので「1 つの連続したやり取り」 を作るには、毎ターンごとに『過去の会話履歴を全部含めた prompt』 を組み立てて送信する 必要がある。普段使っている基盤モデルを利用したチャットサービスも、コーディングエージェントも基本的にはこの構造。
簡易的なイメージ
ターン 1: [system prompt] + [user 質問 1]
ターン 2: [system prompt] + [user 質問 1] + [assistant 応答 1] + [user 質問 2]
ターン 3: [system prompt] + [user 質問 1] + [assistant 応答 1] + [user 質問 2] + [assistant 応答 2] + [user 質問 3]
...
LLM 側はリクエストごとにプロンプト全部を読み直して計算する。前半が毎回同じでも毎回計算するので、連続したチャットやエージェントのようなマルチターンのやりとりでは無駄が大きい。
1.2 前提 その2
prompt caching を理解する範囲で、LLM が次のトークンを 1 つ出すまでの流れをざっくり ステップ化すると....
- プロンプトの全トークンを Transformer に流す
- 各トークンに対して内部表現 (Key / Value など) を計算
- 最終層で「次のトークンの確率分布」を出す
- その確率分布からサンプリングして 1 トークン選ぶ
- それを末尾に足して、2〜4 を繰り返す
実際は Multi-head Attention の複数 head の独立計算、FFN、Layer Norm、数十層の積み重ね等、もっと複雑な処理が走る。ここでは prompt caching の説明に必要な要素だけを抜き出している。
重い計算はステップ 1-2。プロンプトが長くなるほど、毎回ここでトークン数 × モデル層数の演算が走るので時間とコストを食う。
1.3 KV cache が再利用できる理由
仕組みの根っこは KV cache。Transformer の Attention には causal mask という制約があり、各トークンは「自分より前のトークン」しか見ない。だから先頭 N トークンに対して計算される Key / Value (= 各トークンの中間表現ベクトル) は、後ろに何が来ても変わらない。
[A][B][C][D][E][F]
↑ ← A の K,V は他のトークンに一切依存しない
↑ ← C の K,V は A,B にだけ依存。D,E,F には依存しない
↑ ← D の K,V は A,B,C にだけ依存。E,F には依存しない
つまり 「prompt の前半 [A][B][C] が同じ」なら、その分の K/V は再利用できる。これをサーバー側に保持しておき、次回リクエストで前半が一致したら計算をスキップする、これが prompt caching。
prompt caching が保存しているのは 前提 2 のステップ 2 で計算する Key / Value。前半が一致すれば、その分のステップ 1-2 をまるごとスキップできる (= 重い計算を毎回やらずに済む)。
普段僕たちが使っているChatGPTやClaude等のチャットサービスのイメージと照らし合わせると「同じ prompt なら同じ出力」のイメージにはならないと感じるはず。これは出力の揺らぎが最後の サンプリング段 (= ステップ 3-4) で確率的に選ばれるから。
その前段の K/V 計算 (= ステップ 2) は決定論的なので、cache はそこだけ保存する。つまり最終出力に影響するサンプリングの揺らぎとは独立。
ざっくりとしたイメージ
キャッシュ対象は最終出力ではなく、その手前のステップにある計算結果。
2. と、とりあえず、4 種類のトークンのイメージを掴みたいんダッ.....!!
各種基盤モデルAPIの料金表を見ると、input / output のほかに cache_write / cache_read という項目がある。
これらは何で、リクエストするとどう振り分けられるのかイメージを掴む。
2.1 大体4 種類の定義があるッ.....!!!!
| 種別 | 何 | 計上されるタイミング |
|---|---|---|
| input | 通常の入力トークン | 普通にリクエストを送ったとき |
| output | モデルが生成したトークン | 応答が返ってきたとき |
| cache_write | 「ここをキャッシュする」と宣言した入力 (初回書込み) | キャッシュ対象として宣言した部分を、初めて送ったとき |
| cache_read | キャッシュから読み出した入力 (2 回目以降) | 前回 cache_write した部分を、TTL 以内 (= キャッシュの寿命内) に同じ内容で再送したとき |
ざっくりとしたイメージ
通常のinput/outputに並んで、cache関連の読み書きのトークン計上がある。
2.2 TTL は「使うたびにリセットされる」んダッ....!!!!!
TTL = Time To Live = キャッシュの寿命。まず provider ごとの選択肢を整理:
- Anthropic Claude: 5 分 / 1 時間 の 2 択
- Vertex Gemini: 秒単位で指定可能
-
OpenAI: 暗黙キャッシュは標準で 5-10 分程度 (breakpoint や TTL の明示制御はできない、
prompt_cache_keyでルーティングヒントは渡せる)。GPT-5.5/5.4 では Extended Prompt Caching で最大 24h まで延長可
Provider によって選択肢自体が変わる、とだけ頭の片隅に。
「TTL 5 分」 と聞くと「5 分経ったらキャッシュが消える」 と取りがちだが、実際は違う。アクセス (= cache_read) するたびに TTL がリセットされる 仕組み。
時刻 0:00 → cache_write (有効期限 0:05 にセット)
時刻 0:02 → cache_read (有効期限が 0:07 に延長)
時刻 0:04 → cache_read (有効期限が 0:09 に延長)
時刻 0:08 → cache_read (有効期限が 0:13 に延長)
時刻 0:12 → cache_read (有効期限が 0:17 に延長)
...
時刻 0:20 まで何もアクセスなし → 0:17 で失効、次の書込みからやり直し
TTL の意味は「最終アクセスから 5 分」 であって「キャッシュ作成から 5 分」 ではない。
1 タスクで数十ターン回るエージェントは、各ターン間隔が TTL より短ければ、最初に書込んだキャッシュが最後のターンまで生き残り続ける。
ざっくりとしたイメージ
- キャッシュには寿命 (TTL) がある
- TTLはcache_readでのアクセスのたびにリセットされる。TTL以上の間隔をあけずに使い続ける限り生き残る。
- TTLの設定は基盤モデルのproviderごとに差異がある
2.3 コストのイメージを掴みたいんダッ....!! (例: Claude Sonnet 4.6)
| 種別 | 単価 ($/Mtok) | 基準 (input) 比 |
|---|---|---|
| input | $3.00 | 1.00x |
| output | $15.00 | 5.00x |
| cache_write (5m TTL) | $3.75 | 1.25x |
| cache_write (1h TTL) | $6.00 | 2.00x |
| cache_read | $0.30 | 0.10x |
cache_write は基準より高い (5m で 1.25 倍、1h で 2 倍)。cache_read は基準の 1/10。
これがどう効いてくるかは、そのプロンプトが「1 つの連続したやり取り」の中で何回 read されるかで大きく変わる:
- マルチターンのチャット / エージェント (= 履歴を持った「1つの連続的なやり取り」を行うもの。Claude / ChatGPT 等のようなチャット、Claude Code 等のコーディングエージェント): system prompt + 過去ターンの履歴が後続ターンで繰り返し read される → 書込み代を払った分が以降 1/10 価格で取り戻せる
- ワンショット利用 (1 APIリクエストだけで使い捨て): そのやり取りの中で同じ prompt が 2 回目に read される機会がない → 書込み代だけ払って終わるので損
ざっくりとしたイメージ
- cache_write は割高、cache_read は激安。
- 同じ prompt が繰り返し read されるほどお得。
2.4 トークンは 3 つに振り分けられるんダッ.....!!
リクエストを送ると、サーバー側でプロンプトの各トークンを 必ず 1 つだけ のカテゴリに振り分ける。同じトークンが1度のリクエストで複数のカテゴリに二重カウントされることはない。
注: 本章で
cache_controlという用語を使うのは Anthropic API の構文。Anthropic には「明示的に breakpoint を打つ」 方式と、リクエストのトップレベルに 1 つ置くと自動で配置される automatic caching の 2 種類があるが、本章では分かりやすさのため明示方式で説明する。Gemini はCachedContentという別リソース、OpenAI は暗黙キャッシュのみ。
| カテゴリ | どんなトークンか |
|---|---|
| cache_read | 既存キャッシュに前方一致した部分 |
| cache_write | キャッシュ対象 (cache_control 範囲内) で、既存キャッシュにマッチしなかった部分 = 新規書込み |
| input | キャッシュ対象範囲の外 (breakpoint より後ろ)、または breakpoint なしの部分 |
合計するとプロンプト全体になる:
total prompt tokens = cache_read + cache_write + input
つまり「cache_write の課金 + input の課金が同じトークンに対して二重に走る」 ことはない。振り分けは大きく 2 段階で決まる:
- cache_control を打ったか? 打たなければプロンプト全体が input。打てば次のステップへ。
- breakpoint (= キャッシュ対象範囲の切り替え点)より前か後ろか? 前 のうち、前方から既存キャッシュにマッチした範囲までが cache_read、そこから breakpoint までが cache_write。breakpoint より後ろは input。
ざっくりとしたイメージ
- 出力前に入れ込まれる各トークンは input / cache_read / cache_write のどれか 1 つに振り分けられる (二重計上なし)。
2.5 「プロンプトの前半が一致」とは、1 文字も違わないということなんダッ...!!
セクション 1 で「プロンプトの前半が同じならキャッシュが効く」 と書いたが、これは 厳密にバイト単位で同じ という意味。1 文字でも違えば、その地点から後ろは全部キャッシュが効かない扱いで、新規 cache_write になる。
実運用で踏みがちな落とし穴:
- システムプロンプトの冒頭に
現在時刻: 2026-05-24 17:42:00のような動的フィールドを入れる → 毎回違う → 一切キャッシュが効かない - tool 一覧の順序がリクエストごとにシャッフルされる → 順序が違う → そこから後ろが全部キャッシュが効かない
- 環境変数や config をプロンプトに埋め込む → 環境が違うとキャッシュが効かない
「動かない部分は完全に同じバイト列を維持」 が、キャッシュを効かせる原則。
ざっくりとしたイメージ
1 文字でも変わるとキャッシュが効かなくなる。
2.6 挙動を具体例でイメージしたいんだよッ.....!!
2.6.1 シングルターンで 2 回投げる場合
社内の API ドキュメントについて答えるアシスタントを作ったとする。
システムプロンプト (毎回同じ)
あなたは社内 API ドキュメントについて回答するアシスタントです。
以下が最新の API リファレンスです:
## /api/users
- GET /api/users : ユーザー一覧を取得
- POST /api/users : 新規ユーザー作成 (必須: email, name, role)
- DELETE /api/users/{id} : ユーザー削除
... (約 10KB の詳細仕様) ...
回答時は具体的なエンドポイントと使い方を示してください。
このシステムプロンプト全体に cache_control を打って 2 回送る。TTLは5分とする。
リクエストされるプロンプトの構造:
system prompt (10KB / ≒ 2500 tok) <- 毎回同じ (固定)
━━ ★ cache_control ★ ━━ <- breakpoint
user 質問 (≒ 50 tok) <- 毎回違う = input
1 回目 メッセージ「POST /api/users で必須のフィールドは?」
usage:
cache_write: 2500 ← システムプロンプト (10KB) が初回書込みとして計上
cache_read: 0
input: 50 ← user 質問「POST /api/usersで必須のフィールドは?」だけが通常 input
output: 300 ← assistant の回答 (例: "email / name / role が必須です ...")
2 回目 (5 分以内) メッセージ「ユーザーを削除するエンドポイントある?」
usage:
cache_write: 0 ← もう書込みは要らない
cache_read: 2500 ← 同じシステムプロンプトをキャッシュから読み出し (1/10 のコスト)
input: 40 ← user 質問「ユーザーを削除するエンドポイントある?」だけが通常 input
output: 280 ← assistant の回答 (例: "DELETE /api/users/{id} があります ...")
「変わらない部分」 が 2 回目以降は 1/10 の値段で済む。
注: 実 API レスポンスのフィールド名は provider ごとに違う。Anthropic は
cache_creation_input_tokens/cache_read_input_tokens、Vertex Gemini はcached_content_token_count等。意味は同じ。
2.6.2 マルチターンで累積する場合
実際の「1つの連続したやりとり」をするチャットやエージェントは 何十〜何百ターンもループする。その間のキャッシュ挙動は次のように累積していく。
仮に同じ API アシスタントを 5 ターン回したとする。各ターンで cache_control を「保持できる最新地点」 まで打ち直すと、直前のターン分を除く過去履歴 が次のターンで cache_read として再利用される:
| ターン | 何があったか | cache_write | cache_read | input | 合計 prompt |
|---|---|---|---|---|---|
| 1 | 「POST /api/users の必須フィールドは?」 | 2,550 | 0 | 0 | 2,550 |
| 2 | 応答メッセージ + 追加送信メッセージ「ユーザー削除は?」 | 200 | 2,550 | 60 | 2,810 |
| 3 | 応答メッセージ + 追加送信メッセージ「PATCH もある?」 | 250 | 2,750 | 80 | 3,080 |
| 4 | 応答メッセージ + 追加送信メッセージ「認証 token は?」 | 300 | 3,000 | 100 | 3,400 |
| 5 | 応答メッセージ + 追加送信メッセージ「rate limit は?」 | 350 | 3,300 | 120 | 3,770 |
ポイント
- cache_read がターンごとに伸びていく -> 直前分を除く過去履歴をキャッシュから読む
- 1 ターンあたりの cache_write は小さい -> 前回のやり取りを cache 末尾に追加するだけ
- ターン 5 では prompt 全体の 87% (3300/3770) が cache_read = 1/10 価格
→ 同じやりとりが TTL 内でターンを重ね続けるほど、キャッシュの恩恵が累積的に大きい構造。これが prompt caching がエージェント用途で特に効く理由。
特に一定自律的に動くコーディングエージェントでは 1 タスクで数十〜数百ターン回ることもあり、prompt 全体の 95% 以上が cache_read になるようなイメージ (= 単純計算で 6〜8 倍のコスト効率)。
逆に言うと、ここで 要約機構(= ClaudeCodeのCompactionのような動きや、OpenHandsSDKのCondenser等)を入れ込んで「過去履歴を要約に置き換える」 動きをすると、この cache_read の山が一気に崩れることもある。
3. 実際に遭遇したケース: 何も考えずに下手にContext 圧縮を入れたら、キャッシュが効きにくくなっちゃったんダッ!!
これは、弊チームで OpenHands SDK を利用した自律型の開発エージェントの検証をしていた時の話。
トークン消費を抑える狙いで、Execution Agent のトークン消費が膨らんできた段階で OpenHands SDK の Condenser (閾値を設定して閾値を超えた際に会話履歴を圧縮する機能) を入れる実験をした。
Condenser の仕組みを整理すると、「context が threshold を超えたら発火、過去ターンを要約に置き換える」 というもの。要約に置き換わるとその瞬間に プロンプトの前半 (= キャッシュ対象部分、以下 prefix) が変わるので、次の呼び出しでキャッシュが効かなくなる。主要パラメータは 2 つ
-
max_tokens(token): プロンプトが何 token を超えたら発火するか (= token 閾値) -
max_size(event 数): 履歴で保持できる最大 event 数。これを超えても発火
実測値
同じタスクに対して、Condenser の設定を切り替えて 3 回走らせた (OFF / 緩め / 攻め)。
LLM 応答自体が確率的に揺らぐ以上、エージェントのループ数やタスクの進み方は実行ごとに変動する。それでも Condenser の影響を分解する観点として、全体工程の中で「1 LLM 呼び出しあたり cache_write (tokens/回)」 を割り出して比較する ことにした。
「LLM 呼び出し回数」 を数えるために、OpenHands SDK の ActionEvent (= LLM 応答から生成される event。1 LLM 呼び出し ≒ 1 ActionEvent と扱える) をカウントする。
cache_write 合計が増えた時、原因は 2 通り考えられる:
- (A) cache_write 量が増えた
- (B) ActionEventを含む総Event数が増えてリクエスト母数が増えた
cache_write 合計 ÷ ActionEvent 数 で「1 LLM 呼び出しあたりの cache_write」 を出せば、(A) と (B) の寄与を分解する指標になる (= 「合計の増加 ≒ 1 呼び出しあたり × 呼び出し回数」 に分解できる)。
| run | Condenser 設定 | ActionEvent 数 (回) | Condensation 発火 (回) | cache_write 合計 (tokens) | 1 呼び出しあたり cache_write (tokens/回) |
|---|---|---|---|---|---|
| run1 | OFF | 115 | 0 | 114,260 | 993 (基準) |
| run2 (緩めのCondenser設定) |
max_tokens=80K, max_size=240
|
123 | 1 | 171,999 | 1,398 (+41%) |
| run3 (攻めのCondenser設定) |
max_tokens=40K, max_size=100
|
340 | 12 | 493,185 | 1,450 (+46%) |
- 緩めも攻めも 1 LLM 呼び出しあたり cache_write は OFF より 40% 以上多い。1 呼び出しあたりで見ると両者ほぼ同じくらいキャッシュが効きにくくなっている (緩め +41%、攻め +46%)。
設定によって発火パターンが変わる
threshold によって、Condenser がいつ発火して prefix が作り直されるかが変わる
OFF (Condenser なし):
呼び出し 1: context 10K → cache 効く (read)
呼び出し 2: context 25K → cache 効く (read)
呼び出し 3: context 50K → cache 効く (read)
...
→ ずっと cache_read だけ伸びる、cache_write は最初だけ
緩め (80K threshold):
呼び出し 1〜N: context が 80K 未満 → 発火しない、cache 効く
呼び出し N+1: context 80K到達 → ★発火、prefix が要約に置換
呼び出し N+2: 新しい prefix を cache に書き込み (= 大きめの cache_write)
→ 発火は 1 回だけだが、その 1 回で大量に cache_write が発生
攻め (40K threshold):
呼び出し 1〜M: context が 40K 未満 → 発火しない
呼び出し M+1: context 40K到達 → ★発火
呼び出し M+2〜L: 発火しない
呼び出し L+1: 再び発火
...
→ 発火回数は多い (今回は 12 回) が、threshold が低いので 1 回あたりの prefix サイズは小さめ
緩め (1 回発火) も攻め (12 回発火) も、合計 cache_write をLLMの呼び出し回数で割ると 1 回あたりの増分はほぼ同じ (+40〜46%)。これは「閾値が高いと発火少 × 1 回が大きい」「閾値が低いと発火多 × 1 回が小さい」 が相殺している可能性がある (= 各設定 1 run ずつの観測なので、この解釈はあくまで仮説)。
補足: 「発火 1 回でなぜ大きな cache_write が増えるのか?」 と感じるかもしれない。これは 発火 1 回 = prefix まるごと 1 回作り直し = その 1 回で新しい前半全体が新規 cache_write になるため。1 回でも要約が走れば、要約後の prefix サイズ分の書込みコストが発生する。
run3だけ総cache_writeめっちゃ増えてるの何?
これは「キャッシュ機構そのものへの影響」 ではなく、「要約された context でエージェントが状況を見失う → 同じ作業をやり直す → ループが増える」 という別の経路の問題もあるように見えた。Condenser 単体での「圧縮効果」をエージェントループの増加が打ち消し、結果として総cache_writeが膨らんだと見ている。
補足: この比較のいくつかの問題点
- そもそも総 LLM 呼び出し回数を揃えにくいから「1 LLM 呼び出しあたり」 に変換して比較してるのだが、それでも完全に公平な比較にはなっていない。OFF (Condenser なし) は「最初の書込みが大きい (= system prompt 全部を初回に書き込む) / 以降は小さい (= 各呼び出しの末尾を少しずつ追加)」 という構造。なので合計 cache_write を呼び出し回数で割ると、呼び出し回数が多いほど最初の大きい書込みが平均に薄まって、1 呼び出しあたりが下がっていく。今回 OFF は 115 呼び出し、攻めは 340 呼び出し。総呼び出し回数が少ない OFF 側は 1 呼び出しあたりが実態より高めに出やすく、結果として攻めの "+46%" は本当の差より控えめに見えてるかも。
- それぞれ何回か回して数字のブレを測る必要はありそう。今回はそこまでできてない内容です。
- Condenser は自分でも要約用の LLM 呼び出しをするが、それは ActionEvent には数えてない。run3 では 12 回 / 340 で 3-4% のズレ。
結論
プロンプトキャッシングって、"プロンプト"を"キャッシング"してたんですね。
おわりに
クロステック・マネジメント社は、「教育×AI」で次世代の学びを創造するために生まれた芸術大学発のスタートアップです!
ご興味のある方は、ぜひお気軽にお問い合わせいただけると嬉しいです!
参考
-
Anthropic API: Prompt Caching docs —
cache_control/ TTL / 4 種類のトークンの公式仕様 -
OpenHands SDK: LLMSummarizingCondenser — 3 章で使った Condenser の実装 (
max_tokens/max_sizeパラメータ)
京都芸術大学のテックブログです。採用情報:hrmos.co/pages/xtm/jobs 芸大など5校を擁する瓜生山学園は、通信教育で国内最大手、国内で唯一notionと戦略パートナー契約を結ぶなどDX領域でも躍進、EdTech領域でAIプロダクトを開発する子会社もあり、実は多くのエンジニアがいます。
Discussion