エージェントにおけるプロンプトキャッシングの仕組みと課題
TLDR AI は、プロンプトキャッシングがエージェントのコスト削減に寄与する一方、ツール定義の変更やモデル切り替えによりキャッシュが無効化されやすく、レイテンシやコスト設計に影響を与える脆弱性を指摘した。
AI深層分析を開く2026年7月28日 00:15
AI深層分析
キーポイント
コーディングエージェントの非効率な構造
コーディングエージェントはセッションが進むにつれてシステムプロンプトやツール定義などの大部分を再利用するが、現状では毎回全トークンを再計算するため、トークン数が膨大になると遅延とコストが深刻化する。
KV キャッシュの仕組みと限界
Transformer モデルは入力トークンのキーと値(KV)を保持して再計算を防ぐが、ツール定義の変更やモデル切り替えが発生するとキャッシュが無効化され、全コンテキストの再計算という高コストな挙動に陥る。
設計への波及効果
プロンプトキャッシュは単なる実装最適化ではなく、レイテンシやコストだけでなく、ツールの設計方針、セッション管理、および提供すべき機能の選定までを左右する重要な要素となる。
セッションの分岐とキャッシュ効率
会話履歴がツリー構造で管理される場合、過去の分岐点に戻っても共通するトークンプレフィックスがあればキャッシュを再利用できる。ただし、セッションIDが異なるとルーティングシステムが重複に気づかずキャッシュヒット率が低下する可能性がある。
ツール定義の変更によるキャッシュ無効化
ツールリストの順序変更やスキーマ追加はプレフィックスの不一致を引き起こし、直後の会話履歴全体を再処理させるため、コストが跳ね上がる。拡張機能がシステムプロンプトを頻繁に書き換えるとキャッシュ安定性が損なわれる。
重要な引用
most of the input is the same as last time. In other words we mostly append to it.
Once a session has grown to tens or hundreds of thousands of tokens, recomputing the whole prompt for every turn is slow and expensive.
A changed tool definition, a model switch or a provider routing decision can turn what one would expect to be a cheap incremental request into a full replay of the context.
Adding one tool, removing one, changing its schema, or even serializing the tools in a different order can move the first mismatch close to the start of the prompt.
編集コメントを表示
編集コメント
この記事は、大規模言語モデルの応用において「機能の実装」から「運用の最適化」へと視座を移す重要性を説いている。プロンプトキャッシュのような技術的詳細が、製品全体の設計思想やコスト構造に直結する点を指摘しており、実務レベルでの AI 構築における重要な教訓となっている。
Source Article
元記事を日本語で読む
本文に関係しない購読案内、埋め込み通知、サイト内プロモーションは除いています。
大規模言語モデルは、入力したテキストに対して出力が返ってくる「関数」のように捉えられがちです。これは有用な抽象化ですが、コーディングエージェントを実行する際の最も重要な要素の一つを見落としています。つまり、入力の大部分は前回と同じなのです。実際には、私たちは主にテキストを追加していくことになります。
コーディングエージェントは、モデルに対してシステムプロンプト、ツールの定義、プロジェクトの指示、会話履歴、ツール呼び出し、そしてその結果を送信します。次のターンでは、ほぼすべての情報を再送信し、わずかな新しい情報だけを付け加えます。セッションが数万から数十万トークンに成長すると、各ターンでプロンプト全体を再計算するのは遅く、コストもかかります。
プロンプトキャッシングはこの状況を少し経済的にしますが、その性質は非常に脆いものです。ツールの定義が変更されたり、モデルやプロバイダーのルーティング決定が変わったりするだけで、本来は安価な増分リクエストであるはずものが、文脈全体を再計算し直すフルプレイに変わってしまいます。
コーディングエージェントにとって、キャッシュの挙動は単なる実装の詳細や最適化の問題ではありません。それはレイテンシ、コスト、ツールの設計、セッションの設計、さらにはどの製品機能を展開すべきかという判断にも影響を及ぼします。
KV キャッシュが含むもの
トランスフォーマーモデルは、プロンプト処理を主に 2 つのフェーズに分けて行います。プリフィル(prefill)では入力トークンを読み込み、それらのアテンション状態を計算します。一方、デコード(decode)では、新しいトークンを一つずつ生成していきます。
アテンション層の各処理において、トークン一つごとにキーと値が生成されます。これらはハッシュテーブルでのキー・バリュー検索とは少し異なります。どちらも数値の配列であり、通常は浮動小数点数か、精度を落とした量子化された値です。
新しいトークンを処理する際、モデルはそのトークンのクエリと、それまでに生成されたキーを照合し、過去の各トークンがどれだけ関連性があるかを判断します。その後、その関連度スコアに基づいて対応する値の加重混合を形成します。つまり、キーはモデルが照合対象とするものであり、値は取得される情報です(ただし、この検索は辞書参照のように「単一の完全一致」を返すのではなく、曖昧なマッチングを行います)。
これらのキーと値は保持され、次に生成されるトークンが過去のすべての情報を再計算することなく参照できるようにします。この保持された状態こそがKVキャッシュです。
概念的には、リクエストは以下のような構造になります。
リクエスト 1:
[システム][ツール][ユーザー][アシスタント][ツールの結果][ユーザー]
<--------------------- プレフィル -------------------->
|
トークンと層ごとの K と V テンサー
リクエスト 2:
[システム][ツール][ユーザー][アシスタント][ツールの結果][ユーザー][新しいトークン]
<---------------- リユース可能なプレフィックス ----------------><--->
|
新しい処理
実際の表現はより複雑で、モデル固有の特性を持ち、「かなり」大きなサイズになります。重要なのは、これらが特定のトークンプレフィックスに対応している点です。同じ意味を持つプロンプトでもトークン化の方法が異なれば、KVキャッシュを共有することはできません。途中のトークンが変更されれば、その後のすべての続きは異なるものとして扱われます。
プロンプトキャッシングはこの状態の有効期間を1回の生成を超えて延長するものです。コーディングエージェントからの次の API 要求が同じトークンで始まる場合、推論システムは保存された作業を一致するプレフィックスに対して再利用し、新しいサフィックス部分のみを事前計算(prefill)すればよいのです。ここまでは理論的な話です。
キャッシュの格納場所
キャッシュが機能するためには、どこかに保存され、かつアクセス可能である必要があります。推論システムは KV キャッシュを後続の要求に対して利用可能にするために、主に 2 つのアプローチを採用しています。
より単純なアプローチはセッションアフィニティです。これは、KV キャッシュを計算した GPU の上またはその近くに保持し、次のリクエストを同じワーカーにルーティングすることで機能します。セッション ID やプロンプトキャッシュキーは単なるルーティングのヒントとなるため、ペイロードの中身を確認することなく、HTTP レベルのロードバランサでこの問題を処理することも可能です。
request(session-42) --> router --> worker 7 --> GPU 7 KV cache
next(session-42) --> router --> worker 7 --> GPU 7 KV cache
ネットワークを介して非常に大きなキャッシュを転送する必要がなくなるため、この手法は有効です。動作する場合は高速ですが、スケジューリングに制約が生じます。選択されたワーカーが過負荷になったり、再起動したり、エントリが削除されたりする可能性があります。また、ルーター側では、単一のセッションのキャッシュを維持するよりも、ファーム全体のバランスを取る方が重要だと判断することもあります。しかし、追加で導入するインフラやハードウェアがほとんど不要であるため、非常に魅力的な解決策です。
もう一つの手法はキャッシュを分散させることです。KV ブロックを別のメモリ階層に保存したり、ワーカー間で共有可能にすることで、リクエストを特定の GPU に強く紐付ける必要がなくなります。
+--------------------+
request --> scheduler -->| worker 3 / GPU 3 |
| +--------------------+
|
+----------> distributed KV blocks
|
+----------> worker 9 / GPU 9
これにより、スケジューリングの柔軟性と回復性が向上しますが、KV ブロックの移動、インデックス化、保持自体がシステム上の課題となります。実装では、GPU メモリ、ホストメモリ、ローカルストレージ、リモートストレージ、プレフィックス対応ルーティング、エビクションポリシーなどをさまざまな組み合わせで活用しています。
KV キャッシュの規模感を把握しておく必要があります。確かにサイズは大きいですが、直感よりも実は小さい側面もあります。いくつかの工夫を施すことで、長い会話であっても KV キャッシュのサイズを数ギガバイトに抑えることが可能です。
Caches and Prefixes
Pi のセッションはリストではなく木構造です。この「ツリー」機能を使えば、現在の会話を過去の時点に戻し、別の分岐をたどることができます。巻き戻し操作では、アクティブなサフィックス(末尾部分)を破棄できますが、セッションファイル自体から削除されるわけではありません。新しい分岐は、古いコンテキストのほとんどを共有することもあれば、一部だけ共有したり、実質的に何も共有しないことも可能です。
この設計は Pi 固有のものではなく、多くのコーディングエージェントが概念的に似た仕組みを持っています。セッションを木構造として表現していなくても、エージェントが何らかの形で巻き戻し機能を持つことは珍しくありません。
+-- E -- F another branch
|
session S: root -- A -- B -- C -- D current branch
|
+-- Z branch near the startこれら3つの分岐は、すべて同じ Pi セッション ID を持つ可能性があります。ルーターの視点から見ればこれらは1つのセッションですが、プロンプトキャッシュの観点からは、部分的なプレフィックス(先頭部分)しか共有していない3つのトークンシーケンスと見なされます。
キャッシュが再利用可能なプレフィックスブロックを保持している場合、D から F へジャンプしても「root → C」までの部分は引き続き利用可能です。しかし、最もホットな継続部分のみを保持している場合や、共有ブロックがエバクション(削除)された場合、あるいはリクエストが別の経路にルーティングされた場合は、キャッシュヒット率は大幅に低下します。A から始まる Z へのジャンプでは、システムプロンプトと初期のツール定義のみが維持される可能性があります。
この状況における正確なキャッシュ管理の挙動は、プロバイダーによって大きく異なります。
逆のケースも起こり得ます。/fork 処理や新しいセッションで、大量に共通するコンテキストを保持したまま、新しいセッション ID が生成されることがあります。セッションキーごとにキャッシュを隔離するルーティングシステムでは、こうした有益な重複を見逃す可能性があります。
再利用可能なプレフィックスが何をキャッシュできるかを決定します。セッションの正体は、インフラストラクチャが関連するコンテンツを見つけやすくするための補助に過ぎません。システムによってはルーティングキーがキャッシュ管理の要となる一方、他のシステムでは単なる最適化手段です。
明示的キャッシュと自動プレフィックスキャッシュの違い
プロバイダー API は主に 2 つのスタイルでキャッシュ機能を提供しています。
Anthropic の従来のインターフェースでは、明示的な cache_control ポイントを使用します。クライアントは、システムプロンプトやツール定義、あるいは最新のキャッシュ可能な会話内容など、リクエスト内で安定した部分の後に境界線を設けます。サーバーはこの境界点で終わるプレフィックスに対して、書き込みまたは参照を行います。境界線は明確ですが、再利用にはその前のコンテンツが一致している必要があります。キャッシュポイントが明示的であるだけでなく、料金体系も明確です。キャッシュへの書き込みには料金を支払い、どの期間をどの価格帯にするかを自分で選択できます。
一方、他の API では自動プレフィックスキャッシングを採用しています。クライアントは通常通りリクエストを送信するだけで、プロバイダー側がクライアントが設定したブレイクポイントなしで再利用可能なプレフィックスを検出します。prompt-cache キーやセッションヘッダーを使用すると、ルーティングやグループ化の精度が向上することはありますが、異なるプレフィックスを同一視できるわけではありません。
ツールセットがキャッシュを破壊する理由
ツール定義は通常、会話の前に配置され、内部的にシステムプロンプトに「折りたたまれます」。ツールの名前、説明、JSON スキーマは、他のテキストと同様にモデルへの入力です。ツールを 1 つ追加したり削除したり、スキーマを変更したり、あるいはツールを異なる順序でシリアライズするだけで、最初の不一致がプロンプトの先頭に近づいてしまいます。
turn 1: [system][read][write][bash][conversation...........]
turn 2: [system][read][write][bash][deploy][conversation...]
|
old conversation is now
after a mismatch
これは、プラグインシステムや MCP スタイルのツールカタログでよくある予期せぬ問題です。ツールが必要になった時点で読み込む方が効率的に思えますが、実際には多くのモデルにおいて、新たに拡張されたツールのセットは、その直後のキャッシュされた会話を無効化してしまいます。わずかなツールスキーマのトークンを節約できたとしても、数万トークンに及ぶ会話データを再度処理させることになりかねません。
一部の新しいモデル API では追加的なツール読み込みをサポートしています。これは、ツールを元のリストに挿入するのではなく、トランスクリプト内の特定のツール結果で利用可能にする方式です。これにより、古いプレフィックスは変更されません。
[system][initial tools][conversation][new tool][next turn]
<--------- cached prefix ----------->
現在、Pi はネイティブの遅延ツールメカニズムを持つモデルに対してこの機能をサポートしています。拡張機能が setActiveTools() を使用して純粋に追加的な変更を行う場合、Pi はその追加された名前をツール結果として記録します。対応する Anthropic モデルでは遅延定義と tool_reference が利用され、対応する OpenAI モデルでは該当する tool-search アイテムが生成されます。それ以外のモデルについては安全なフォールバックが用意されており、Pi は次のリクエストで完全なアクティブツールリストを送信します。これは機能的には問題ありませんが、プロンプトキャッシュを無効化してしまう可能性があります。
追加的(additive)という言葉が重要です。ツールの削除や、別のセットアップへの置き換え、プロンプトスニペットの変更は、いずれも以前の入力を変更することになります。システムプロンプトの再構築、ツール順序の入れ替え、タイムスタンプの挿入、あるいはターンごとにアクティブなツールを変更する拡張機能は、意図せずしてセッション全体のキャッシュを無効化してしまう恐れがあります。
拡張性の観点から、Pi がすべての拡張機能にわたってキャッシュの安定性を保証することはできません。キャッシュに優しいメカニズムを提供することはできますが、それらを利用するのはあくまで拡張機能側です。これまでの経験では、多くの拡張機能にとってキャッシュ効率性は後回しにされがちです。その一因として、固定サブスクリプションで課金される場合、キャッシュミスに伴うコストが必ずしも明確ではないという事情があります。
中断と TTL
重要なプロンプトキャッシュには、デフォルトで短い有効期限が設定されている場合があります。特にAnthropicの5分間のデフォルトキャッシュは、多くのコーディング作業よりも短いため注意が必要です。
Fable を使用中にコーヒーを飲みに行き、10 分後に戻ったとしましょう。その場合、「こんにちは」というたった一つのメッセージを送るだけで、予想以上に高額な請求が来る可能性があります。
これは、ユーザーにとってはコーディングセッションが継続的に行われているように思えても、推論プロバイダーの側では「孤立したリクエストの連続」として認識されているからです。
モデルへのリクエスト → 7 分間のテスト実行(この間キャッシュは利用されない)→ モデルへのリクエスト
長いビルド時間、テストスイートの実行、昼食、会議、あるいは単に差分レビューのために作業を中断するだけで、キャッシュの有効期限が切れてしまいます。次のリクエストでは同じプロンプトが含まれていますが、保存された KV 状態は失われており、プレフィックス部分が再度入力として課金されてしまうのです。
現在、Pi は Anthropic のサブスクリプションで許可されているハーンネスではないため、API ユーザー向けに Anthropic が推奨する5分間のデフォルト設定に従っています。しかし、Claude Code のコードベースを確認したところ、同社のサブスクリプションユーザーに対してはキャッシュタイムアウトを1時間に延長していることがわかります。
ただし、API トークンの価格を支払う必要がある場合、この延長によるコスト増がその価値に見合わないケースも多々あります。
ただし、この機能を利用するオプションは用意されています。Anthropic などの一部のプロバイダーは、より長い保持期間の制御機能を公開しています。対応している直接 API を利用する場合、Pi ユーザーは PI_CACHE_RETENTION=long を設定することで、長期保持をリクエストできます。ただし、これはあくまでリクエストに過ぎません。Pi がゲートウェイに対してエントリの保持を強制したり、メモリ不足による削除を防いだり、モデルへのリクエストがない間もキャッシュを維持させたりすることはできません。
見落としのコスト
プロバイダーは通常、キャッシュ未入力(uncached input)、キャッシュ書き込み、キャッシュ読み取りに対して異なる価格を設定しています。キャッシュ読み取りは、高コストなプリフィル処理がすでに完了しているため、一般的に割引価格で提供されます。一方、キャッシュ書き込みにはプレミアムが付けられることが多く、プロバイダーは後日の利用のために状態を保持することを約束するからです。
例えば、10 万トークンの履歴を持つコーディングセッションがあり、その後に上記の Fable の例のような短い新しいリクエストが来たと想像してください。キャッシュが機能していれば、履歴のほとんどすべてが低いキャッシュ読み取り価格で課金されます。通常の入力価格で処理され、かつキャッシュへの書き込み対象となるのは、ごく一部の新しいデータだけです。
しかし、キャッシュミスが発生すると、プロバイダーは 10 万トークンに及ぶ履歴全体を通常の入力価格で再処理しなければなりません。さらに、その履歴をキャッシュへ書き戻すための料金も請求される可能性があります。これが、キャッシュ期限切れの後に「continue」といった短いリクエストが予想以上に高額になる理由です。長いコーディングセッションにおいては、古い入力を再読み取りするコストは、次の回答を生成するコストよりも遥かに高くなるのです。
また、キャッシングには予期せぬインセンティブを生み出す可能性もあります。
ユーザーにとってキャッシュヒット率が高いことは、レイテンシの短縮とコスト削減につながるため望ましいです。GPU を管理する推論オペレーターにとっても同様で、事前計算(prefill)の負荷が減れば、同じハードウェアでより多くのリクエストを処理できるようになります。適切に設計されたキャッシュトークン割引は、双方の利益を一致させつつ、オペレーターにはより良いマージンを残すことができます。
一方、ゲートウェイや再販売業者には異なるインセンティブが働く可能性があります。入力トークンに対して未キャッシュ料金を課して収益を得ている場合、キャッシュミスが発生するとヒット時よりも顧客への請求額が大きくなることもあります。それが実際に利益増につながるかは、上流のコスト、契約内容、そして誰がキャッシュを運用しているかによって異なります。整合性が取れていないシステムでは、ルーティングを担当する側がミスのコストを十分に負担しない一方、ユーザーに課金する側はミスが発生した際に収益が増えるという構造になりかねません。
これはプロバイダーが意図的にキャッシュを妨害しているという意味ではありません。重要なのは、キャッシュのパフォーマンスを可視化しておくことです。ユーザーが請求書の異常な大きさから推測する必要はありません。キャッシュに何らかの違和感があるかどうかを理解することは、重要な洞察となり得ます。
厳格なキャッシュ準拠は、ゲートウェイがターン間の最適な選択肢へ柔軟にルーティングする能力を制限することにもつながります。例えば、キャッシュヒットを利用して別のモデルに移行し、その先でより経済的な運用を目指すケースや、負荷分散によって他のプロバイダーへ切り替えた方が有利になるケースなどが考えられます。
Why Pi Does Not Prune Aggressively
ここまでお読みいただき、なぜ Pi がツール呼び出しを削除しないのか、その理由がお分かりいただけたかと思います。
コストを抑えるために古いツールの結果を継続的に削除したり、履歴を書き換えたりしたくなるのは自然なことです。特にコンテキストウィンドウの限界に近づいた際には、それが必要な場合もあります。しかし、私たちが学んだ通り、削除自体にもキャッシュのコストが発生します。
途中からコンテンツを削除すると、その地点でのプレフィックス(先頭部分)が変更されます。その後の会話で生き残ったすべての内容が、再処理を余儀なくされる可能性があります。長いキャッシュされたコンテキストを書き換える即時コストは、少数の安価なトークンを削除することで得られる将来の節約額を上回る恐れがあります。
大まかな損益分岐点の比較は以下のようになります。
一度きりの書き換えコスト
~= 編集後に残存するトークン数 * (キャッシュなし価格 - キャッシュ読み取り価格)
ターンごとの将来の節約額
~= 削除されたトークン数 * キャッシュ読み取り価格
これは単なる経理上の問題ではありません。古いツールの結果には、モデルが後の意思決定を行う際に参照した根拠が含まれていることが多いためです。それらを削除すると、要約で要点を保持していても、モデルの動作品質が低下する可能性があります。
そのため Pi は、安定した追加型のトランスクリプト(記録)を好みます。すべての古いトークンを無駄なものと見なすことはありません。コンテキスト圧力が損失のある書き換えを正当化する場合にのみ、コンパクション(圧縮)機能を利用します。Pi は、コンパクションが意図的に新しいコンテキストを作成するものであり、変更されていないプロンプトに対して誤って再課金されるものではないと理解しているため、セッション統計においてはこれをキャッシュの失敗ではなく、キャッシュのリセットとして扱います。
目指すべきは、最小限のプロンプトサイズではなく、モデルのコンテキスト容量、キャッシュ再利用性、レイテンシ、コストという要素の間で最適なバランスを取ることです。
同時に、プロンプトを削る(プルーニング)べきケースも存在します。例えば、優れたキャッシュ利用に対して割引を提供しないプロバイダーを利用している場合や、何らかの理由で高いキャッシュヒット率を得ることが難しい場合は、プルーニングを検討したほうが賢明な場合があります。これは、キャッシュが転送できないため、ルーターが異なるバックエンド間でバランスを取る機会を確実に高めるからです。
Pi ができることとできないこと
Pi は、安定した入力を安定的に保つことを目指しています。一貫性のあるセッション ID とプロバイダー固有のキャッシュヒントを渡すほか、API 要件に応じて明示的なキャッシュポイントを配置し、キャッシュ読み取り・書き込みの使用状況を記録します。また、モデルが対応している場合は、メッセージに紐付いた追加型ツールロードもサポートします。デフォルトのトランスクリプト動作では、不要な古いコンテキストの再書き込みも行いません。
Pi が制御できないのは、リクエストがマシンを離れた後のすべてのレイヤーです。プロバイダーのキャッシュ削除ポリシーを選んだり、API の制限を超えてキャッシュ期間を延長したり、特定の GPU を稼働状態に維持し続けたり、ゲートウェイがアフィニティ(親和性)ルールを遵守することを保証したりすることはできません。また、拡張機能によって変更されたプレフィックス(先頭部分)を保持することもできません。
しかし、Pi ができることはあります。キャッシュの健全性を可視化することです。
インタラクティブなフッターには、累積的なキャッシュの読み書き回数(R と W)と、最新のリクエストにおけるキャッシュヒット率(CH)が表示されます。より詳細な情報については /session コマンドを使用してください。ここでは、キャッシュされた入力とされていない入力の合計、累積ヒット率、コスト、そして 重大なキャッシュミス によって再請求された可能性のあるトークン数と金額の見積もりを確認できます。
メッセージ
合計:178
ユーザー:6
アシスタント:58
ツール:114 回の呼び出し、114 件の結果
トークン
入力:7,129,883
キャッシュ済み:6,776,832 (95.0%)
キャッシュなし:353,051
出力:30,013
合計:7,159,896
コスト
合計:$6.054
キャッシュ再請求分:$0.728(161,744 トークン、ミス 2 回)
ミスが発生した際にその都度通知を受けたい場合は、/settings で Show cache miss notices を有効にできます。これは settings.json の showCacheMissNotices 設定に対応しています。Pi は重大なミスが発生すると警告を表示し、再請求される見込みのトークン数とコストを含めます。モデルの切り替えや、通常の短時間 TTL を超えるアイドル期間が検知された場合も同様に通知します。その他のミスについては、プロバイダー内部で何が起きたのかを推測することなく、事実のみを報告します。
キャッシュパフォーマンスが悪化する一般的な原因
セッションのキャッシュヒット率が異常に見える場合、主な原因は以下の通りです。
- 待機時間の発生:コマンドの実行、レビュー、または会話の一時停止が、プロバイダーの保持期間(retention window)を超えてしまった場合。
- モデルやプロバイダーの切り替え:KV キャッシュの状態はモデル固有のものであり、一般的にプロバイダー間で共有されることはありません。
ブランチのナビゲーションでは、/tree による移動や巻き戻し、フォーク、別ブランチへの切り替えが行われると、セッション ID が同じであってもアクティブなトークンシーケンスが変化します。
圧縮や手動での履歴書き換えは、意図的にプロンプトの一部を置き換えて新しいプレフィックスを確立する行為です。
ツールや推論レベルの変更も注意が必要です。ツールの定義を追加・削除・順序変更・編集を行うと、モデルがメッセージアンカー型ロードをサポートしていない限り、あるいは変更が純粋に追加的なものではない限り、リクエストの初期部分が変化します。同様に、推論レベルの変更も同じ影響を及ぼします。
動的なシステムプロンプトでは、タイムスタンプやランダム値、変化するプロジェクトコンテキスト、拡張機能から提供されるプロンプトスニペットが含まれると、それ以降の内容がすべて無効化されることがあります。
拡張機能によるコンテキスト変換もリスク要因です。過去のメッセージやプロバイダーペイロードを修正する拡張機能が存在すると、一見安定しているように見える Pi の転送記録でも、通信経路上で不安定になる可能性があります。
プロバイダーのルーティングとエビクションでは、プロンプト内容が同一であっても、必要な KV ブロックがリクエスト着地点に存在しない場合、キャッシュミスが発生します。
原文を表示
Large language models are often thought of like functions: send in some text,
receive some text. That is a useful abstraction, but it ignores one of the most
important parts of running a coding agent: most of the input is the same as last
time. In other words we mostly append to it.
A coding agent sends the model its system prompt, tool definitions, project
instructions, conversation history, tool calls, and tool results. On the next
turn it sends almost all of that again, plus a small amount of new material.
Once a session has grown to tens or hundreds of thousands of tokens, recomputing
the whole prompt for every turn is slow and expensive.
Prompt caching is what makes this somewhat economic, but it is also quite
fragile. A changed tool definition, a model switch or a provider routing
decision can turn what one would expect to be a cheap incremental request into a
full replay of the context.
For coding agents, cache behavior is therefore not just an implementation detail or
optimization. It affects latency, cost, tool design, session design, and even
which product features should be made available.
What a KV Cache Contains
A transformer processes a prompt in two broad phases. During prefill, it
reads the input tokens and computes attention state for them. During decode,
it produces new tokens one at a time.
At each attention layer, every processed token produces a key and a value. These
are not quite like key-value lookups in a hash table: both are arrays of
numbers, usually floats or lower-precision quantized values. When processing a
new token, the model compares that token's query with the earlier keys
to determine how relevant each earlier token is. It then uses those relevance
scores to form a weighted mixture of the corresponding values. In that
sense, a key is what the model matches against, while a value is the information
it retrieves (but the lookup is fuzzy rather than "returning a single exact
match" like a dictionary lookup.)
Those keys and values are retained so that the next generated token can attend
to everything that came before without recomputing the earlier tokens. This
retained state is the KV cache.
Conceptually, a request looks like this:
request 1:
[system][tools][user][assistant][tool result][user]
<--------------------- prefill -------------------->
|
K and V tensors per token and layer
request 2:
[system][tools][user][assistant][tool result][user][new]
<---------------- reusable prefix ----------------><--->
|
new work
The real representations are more complicated, model-specific, and "quite"
large. The important property is that they correspond to a particular token
prefix. Two prompts that mean the same thing but tokenize differently do not
share a KV cache. If a token changes in the middle, everything after that token
is a different continuation.
Prompt caching extends the lifetime of this state beyond one generation. When
the next API request from the coding agent begins with the same tokens, the
inference system can reuse the stored work for the matching prefix and prefill
only the new suffix. So far, the theory.
Where the Cache Lives
In order for a cache to work it needs to be stored somewhere, and it needs to be
addressable. There are two broad ways inference systems make KV caches
available to a later request.
The simpler approach is session affinity. It works by keeping the KV cache
on or near the GPU that computed it, and routing the next request back to the same
worker. A session ID or prompt-cache key becomes a trivial routing hint and so
you can potentially even deal with this problem on the HTTP load balancer level
without having to look into the payload.
request(session-42) --> router --> worker 7 --> GPU 7 KV cache
next(session-42) --> router --> worker 7 --> GPU 7 KV cache
This avoids moving a very large cache over the network. It is fast when it
works, but it constrains scheduling. The selected worker can become overloaded,
restart, or evict the entry. A router may also decide that balancing the fleet
is more important than preserving one session's cache. It is however a very
attractive solution because it works with little extra deployed infrastructure
and hardware.
The other approach is to distribute the cache. KV blocks can be stored in
another memory tier or made available across workers, so a request is not tied
as tightly to one GPU.
+--------------------+
request --> scheduler -->| worker 3 / GPU 3 |
| +--------------------+
|
+----------> distributed KV blocks
|
+----------> worker 9 / GPU 9
That improves scheduling flexibility and recovery, but moving, indexing, and
retaining KV blocks is itself a systems problem. Implementations mix GPU memory,
host memory, local storage, remote storage, prefix-aware routing, and eviction
policies in different ways.
To put KV caches into perspective: they can be large but they are in some ways
smaller than one would assume. With various tricks, the size of KV caches can
be reduced to a handful of gigabytes, even for long conversations.
Caches and Prefixes
Pi sessions are trees, not lists. /tree can move the active conversation back
to an earlier point and continue along another branch. A rewind can discard the
active suffix without deleting it from the session file. A new branch can share
most of the old context, a little of it, or effectively none of it. This design
is not unique to Pi, quite a few coding agents have something at least
conceptually similar. Even if you do not represent the session as a tree,
it's not uncommon for agents to have some form of rewinding.
+-- E -- F another branch
|
session S: root -- A -- B -- C -- D current branch
|
+-- Z branch near the start
All three branches can have the same Pi session ID. From the router's
perspective they are one session. From the prompt cache's perspective they are
three token sequences with only partial prefix overlap.
If the cache keeps reusable prefix blocks, jumping from D to F may still
reuse root -> C. If it only retains the hottest continuation, if the shared
blocks were evicted, or if the request is routed elsewhere, the hit can be much
smaller. Jumping to Z may preserve only the system prompt and initial tool
definitions even though it starts from A. The precise cache management
behavior here depends greatly on the providers.
The reverse can also happen. /fork or a new session can produce a new session
ID while carrying over a large amount of identical context. A routing system
that isolates caches by session key may fail to notice that useful overlap.
The reusable prefix determines what work can be cached. Session identity merely
helps the infrastructure find likely content. On some systems the routing key
is crucial to manage caches, on others it's merely an optimization.
Explicit vs Automatic Prefix Caching
Provider APIs expose caching in two main styles.
Anthropic's traditional interface uses explicit cache_control points. The
client marks boundaries after stable parts of the request, such as the system
prompt, tool definitions, or the latest cacheable conversation content. The
server can then write or look up the prefix ending at those points. The boundary
is explicit, but reuse still requires the content before it to match. Not only
are the cache points explicit, so is the pricing. You pay for cache writes, and
you get to choose for how long which comes at different price points.
Other APIs use automatic prefix caching. The client sends the request normally,
and the provider finds a reusable prefix without client-placed breakpoints. A
prompt-cache key or session header may improve routing or grouping, but it does
not make different prefixes equal.
Why Tool Loadouts Trash Caches
Tool definitions usually appear before the conversation and they are "folded"
into the system prompt internally. Their names, descriptions, and JSON schemas
are model input just like any other text. Adding one tool, removing one,
changing its schema, or even serializing the tools in a different order can move
the first mismatch close to the start of the prompt.
turn 1: [system][read][write][bash][conversation...........]
turn 2: [system][read][write][bash][deploy][conversation...]
|
old conversation is now
after a mismatch
This is a common surprise with plugin systems and MCP-style tool catalogs.
Loading a tool only when it becomes relevant sounds efficient because fewer
schemas are sent initially. On most models, however, the newly expanded loadout
invalidates the cached conversation that follows it. Saving a few tool-schema
tokens can cause tens of thousands of conversation tokens to be processed again.
Some newer model APIs support additive tool loading. A tool can become
available at a specific tool result inside the transcript instead of being
inserted into the original tool list. The old prefix remains unchanged:
[system][initial tools][conversation][new tool][next turn]
<--------- cached prefix ----------->
Pi nowadays supports this for models with native deferred-tool mechanisms. When
an extension makes a purely additive change with setActiveTools(), Pi records
the added names on the tool result. For supported Anthropic models it uses
deferred definitions and a tool_reference and for supported OpenAI models it
emits the corresponding tool-search items. Other models get a safe fallback: Pi
sends the complete active tool list on the next request, which works
functionally but may wipe the prompt cache.
The word additive matters as removing tools, replacing one loadout with
another, or changing prompt snippets still changes earlier input. An extension
that rebuilds the system prompt, shuffles tool order, injects timestamps, or
changes active tools every turn can accidentally defeat caching for the entire
session.
Extensibility means Pi cannot guarantee cache stability on behalf of every
extension. We can provide cache-friendly mechanisms; extensions still have to
use them and from what we have seen, for many extensions cache efficiency is an
afterthought. This is, in part, because when you pay on a fixed subscription the
associated cost with cache misses is not quite as obvious.
Interruptions and TTLs
Some important prompt caches have short default lifetimes. Anthropic's default
five-minute cache is particularly important because it is shorter than many
normal coding activities. If you go to sip a coffee when using Fable, and you
come back 10 minutes later, a single "say hi" message will cost you more money
than you expect.
That's because while the user may think of a coding session as continuously
active, the inference provider sees a sequence of isolated requests:
model request --> run tests for 7 minutes --> model request
no cache traffic here
A long build, a test suite, lunch, a meeting, or simply stopping to review a
diff can outlive the cache. The next request contains the same prompt, but the
stored KV state is gone and the prefix is billed again as input.
Since Pi is currently not a permitted harness on Anthropic's subscription we're
following the 5 minute default that Anthropic recommends for API users. However
from looking at Claude Code's codebase we know that for their own subscription
users, they are increasing that cache timeout to one hour. The increased cost
of this however often is not worth it, when you need to pay API token prices.
But you can opt into this. Some providers such as Anthropic expose longer
retention controls. For supported direct APIs, Pi users can set
PI_CACHE_RETENTION=long to request them. That is still only a request: Pi
cannot force a gateway to retain an entry, prevent eviction under memory
pressure, or keep a cache alive while no model request is being made.
The Price of a Miss
Providers usually price uncached input, cache writes, and cache reads
differently. Cache reads are commonly discounted because the expensive prefill
work has already happened. Cache writes can carry a premium because the provider
is promising to retain state for later use.
Imagine a coding session with 100,000 tokens of history followed by a short new
request as the Fable example above. When the cache works, almost all of that
history is charged at the lower cache-read price. Only the small amount of new
material needs to be processed at the regular input price and potentially
written to the cache.
When the cache misses, the provider has to process the entire 100,000-token
history again at the regular input price. It may also charge to write that
history back into the cache. This is why a short request such as continue can
be surprisingly expensive after a cache expires. In a long coding session,
re-reading the old input can cost much more than generating the next answer.
Caching also has a chance to create non-obvious incentives.
The user should want high hit rates because they reduce latency and price. An
inference operator that owns the GPUs should want them too: less prefill work
means more requests served with the same hardware. A well-designed cached-token
discount can align both sides while leaving the operator with better margins.
A gateway or reseller can have a different incentive. If it earns revenue from
input tokens billed at the uncached rate, a cache miss can produce a larger
customer invoice than a hit. Whether that also produces more profit depends on
its upstream costs, contracts, and who operates the cache. In a badly aligned
stack, the party responsible for routing may not bear the full cost of misses,
while the party billing the user earns more revenue when they happen.
That does not mean providers sabotage caches but it means cache performance
should be observable. Users should not have to infer that only from a
surprisingly large bill. Understanding if something odd is going on with caches
can be an important insight.
Strict cache adherence also means less flexibility for a gateway to route you to
the best option in-between turns. You might want to take a cache hit to continue
with a different model which from that point onwards might be more economical,
or it might be the case that you might be better off load balancing to another
provider.
Why Pi Does Not Prune Aggressively
Now that you've made it this far, you probably have an idea why Pi does not prune
tool calls. It is tempting to control cost by continuously deleting old tool
results or rewriting history and sometimes that is necessary, especially near
the context-window limit. But as we have learned, pruning has a cache cost of
its own.
Deleting content from the middle changes the prefix at the deletion point. All
surviving conversation after it may need to be processed again. The immediate
cost of rewriting a long cached context can exceed the future savings from
removing a small number of cheaply cached tokens.
A rough break-even comparison is:
one-time rewrite cost
~= surviving tokens after the edit * (uncached price - cache-read price)
future savings per turn
~= pruned tokens * cache-read price
This is not only an accounting question as old tool results often contain the
evidence the model used to make later decisions. Removing them can degrade
behavior even if a summary preserves the gist.
Pi therefore prefers a stable, append-oriented transcript and does not treat
every old token as waste. Compaction is available when context pressure
justifies a lossy rewrite. Because compaction deliberately creates new context
rather than accidentally re-billing an unchanged prompt, Pi treats it as a cache
reset rather than a cache failure in its session statistics.
The goal is not the smallest possible prompt but the best trade-off among model
context, cache reuse, latency, and price.
Simultaneously there can be a case for pruning too. If you are working with
providers that do not discount you for good cache usage, or it's for whatever
reason not possible to get high cache rates, it might be preferable to prune.
It definitely improves the opportunity for the router to balance between
different backends as caches are not transferable.
What Pi Can and Cannot Do
Pi works to keep stable inputs stable. It passes a consistent session ID and
provider-specific cache hints, places explicit cache points for APIs that
require them, records cache-read and cache-write usage, and supports
message-anchored additive tool loading where models allow it. Its default
transcript behavior also avoids gratuitously rewriting old context.
Pi cannot control every layer after the request leaves the machine. It cannot
choose a provider's eviction policy, extend a cache beyond what the API permits,
keep a particular GPU alive, or guarantee that a gateway honors affinity. It
also cannot preserve a prefix that an extension changes.
What it can do is make cache health visible.
The interactive footer shows cumulative cache reads and writes as R and W,
plus CH for the latest request's cache-hit rate. The /session command gives
a fuller view: total cached and uncached input, cumulative hit rate, cost, and
an estimate of tokens and dollars re-billed by significant cache misses.
Messages
Total: 178
User: 6
Assistant: 58
Tools: 114 calls, 114 results
Tokens
Input: 7,129,883
Cached: 6,776,832 (95.0%)
Uncached: 353,051
Output: 30,013
Total: 7,159,896
Cost
Total: $6.054
Cache Re-billed: $0.728 (161,744 tokens, 2 misses)
Users who want misses called out as they happen can enable **Show cache miss
notices** in /settings, corresponding to showCacheMissNotices in
settings.json. Pi then inserts a warning after a significant miss, including
the estimated re-billed tokens and cost. When it can observe a model switch or
an idle gap beyond the usual short TTL, it says so. For other misses it reports
the fact without pretending to know what happened inside the provider.
Common Reasons for Worse Cache Performance
When a session's cache-hit rate looks wrong, the usual causes are:
- Idling. A command, review, or conversation pause exceeds the provider's retention window.
- Model or provider switches. KV state is model-specific and generally does not move across providers.
- Branch navigation. /tree, rewinds, forks, and alternate branches can change the active token sequence even when the session ID remains the same.
- Compaction or manual history rewriting. These intentionally replace part of the prompt and establish a new prefix.
- Tool and reasoning level changes. Adding, removing, reordering, or editing tool definitions changes an early part of the request unless the model supports message-anchored loading and the change is purely additive. Reasoning level changes usually have the same effect.
- Dynamic system prompts. Timestamps, random values, changing project context, and extension-provided prompt snippets can invalidate everything after them.
- Extension context transforms. An extension that modifies old messages or provider payloads can make an apparently stable Pi transcript unstable on the wire.
- Provider routing and eviction. The prompt can be identical and still miss because the relevant KV blocks are no longer available where the request lands.
AI算出
技術分析ainew評価標準
AI エージェントの実装とコスト最適化に関する具体的な技術的洞察を提供しており、新規性は既存の概念を深掘りした独自分析として評価できるが、特定の製品発表ではないため novelty は中程度となる。検索機会については「プロンプトキャッシング」や「エージェント」という具体的だが広範なキーワードが含まれるため 0.5 と判断し、日本固有の事例や規制情報がないため低めとした。
6つの評価軸を見る
- AI関連度
- 90
- 情報源の信頼性
- 50
- 新規性
- 60
- 調べる価値
- 50
- 重複の少なさ
- 100
- 日本での有用性
- 25
関連記事
今日のまとめ
AIデイリーブリーフで今日の重要ニュースをまとめ読み