yoi/tickets/anthropic-prompt-cache.md

118 lines
6.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Anthropic プロンプトキャッシュの有効化
レビュー中: [anthropic-prompt-cache.review.md](anthropic-prompt-cache.review.md)
## 背景
Anthropic プロバイダ経由のリクエストで prompt caching が一切機能していない。セッションログの `cache_read_tokens` / `cache_write_tokens` が常に 0、`AnthropicScheme::build_request` に `cache_control: ephemeral` の breakpoint が一つも入っていない。
結果:
- 毎ターン system prompt + tools + 全履歴が full-price / full-token で再送信される
- 30k ITPM 帯の組織では、数ターン debug を回しただけで `rate_limit_error` (429) に到達する。実例:`ListPods` → `SpawnPod`(失敗)→ `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 されるだけで意味がないので採用しない。
### 実装方針
`AnthropicContentPart``cache_control` フィールドを追加。Anthropic の API 形式:
```json
{ "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別課題