· Engineering

Claude Codeの構築から得た教訓:プロンプトのキャッシュこそがすべて

Lessons from building Claude Code: Prompt caching is everything

この記事は 原文 を DeepL で機械翻訳したものである。正確な内容は原文を参照のこと。

工学の世界では、「キャッシュがすべてを支配している」とよく言われますが、エージェントについても同じことが言えます。

Claude Codeのような長期にわたって実行されるエージェント型製品は、プロンプトキャッシュによって実現されています。これにより、前回のラウンドトリップでの計算結果を再利用でき、レイテンシとコストを大幅に削減することが可能になります。

Claude Codeでは、プロンプトキャッシュを中核としてシステム全体を構築しています。プロンプトキャッシュのヒット率が高いとコストが削減され、サブスクリプションプランのレート制限をより寛容に設定できるため、プロンプトキャッシュのヒット率に対してアラートを設定し、ヒット率が低すぎる場合はSEVを発令しています。

これらは、大規模なプロンプトキャッシュの最適化を通じて私たちが学んだ(しばしば直感に反する)教訓です。

キャッシュ用のプロンプトを設定する

図A 『Claude Code』のシステムプロンプトは、安定した部分がキャッシュされたまま維持され、会話そのものがターンごとにのみ増加するように構成されています。
図A 『Claude Code』のシステムプロンプトは、安定した部分がキャッシュされたまま維持され、会話そのものがターンごとにのみ増加するように構成されています。

プロンプトのキャッシュはプレフィックス一致によって機能します。つまり、APIはリクエストの先頭から各 cache_control ブレークポイントまでのすべてをキャッシュします。つまり、要素を配置する順序が極めて重要であり、できるだけ多くのリクエストでプレフィックスを共有できるようにする必要があります。

プロンプトの構成方法は、キャッシュヒットだけでなく、出力の品質にも影響します。プロンプトエンジニアリングの基礎知識こそが、この分野のもう一つの重要な要素なのです。

これを行う最善の方法は、静的コンテンツを先に、動的コンテンツを最後に配置することです。Claude Code の場合、次のような構成になります:

  1. 静的システムプロンプトとツール(グローバルにキャッシュ)
  2. CLAUDE.md(プロジェクト内にキャッシュされている)
  3. セッションコンテキスト(セッション内にキャッシュされる)
  4. 会話のメッセージ

こうすることで、キャッシュヒットを共有するセッションの数を最大化できます。

しかし、このアプローチは意外にも脆弱な場合があります。これまで、静的なシステムプロンプトに詳細なタイムスタンプを挿入したり、ツールの順序定義を非決定論的に並べ替えたり、ツールのパラメータ(例えば、「Agent」ツールが呼び出せるエージェントなど)を更新したりするなど、さまざまな理由でこの順序が崩れてしまったことがあります。

最新情報はメッセージをご利用ください

プロンプトに入力した情報が古くなってしまう場合があります。例えば、時間が経過した場合や、ユーザーがファイルを変更した場合などです。プロンプトを更新したくなるかもしれませんが、そうするとキャッシュミスが発生し、ユーザーにとってかなりの負荷となる可能性があります。

代わりに、エージェントの次のターンでメッセージを介してこの情報を渡すことはできないか検討してみてください。Claude Code では、次のユーザーメッセージまたはツール結果に、モデル用の更新された情報を含む <system-reminder> タグを追加しており、これによりキャッシュを維持しやすくなります。

セッションの途中でモデルを変更しないでください

プロンプトキャッシュはモデルごとに固有のものであり、このためプロンプトキャッシュの仕組みはかなり直感に反するものになりがちです。

たとえば、Opus との会話で 100k トークンを消費した後、比較的答えやすい質問をしたい場合、Opus に回答してもらうよりも、Haiku に切り替えるほうが実際にはコストが高くなります。これは、Haiku に対してプロンプトキャッシュを再構築する必要があるためです。

モデルを切り替える必要がある場合、サブエージェントを利用するのが最善の方法です。上記の例をさらに発展させると、Opusに対し、実行すべきタスクについて別のモデルへの「引き継ぎ」メッセージを作成するよう促すサブエージェントを展開することができます。私たちは、Claude CodeのExploreエージェントでこの手法を頻繁に利用しており、そこではHaikuが使用されています。

セッションの途中でツールを追加したり削除したりしてはいけません

会話の途中でツールセットを変更することは、プロンプトのキャッシュを無効にしてしまう最も一般的な原因の一つです。これは直感的に理解できることかもしれません――モデルには、その時点で必要だと考えるツールだけを渡すべきだ、と。しかし、ツールはキャッシュされたプレフィックスの一部であるため、ツールを追加または削除すると、会話全体のキャッシュが無効になってしまいます。

プランモードを使用してキャッシュを考慮した設計を行う

「プランモード」は、キャッシュの制約を考慮して機能を設計した好例です。直感的なアプローチとしては、ユーザーがプランモードに入ると、ツールセットを読み取り専用ツールのみを含むように切り替えるという方法がありますが、それではキャッシュが破綻してしまいます。

その代わりに、すべてのツールを常にリクエスト内に保持し、「EnterPlanMode」と「ExitPlanMode」自体をツールとして使用します。 ユーザーがプランモードをオンにすると、エージェントは、現在プランモードにあること、および「コードベースを調査し、ファイルを編集せず、計画が完了したらExitPlanModeを呼び出す」という指示を説明するシステムメッセージを受け取ります。ツールの定義は決して変更されません。

これにはもう一つの利点があります。EnterPlanModeはモデル自身が呼び出すことができるツールであるため、モデルは困難な問題を検知した際に、キャッシュの破綻を招くことなく、自律的にプランモードに移行することができます。

削除する代わりに、ツール検索機能を使って先送りする

この原則は、当社のツール検索ツールにも当てはまります。Claude Code には、数十もの MCP ツールが読み込まれている場合がありますが、それらすべてをリクエストごとに含めるのは負荷が大きくなります。しかし、処理の途中でそれらを削除してしまうと、キャッシュが破損してしまいます。

当社の解決策: defer_loading. ツールを削除する代わりに、軽量なスタブ(ツール名のみ、 defer_loading: true)をモデルがツール検索を通じて必要に応じて「検出」できるようにします。完全なツールスキーマは、モデルがそれらを選択したときにのみ読み込まれます。これにより、常に同じスタブが同じ順序で存在するため、キャッシュされたプレフィックスが安定した状態を保つことができます。

また、当社のAPIを通じて検索ツールを利用することで、この作業を簡略化することもできます。

キャッシュを破棄せずに圧縮する

図 B コンテキストウィンドウがいっぱいになると、Claude Codeは会話を要約するためのキャッシュされた呼び出しをフォークし、元のメッセージの代わりに要約を表示して処理を再開します。
図 B コンテキストウィンドウがいっぱいになると、Claude Codeは会話を要約するためのキャッシュされた呼び出しをフォークし、元のメッセージの代わりに要約を表示して処理を再開します。

コンテキストウィンドウの範囲外に出ると、コンパクションが発生します。これまでに交わされた会話を要約し、その要約を基に新しいセッションを継続します。

コンパクションとプロンプトのキャッシュは、誤りやすい形で相互に影響し合います。会話をコンパクションするには、モデルが要約を作成できるよう、会話の全文をモデルに送信する必要があります。 その最も簡単な方法は、独自のシステムプロンプト(「これを要約してください」のようなもの)を使用し、ツールを一切指定しない別のAPI呼び出しを行うことですが、まさにそこにコストの落とし穴があります。プロンプトのキャッシュは、リクエストのプレフィックスが、最初からバイト単位で既にキャッシュされているものと完全に一致する場合にのみ適用されます。 メインの会話は、1つのシステムプロンプトとツールセットの下でキャッシュされていますが、要約の呼び出しでは別のシステムプロンプトが使用され、ツールは添付されていないため、プレフィックスは最初のトークンで分岐し、キャッシュは一切適用されません。 その結果、送信する会話全体に対して、キャッシュされていないフル入力レートが課金されてしまいます。そして、会話が長ければ長いほど(つまり、そもそも圧縮処理が必要になるほど)、その1回の呼び出しにかかるコストは高くなります。

解決策:キャッシュセーフなフォーク

コンパクションを実行する際は、親の会話とまったく同じシステムプロンプト、ユーザーコンテキスト、システムコンテキスト、およびツール定義を使用します。親の会話メッセージを先頭に配置し、その最後にコンパクションのプロンプトを新しいユーザーメッセージとして追加します。

APIの観点から見ると、このリクエストは親の前回リクエストとほぼ同じように見えます(プレフィックス、ツール、履歴がすべて同じであるため)、そのためキャッシュされたプレフィックスが再利用されます。新しいトークンは、コンパクションのプロンプト自体のみです。

ただし、これは、コンテキストウィンドウにコンパクトメッセージと要約出力トークンを表示するための十分なスペースを確保するために、「圧縮バッファ」を保存しておく必要があることを意味します。

コンパクションは扱いが難しいですが、幸いなことに、これらのノウハウを自分で学ぶ必要はありません。『Claude Code』での知見をもとに、コンパクション機能をAPIに直接組み込んだため、皆様はご自身のアプリケーションでこれらのパターンをそのまま活用することができます。

得られた教訓

エージェントを構築する際に、プロンプトのキャッシュを最適化するために役立つと私たちが実感したパターンをいくつかご紹介します:

  1. プロンプトのキャッシュはプレフィックス一致です。プレフィックス内のどこかで変更があると、それ以降のすべてが無効になります。この制約を踏まえてシステム全体を設計してください。順序を正しく設定すれば、キャッシュの大部分は自動的に機能します。
  2. システムプロンプトの変更ではなく、メッセージを活用しましょう。プランモードへの切り替えや日付の変更などを行うために、システムプロンプトを編集したくなるかもしれませんが、実際には、会話の中でメッセージにこれらの内容を盛り込むほうが望ましいでしょう。
  3. 会話の途中でツールやモデルを変更しないでください。ツールセットを変更するのではなく、ツールを使って状態遷移(プランモードなど)をモデル化してください。ツールを削除するのではなく、ツールの読み込みを後回しにしてください。
  4. 稼働時間を監視するのと同じように、キャッシュのヒット率も監視してください。当社はキャッシュの不具合を検知するとアラートを発し、それをインシデントとして扱います。キャッシュのミス率がわずか数パーセント上昇するだけで、コストやレイテンシに劇的な影響が及ぶ可能性があります。
  5. フォーク操作では、親のプレフィックスを共有する必要があります。サイド計算(コンパクション、集計、スキルの実行)を実行する必要がある場合は、親のプレフィックスでキャッシュヒットが得られるよう、同一のキャッシュセーフなパラメータを使用してください。

Claude Code は、導入当初からプロンプトのキャッシュ機能を中核に据えて設計されています。エージェントを構築する際に最良の結果を得るためには、こちらも同様にキャッシュ機能を活用することをお勧めします。

今すぐClaude Codeを使い始めましょう。

この記事は、Claude Codeチームの技術スタッフであるThariq Shihipar氏が執筆しました。