エージェントワークフローにおけるプロンプトキャッシュの重要性
大規模言語モデル(LLM)は「テキストを入力し、テキストを出力する関数」と捉えられがちですが、エージェント、特にコーディングエージェントの運用においては、この抽象化では見過ごされがちな重要な側面があります。それは、ほとんどの入力が前回のリクエストと同じであるという点です。(出典)
エージェントは、システムプロンプト、ツール定義、プロジェクト指示、会話履歴、ツール呼び出し、そしてツール実行結果といった大量の情報をモデルに送ります。次のターンでは、これらの情報のほとんどを再送信し、新しい情報が少量追加される形です。
セッションが数万、数十万トークンに達すると、ターンごとに全てのプロンプトを再計算することは、非常に遅く、そして高価になります。ここでプロンプトキャッシュが経済的な運用を可能にする鍵となりますが、その挙動は非常に不安定であるのが実情です。
プロンプトキャッシュは、LLMエージェントの推論コストとレイテンシを大幅に削減するための不可欠な技術です。
個人的な感想としては、私の運用している個人プロダクトでも、LLMの呼び出しコストは常に課題です。特に会話エージェントのようなステートフルなシステムでは、このキャッシュの有無が運用コストに直結するため、非常に興味深い話題だと感じます。
KVキャッシュの仕組みとそのライフサイクル
Transformerモデルは、プロンプトを2つの主要なフェーズで処理します。まず Prefill(事前埋め込み)フェーズで入力トークンを読み込み、アテンションステートを計算します。
次に Decode(生成)フェーズで、一度に1つずつ新しいトークンを生成します。(出典)
各アテンション層では、処理されたトークンごとにキー(Key)とバリュー(Value)が生成されます。これらはハッシュテーブルのような厳密なルックアップとは異なり、数値の配列です。新しいトークンを処理する際、モデルはそのトークンのクエリを以前のキーと比較して、各以前のトークンがどれだけ関連性があるかを判断します。
その関連性スコアを使用して、対応するバリューの重み付き混合を形成します。つまり、キーはモデルがマッチングする対象であり、バリューは取得する情報であると理解できます。
これらのキーとバリューは、次に生成されるトークンが以前の全てのトークンに再度アテンションをかける際に、再計算することなく利用できるように保持されます。この保持された状態こそが、KVキャッシュです。
概念的なリクエストの動作は以下のようになります。
| リクエストフェーズ | 動作 | 説明 |
|---|---|---|
| リクエスト1 | [システム][ツール][ユーザー][アシスタント][ツール結果][ユーザー] |
全体をPrefillフェーズで処理し、KとVのテンソルが生成されます。 |
| リクエスト2 | [システム][ツール][ユーザー][アシスタント][ツール結果][ユーザー][New] |
[システム]...[ユーザー]の部分は再利用可能なPrefixとしてKVキャッシュから読み込まれ、[New]の部分のみPrefillフェーズで処理されます。 |
KVキャッシュは特定のトークンプレフィックスに対応しています。同じ意味でもトークン化が異なる場合や、途中のトークンが変更された場合は、それ以降の全てが異なる継続と見なされ、キャッシュは再利用できません。プロンプトキャッシュは、このKVキャッシュの状態を、一度の生成を超えて維持することで、次のAPIリクエストで同じトークンで始まる場合に、推論システムが既存の作業を再利用し、新しいサフィックスのみをPrefillできるようにするものです。
キャッシュの保存場所と運用の課題
キャッシュが機能するためには、どこかに保存され、アドレス指定可能である必要があります。推論システムがKVキャッシュを後続のリクエストで利用可能にするには、主に2つの方法があります。
-
セッションアフィニティ: 最もシンプルなアプローチです。KVキャッシュは、それを計算したGPU上またはその近くに保持され、次のリクエストは同じワーカーにルーティングされます。セッションIDやプロンプトキャッシュキーは、単純なルーティングヒントとして機能し、HTTPロードバランサーレベルでこの問題を処理することも可能です。(出典)
この方式の課題は、非常に大きなKVキャッシュをGPU間で移動させるコストを回避できる一方で、特定のワーカーに負荷が集中したり、ワーカーの障害時にキャッシュが失われたりするリスクがある点です。また、GPUリソースの効率的な利用という観点からも課題があります。
-
分散キャッシュ: (主記事では詳細に触れられていませんが、文脈から推測されるもう一つの方式)KVキャッシュを分散ストレージに保存し、複数のワーカーやGPUからアクセスできるようにするものです。これにより、スケーラビリティや耐障害性が向上しますが、キャッシュのシリアライズ・デシリアライズ、ネットワーク転送のオーバーヘッド、キャッシュの一貫性維持といった新たな課題が生じます。
参考記事によると、多くのエージェント開発者が「30秒ごとにプロンプトキャッシュをPingする」という慣習を採用していますが、これは8倍も非効率的であり、実際の最適な間隔は約4分であることが測定されています。Anthropic、OpenAI、Gemini、DeepSeekの4つの主要プロバイダーで検証した結果、30秒間隔のPingはほとんどのプロバイダーでコスト増につながり、4分間隔がコスト削減に寄与するのはAnthropicのみでした。(出典)
これは、各プロバイダーのバックエンドにおけるキャッシュ管理戦略が大きく異なることを示唆しています。開発者側から見ると、プロバイダーごとの挙動の違いを理解し、それに合わせてキャッシュ戦略を調整することが極めて重要だと感じます。私の自宅のRTX 3090でローカルLLMを動かす場合、KVキャッシュはGPUメモリ上に直接保持されるため、外部からのアクセスやネットワーク転送といった問題は生じませんが、クラウドAPIを利用する際はプロバイダーの動向を常に注視する必要があると感じます。
エージェント設計への影響と今後の展望
プロンプトキャッシュの挙動は、単なる実装の詳細や最適化に留まりません。それは、エージェントのレイテンシ、運用コスト、ツール設計、セッション設計、さらには提供すべき製品機能にまで影響を与えます。(出典)
例えば、ツールの定義が変更されたり、利用モデルが切り替わったり、あるいはプロバイダーのルーティングポリシーが変更されたりすると、期待していたはずの安価な増分リクエストが、コンテキスト全体の完全な再計算(フルリプレイ)になってしまう可能性があります。これは開発者にとって予測困難なコスト増に直結します。
キャッシュの挙動は、エージェントのパフォーマンスとコスト効率を左右する、設計上不可欠な要素です。
この課題に対し、エージェント設計者は以下の点を考慮する必要があります。
- ツール定義の安定性: ツールの変更はキャッシュの失効につながるため、頻繁な変更を避け、安定したツールセットを維持する。
- セッション管理の最適化: 不要なコンテキストを適宜削除し、キャッシュされるプロンプトサイズを最小限に抑える。
- プロバイダー選定とベンチマーク: 各LLMプロバイダーのキャッシュ挙動を理解し、自身のワークロードに最適なプロバイダーを選択する。必要であれば、Ping間隔などのKeep-alive戦略を調整する。特に、先述の参考記事のように、プロバイダー間のキャッシュ保持ポリシーを比較することは非常に有効でしょう。
これらの点を踏まえることで、より堅牢でコスト効率の良いLLMエージェントを構築できるのではないでしょうか。個人的には、GCPのCloud Runでエージェントを運用している場合、コンテナの起動・停止とキャッシュのライフサイクルがどう連携するのか、BigQueryで管理するデータとLLMのコンテキスト情報をどのように同期させるかといった点も気になります。これらのクラウドインフラの特性を考慮したキャッシュ戦略は、今後のエージェント開発において重要な要素になると感じています。