yoi/tickets/anthropic-prompt-cache.md

6.6 KiB
Raw Blame History

Anthropic プロンプトキャッシュの有効化

レビュー中: anthropic-prompt-cache.review.md

背景

Anthropic プロバイダ経由のリクエストで prompt caching が一切機能していない。セッションログの cache_read_tokens / cache_write_tokens が常に 0、AnthropicScheme::build_requestcache_control: ephemeral の breakpoint が一つも入っていない。

結果:

  • 毎ターン system prompt + tools + 全履歴が full-price / full-token で再送信される
  • 30k ITPM 帯の組織では、数ターン debug を回しただけで rate_limit_error (429) に到達する。実例:ListPodsSpawnPod(失敗)→ Glob(大量出力)→ Read のデバッグシーケンスで 429
  • 長期セッションのコストが本来の ~10 倍になっている(キャッシュなら cache read は通常 input の ~10%

Anthropic の自動キャッシュ(モデル・時期依存)の有無に関わらず、明示的 breakpoint は自分で制御できる確実な手段。

依存

  • なし。crates/llm-worker/src/llm_client/scheme/anthropic/request.rs 単独

設計

Breakpoint 戦略

Anthropic の breakpoint は「その位置までの prefix をキャッシュする」ので後方が前方を subsume する。前方 breakpoint は後方キャッシュが TTL で失効した時の fallback として機能する。

安定度の異なる 3 層に置く:

位置 前進するタイミング 主な用途
Prune 位置 compaction 走行時 超長期フォールバック
最後のターン末 ターン完了時 次ターン以降の read
新規リクエスト Head 毎 LLM コール(= messages 末尾に追従) 同ターン内の tool round での read

期待される挙動

1 ターン内で M 回の tool roundagent loop:

  • Call 1: 最後のターン末 = 前ターンの終わり → read、新規 Head = Call 1 の messages 末尾に creation
  • Call 2: 新規 Head前 Call の末尾)→ read、新しい Head を messages 末尾に creation
  • ...K 回目も同様)

ターン全体の累積入力コスト: O(K²) → O(K) に改善。

ターン N+1 開始時:

  • 最後のターン末 = ターン N 最終状態 → read
  • Head = N+1 最初の Call の末尾に creation

compaction 走行時:

  • Prune 位置が前進、以降 read

TTL 耐性

5 分 TTL で最新が失効しても段階的に fallback

  • 新規 Head 失効 → 最後のターン末 で read
  • 最後のターン末 失効 → Prune 位置 で read
  • Prune 位置 失効 → compaction 境界から re-create

4 枠のうち 3 枠使用

残り 1 枠は将来の拡張用tools 配列を別 TTL で管理したい場合など)に温存。

前方に system 単独 / tools 単独の breakpoint を打つ案は subsume されるだけで意味がないので採用しない。

実装方針

AnthropicContentPartcache_control フィールドを追加。Anthropic の API 形式:

{ "type": "text", "text": "...", "cache_control": { "type": "ephemeral" } }

Option<CacheControl> で持ち、値がある場合のみシリアライズ(skip_serializing_if)する。既存の非 Anthropic プロバイダOpenAI / Gemini / Ollamaには影響させない — Anthropic scheme 内部型のみ。

3 つの breakpoint 位置の決定:

  • Prune 位置: 既存 compaction 機構(pod::compact_state か Pod 内で管理される prune 済みインデックス)から取得。build_request 経路で Anthropic scheme に prune インデックスを渡す API 拡張が必要
  • 最後のターン末: Pod / Worker 側が既にターン境界を記録しているならそれを使う。無ければ messages 逆走査で「直近 user メッセージの 1 つ前」を探す
  • 新規リクエスト Head: messages.last() に付ける(= 現行 request の末尾)

3 箇所が重なる場合(例: 初回 request で Prune 位置・ターン末・Head が全部同じ itemは重複を除去して実質 1 breakpoint にする。

自動テスト

  • breakpoint が Prune 位置 / 最後のターン末 / 新規リクエスト Head の 3 箇所に付いたリクエスト JSON が生成されること
  • Prune 位置が 0compaction 未走行)のケースでは 2 箇所(ターン末 + Headのみに付くこと
  • ターン末と Head が重なる最終 request= 新ターンの最初の Callでは 2 箇所に縮退すること
  • 3 箇所が全て重なる初回 request では 1 箇所に縮退すること
  • OpenAI / Gemini の request 生成が一切変わらないことAnthropic 専用だが回帰防止)

影響範囲

  • crates/llm-worker/src/llm_client/scheme/anthropic/request.rs: 内部型 + breakpoint 配置ロジック
  • crates/llm-worker/src/llm_client: Prune 境界インデックスを build_request 経路で渡す API 拡張(Request に prune hint を足すか、別経路で scheme に伝える)
  • crates/pod/src/pod.rs or compact_state.rs: 現在の prune 済み件数を読み出せるようにする(既に内部で管理されているはず、公開 API 化)
  • 既存の serde round-trip テスト: 追加フィールドを skip_serializing_if で出さないので差分なし

完了条件

  • Anthropic リクエストの Prune 位置compact 済みサマリ末尾)に cache_control: ephemeral が付く
  • Anthropic リクエストの最後のターン末(直近 user メッセージの直前)に cache_control: ephemeral が付く
  • Anthropic リクエストの新規リクエスト Headmessages 末尾)に cache_control: ephemeral が付く
  • Prune 位置 / ターン末 / Head が重なる場合は重複除去される
  • 実セッションで cache_read_tokens が 2 コール目以降に非 0 になる
  • 特に同一ターン内の 2 回目以降の LLM コールで直前の tool_result 以前が cache read されること
  • 既存の Anthropic / OpenAI / Gemini テストが全 pass
  • cache_control が正しい位置に入ることを検証する新規ユニットテスト

範囲外

  • OpenAI / Gemini の prompt caching各プロバイダの API 設計が違うため別チケット)
  • 動的な breakpoint 数の調整4 枠目を状況により使い分ける、など)。まずは固定 3 箇所
  • Cache hit 率の可観測化TUI 表示など)。集計は cache_read_tokens として既に記録されるので、表示は別途
  • Rate limit 429 を受けた際の retry-after honor別課題