From ec3bf7324b0eb272ff0aeed77334b605d05dbc6b Mon Sep 17 00:00:00 2001 From: Hare Date: Sun, 19 Apr 2026 12:07:03 +0900 Subject: [PATCH] =?UTF-8?q?anthropic-cache=E5=AE=8C=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TODO.md | 1 - tickets/anthropic-prompt-cache.md | 117 ----------------------- tickets/anthropic-prompt-cache.review.md | 75 --------------- 3 files changed, 193 deletions(-) delete mode 100644 tickets/anthropic-prompt-cache.md delete mode 100644 tickets/anthropic-prompt-cache.review.md diff --git a/TODO.md b/TODO.md index 58f38a26..974d878a 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,4 @@ - [ ] テスト設計 → [tickets/test-design.md](tickets/test-design.md) -- [ ] Anthropic プロンプトキャッシュの有効化 → [tickets/anthropic-prompt-cache.md](tickets/anthropic-prompt-cache.md) - [ ] ツール設計 - [ ] Bash ツール (Permission 層と統合) → [tickets/bash-tool.md](tickets/bash-tool.md) - [ ] Compact の改善(要約品質 + 挙動詳細) → [tickets/compact-improvements.md](tickets/compact-improvements.md) diff --git a/tickets/anthropic-prompt-cache.md b/tickets/anthropic-prompt-cache.md deleted file mode 100644 index d51d38b2..00000000 --- a/tickets/anthropic-prompt-cache.md +++ /dev/null @@ -1,117 +0,0 @@ -# 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 round(agent 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` で持ち、値がある場合のみシリアライズ(`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 位置が 0(compaction 未走行)のケースでは 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 リクエストの新規リクエスト Head(messages 末尾)に `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(別課題) diff --git a/tickets/anthropic-prompt-cache.review.md b/tickets/anthropic-prompt-cache.review.md deleted file mode 100644 index bec466dd..00000000 --- a/tickets/anthropic-prompt-cache.review.md +++ /dev/null @@ -1,75 +0,0 @@ -# Review: anthropic-prompt-cache - -実装は未コミット(staged + unstaged)。`cargo build` clean、`cargo test --workspace` 467 / 0 fail。 - -## 総評 - -チケット要件を忠実に反映。3 層 breakpoint(Prune 位置 / 最後のターン末 / 新規 Head)を `BTreeSet` で管理し重複除去、Anthropic scheme のみに局所化、OpenAI / Gemini は透過的に無視。エッジケース(empty / out-of-range / overlap / shorthand 保持)まで網羅。軽微な指摘のみで blocker なし。 - -## 完了条件の対応 - -| 要件 | 状態 | 根拠 | -|---|---|---| -| Prune 位置に `cache_control` | ✅ | `compute_breakpoints` anchor 分岐、`three_breakpoints_when_compact_plus_prior_turn` | -| 最後のターン末に `cache_control` | ✅ | `last_user - 1` 計算、同テスト | -| 新規 Head に `cache_control` | ✅ | `items.len() - 1`、`two_breakpoints_without_compaction` | -| 3 箇所重複の除去 | ✅ | `BTreeSet` で自動重複削除、`overlap_collapses_*` / `single_breakpoint_*` | -| 実セッションで cache_read 非 0 | ⚠️ | 結合で要確認(自動テスト範囲外、実運用で観測) | -| 既存テスト全 pass | ✅ | workspace 467 / 0 fail | -| 新規ユニットテスト | ✅ | 10 件(シリアライズ形状検証含む) | - -## 良い点 - -1. **層構造が素直に分離**: `Request::cache_anchor: Option` を追加、`Worker::set_cache_anchor` で透過的に伝播、Anthropic scheme のみが実際の breakpoint 配置を知る。OpenAI / Gemini は透過的に無視して回帰なし。 - -2. **`Pod::compact` / `Pod::from_state` の両方で anchor を復旧**: `from_state` は `history[0]` が `Role::System` かどうかで compact の痕跡を検出して anchor を張る。resume 時のキャッシュ継続性が確保されている。 - -3. **`force_parts` で breakpoint 位置だけ array 形式に落とす**: 通常は text shorthand を保ちつつ、breakpoint を付ける位置だけ array 化。リクエストサイズを最小化する配慮(`single_text_message_uses_text_shorthand_without_breakpoint` で検証)。 - -4. **シリアライズ形状の検証**: `serialized_json_shape_matches_anthropic_spec` で `{"type":"ephemeral"}` が正しい階層に出ることを明示的にテスト。Anthropic spec からずれたら即発見できる。 - -5. **エッジケース網羅**: 空 items / out-of-range anchor / 1 item のみ / tool_result が head になるケース / Parts 強制化 / tool 定義に cache_control が漏れないこと、が全部テスト。 - -6. **冪等な anchor 範囲チェック**: `request.cache_anchor = self.cache_anchor.filter(|&anchor| anchor < context.len())`。prune が先頭をトリムしても安全。 - -## 指摘と判断 - -### 軽微 - -#### 1. `anchor=0` に `compact` が暗黙依存 - -```rust -// pod.rs :872 -worker.set_history(new_history); -worker.set_cache_anchor(Some(0)); -``` - -`compact` が history[0] に summary を置く前提で `Some(0)` をハードコード。将来 compact のレイアウトを変えたら breaker になる。コメントで invariant を明記してあるので read 可能だが、`compact_state` 側に「summary の位置」を返す API を置いて受け渡す方が健全。 - -**判断**: 対応は任意。現状は compact の設計上 history[0] が summary であることが不変、from_state 側の検出ロジックも Role::System を見ているので、レイアウトを変えるなら両方を直すことになる。今回は放置で OK。 - -#### 2. `Request::cache_anchor` が raw フィールド代入 - -既存の `Request` builder は fluent(`Request::new().user(...).item(...)`)だが、`cache_anchor` のみ `request.cache_anchor = Some(0)` の直代入。テスト内で何度も使われていて少し浮く。`fn cache_anchor(self, anchor: Option) -> Self` を足して fluent を揃える方が一貫性が良い。 - -**判断**: スタイルの範囲。機能に影響なし、後で揃えても良い。本チケットでは見送り可。 - -#### 3. 「直近 user msg の 1 つ前」が tool_call になりうるケース - -前ターンが interrupted で最後が `tool_call`(tool_result 未応答)だった場合、turn_end が tool_call を指す。Anthropic 的には tool_use にも cache_control を付けられるので wire 上は OK。意味論的に「ターン末」と呼ぶには微妙だが、キャッシュ位置としての機能は成立(その prefix が安定ならキャッシュが効く)ので実害なし。 - -**判断**: 仕様上許容。注記不要。 - -#### 4. Tools array 自体にキャッシュマーカーを付けない選択 - -ticket で「tools 別 TTL 管理は将来」と明記されており、今は意図的に off。tools は message-level breakpoint の prefix に含まれるので、実質的に system + tools + history のまとまりで cache される。問題なし。 - -**判断**: ticket 通り。 - -## 完了に向けた作業 - -- 必須修正なし -- 指摘 1〜2 は style の範囲、本チケットでは対応不要 -- 実セッションで cache_read_tokens が 2 コール目以降に非 0 になるかは実運用で確認(LLM への実アクセスが必要、CI の範囲外) - -**完了 OK**。