Alejandro Rioja.
AI Agents Operations

Claude API のプロンプトキャッシュ:モデルを変えずに入力コストを削減する

Alejandro Rioja
Alejandro Rioja
4 分で読める
TL;DR

プロンプトキャッシュは、大きく安定した入力 — システムプロンプト、ツール定義、few-shot の例 — のコストを、繰り返しのリクエストでは通常の入力料金のおよそ 10% にまで削減します。その仕組みはプレフィックス一致です。安定したコンテンツの末尾に cache_control マーカーを置き、それより後はすべて可変な内容にします。キャッシュのヒット率を台無しにするミスは、タイムスタンプや UUID がプレフィックスに紛れ込んでしまうことです。

無料ニュースレター

毎週水曜。28,400人以上の読者。無駄なし。

Table of contents

Open Table of contents

プロンプトキャッシュが実際に行うこと

Claude API へのすべての呼び出しはトークンを送信します。キャッシュがなければ、リクエスト内のすべてのトークン — システムプロンプト、ツール定義、few-shot の例、そしてユーザーメッセージ — は通常の入力料金で課金されます。キャッシュを使うと、それらのトークンのプレフィックスが最初のリクエストの後に Anthropic のサーバーに保存されます。その同じプレフィックスを共有する後続のリクエストでは、ゼロから再処理する代わりにキャッシュの読み取り料金を支払うことになります。

コストの差は本物です:

損益分岐点を超えると — これは 1 日に数回以上動くエージェントなら早々に起こります — それ以降のキャッシュヒットはすべて、それらのトークンに対する約 90% の割引になります。

プレフィックス一致の不変条件

これが、他のすべてが従う唯一のルールです: キャッシュキーは、レンダリングされたプロンプトのプレフィックス一致である

Anthropic のサーバーは、プロンプトの先頭から cache_control マーカーまでのレンダリングされたコンテンツを保存します。次のリクエストでキャッシュヒットが起こるためには、プロンプトの先頭からそのマーカーまでのすべてのトークンが、バイト単位で同一でなければなりません。

プレフィックス一致のレンダリング順序は: tools → system → messages です。つまり、まず tools 配列がハッシュ化され、次に system ブロック、その後 messages が順番に処理されます。

これが実務で意味するのは: 安定したコンテンツが最初に来なければならない、ということです。システムプロンプトが何か動的なもの — 現在の日付、ユーザー ID、リクエストのトレース ID — を参照していて、それが cache_control マーカーよりに現れる場合、プレフィックスが変わり続けるため、キャッシュは毎回のリクエストでミスします。

キャッシュマーカーをどこに置くか

最もレバレッジの高い対象は次のとおりです:

1. システムプロンプト

システムプロンプトは通常、最も大きな安定ブロックです。詳細なエージェントのペルソナ、振る舞いに関するルールのリスト、出力フォーマットの指示一式 — これらすべては、同じエージェントの呼び出しごとに同一です。マークしましょう:

typescript
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();

const response = await client.messages.create({
  model: "claude-opus-4-8",
  max_tokens: 1024,
  system: [
    {
      type: "text",
      text: `You are a content operations agent for alejandrorioja.com.
Your job is to draft blog posts in Alejandro's voice: direct, practitioner, 
first-person, numbered lists, honest caveats. No hedging. No filler. 
Every section must earn its place.

[... 2000 more tokens of stable instructions ...]`,
      cache_control: { type: "ephemeral" },
    },
  ],
  messages: [
    {
      role: "user",
      content: "Draft a post about prompt caching.",
    },
  ],
});

system ブロックの cache_control: { type: "ephemeral" } は、そのブロックまで(それを含む)すべてをキャッシュするよう Claude に指示します。messages 配列は可変 — リクエストごとに異なる — であり、キャッシュ境界の外側にとどまります。

2. ツール定義

エージェントがツールを使う場合、それらの定義は相当な量になり得ます。description、パラメータ名、enum 値を備えた、よく文書化されたツールスキーマは、1 ツールあたり 500〜1,000 トークンに達することがあります。5 つのツールがあれば、毎回の呼び出しで再処理のために支払うトークンは最大 5,000 トークンになります:

typescript
const response = await client.messages.create({
  model: "claude-opus-4-8",
  max_tokens: 1024,
  tools: [
    {
      name: "search_airtable",
      description: "Search the Airtable content queue...",
      input_schema: { type: "object", properties: { query: { type: "string" } } },
    },
    // ... more tools ...
    {
      name: "post_to_kit",
      description: "Schedule a broadcast via the Kit API...",
      input_schema: { /* ... */ },
      // Mark the last tool to cache the entire tools array
    } as Anthropic.Tool & { cache_control: { type: "ephemeral" } },
  ],
  system: "...",
  messages: [...],
});

配列の最後のツールをマークします。プレフィックス一致は、その地点から tools 配列全体をカバーします。

3. messages 内の few-shot の例

messages 配列の前方のメッセージとして静的な few-shot の例を渡す場合、それらもキャッシュできます。最初の N 個のメッセージとして構成し、最後の例のターンをマークします:

typescript
const messages: Anthropic.MessageParam[] = [
  {
    role: "user",
    content: [
      {
        type: "text",
        text: "Here are examples of posts in my voice:\n\n[Example 1...]\n\n[Example 2...]",
        cache_control: { type: "ephemeral" },
      } as Anthropic.TextBlockParam & { cache_control: { type: "ephemeral" } },
    ],
  },
  {
    role: "assistant",
    content: "Understood. I'll follow that voice.",
  },
  // The actual user turn follows — this is volatile, no cache marker
  {
    role: "user",
    content: actualUserRequest,
  },
];

キャッシュしてはいけないもの(サイレントな無効化要因)

これらは安定しているように見えて、そうではないものです — そしてあなたのヒット率を静かに台無しにします。API は警告してくれません。毎回のリクエストで cache_creation_input_tokens が表示され、なぜなのかと首をかしげることになるだけです。

システムプロンプト内のタイムスタンプ。 最もよくある単一のミスです:

typescript
// This invalidates the cache on every request
const system = `You are an agent. Current time: ${new Date().toISOString()}`;

タイムスタンプは、それがふさわしい場所であるユーザーメッセージへ移しましょう:

typescript
// Stable system prompt — cacheable
const system = `You are an agent. Use the current time provided by the user.`;

// Volatile user message — not cached
const userMessage = `Current time: ${new Date().toISOString()}. Run the daily brief.`;

ランダムな UUID とトレース ID。 同じ問題です。ログ記録のために system ブロックにトレース ID を注入すると、毎回のリクエストで新しいプレフィックスになります。

非決定的な JSON シリアライズ。 オブジェクトをシステムプロンプトにシリアライズする際にキーの順序が保証されていないと、元になるデータが同じでも、レンダリングされた文字列が異なることがあります。安定したキー順序でシリアライズするか、テンプレート文字列を使いましょう。

動的な few-shot の選択。 現在のクエリに基づいて few-shot の例を選び、それをキャッシュされるプレフィックスに入れているなら、「安定した」プレフィックスをクエリ依存にしてしまっています。キャッシュ層には固定の例を使うと決めるか、動的な例をキャッシュされないメッセージのターンへ移しましょう。

キャッシュのヒット率を検証する

すべてのレスポンスには使用量のメタデータが含まれます。確認しましょう:

typescript
const response = await client.messages.create({ /* ... */ });

console.log({
  inputTokens: response.usage.input_tokens,
  cacheRead: response.usage.cache_read_input_tokens,
  cacheWrite: response.usage.cache_creation_input_tokens,
  outputTokens: response.usage.output_tokens,
});

最初のリクエストでは: cache_creation_input_tokens がゼロでない値になり、cache_read_input_tokens は 0 になります。これが書き込みです。

キャッシュヒットでは: cache_read_input_tokens がゼロでない値になり、cache_creation_input_tokens は 0 になります。これが読み取りです。

毎回のリクエストで cache_creation_input_tokens が見えているなら、プレフィックスが変化しています。各呼び出しの前に、レンダリングされたシステムプロンプトの最初の 200 文字を出力するログ文を追加しましょう — 浮動するタイムスタンプがあれば、すぐに目に飛び込んでくるはずです。

1 時間の TTL:追加の書き込みコストに見合うとき

デフォルトの TTL は 5 分です。エージェントが低頻度で動く場合 — 5 分に 1 回未満 — 読み取りを得られないまま、ほとんどのリクエストでキャッシュ書き込みコストを支払うことになります。

typescript
// Opt into a 1-hour TTL
cache_control: { type: "ephemeral", ttl: "1h" }

1 時間の書き込みコストは、1.25 倍ではなくベース入力料金の約 2 倍です。計算はこうです: 1 時間に 3 回以上キャッシュにヒットしているなら、1 時間の TTL は節約になります。エージェントが 1 日に 1 回しか動かないなら(私のデイリーブリーフのように)、1 時間の TTL でも助けにはなりません — 毎回書き込みコストを支払うことになります。その場合、システムプロンプトが膨大でない限り、キャッシュの恩恵はわずかです。

私のデイリーブリーフのエージェントは 3,000 トークンのシステムプロンプトを持ちますが、1 日に 1 回しか動きません。キャッシュは役に立ちません。私のニュースレターのエージェントは、ドラフト作成中に 1 セッションあたり何十回も動きます — キャッシュは大幅に節約してくれます。

事前ウォーミング:最初のリクエストを安くする

来ることが分かっているトラフィックの急増 — バッチジョブ、API のローンチ — があるなら、低コストのダミーリクエストでキャッシュを事前にウォーミングできます:

typescript
// Pre-warm: write the cache at near-zero output cost
await client.messages.create({
  model: "claude-opus-4-8",
  max_tokens: 1, // minimal output
  system: [{ type: "text", text: stableSystemPrompt, cache_control: { type: "ephemeral" } }],
  messages: [{ role: "user", content: "ping" }],
});

// Now the real requests read from cache

これが主に役立つのは、多数の並列リクエストを立ち上げていて、それぞれがキャッシュの書き込みを競い合うのではなく、温まったキャッシュにヒットしてほしいバッチ処理の場合です。

エージェント型ループにおけるプロンプトキャッシュ

マルチターンのエージェント型ループでは、会話の履歴がターンごとに増えていきます。キャッシュはこれをうまく扱えるほど賢くできています: 20 ブロックの遡及ウィンドウを使い、直近 20 個のコンテンツブロックの中で最も長く一致するプレフィックスを見つけます。

実務上の含意は: 安定したコンテンツ(システムプロンプト、ツール定義)を一番上に固定しておく、ということです。messages 配列の末尾で増えていく会話履歴は、安定ブロックのプレフィックス一致を壊しません — それらは可変なコンテンツより前にあり、プレフィックス一致は一番上から始まるからです。

実際、私のエージェントはターンを次のように構成しています:

code
System (cached) → Tools (cached) → Few-shot (cached) → Turn 1 → Turn 2 → ... → Current turn

キャッシュは few-shot のマーカーまでのすべてをカバーします。その後で増えていくターンの履歴は毎回再処理されますが、それで構いません — それらのトークンはセッション固有であり、安定したプレフィックスに比べれば小さいからです。

請求書での見え方

高頻度のエージェントを例に取りましょう: 1 日 100 回の呼び出し、4,000 トークンのシステムプロンプト、Sonnet の料金。

キャッシュなし:

キャッシュあり(5 分の TTL、ピーク時に 50 回/時と仮定):

これは、それらの入力トークンに対しておよそ 90% の削減です。規模が大きくなると — 1 日 1,000 回の呼び出し — 差はさらに積み重なります。そしてこれは、Haiku 対 Sonnet の計算によるモデルルーティングの節約に上乗せされるものです: キャッシュはどのティアでも機能します。

実務者としての結論

プロンプトキャッシュは Claude API において最も簡単なコスト最適化です: すでに書いているコンテンツブロックに 1 つフィールドを追加するだけです。制約はプレフィックスの安定性に対する規律です — キャッシュマーカーより前に動的なものを置かないこと。システムプロンプト、ツール、そして静的な例を可変なコンテンツから解放しておけるなら、キャッシュヒットごとに通常の入力コストの約 10% を支払うだけで済みます。大きく安定したプロンプトを持つ高頻度のエージェントにとって、これはモデルのティアを切り替えるよりも大きなレバーです。


関連記事: AI エージェントのコスト計算:Haiku が Sonnet に勝つとき · イベント駆動型 対 スケジュール型のエージェント · ビジネスを回すために私が実際に使っている 5 つの AI ツール

続きを読む

AIプレイブックをメールでお届け

毎週水曜。28,400人以上の読者。無駄なし。

↵ すべての結果を見る esc esc で閉じる