第 39 章:Prompt Cache の安定性
Provider がキャッシュするのは意味ではなく接頭辞です。最初に異なる byte で再利用は終わります。
クイックトラック(5 分で核心をつかむ)
- キャッシュ可能性はシリアライズ契約であり、意味的な同値性は助けにならない
- 希少な Breakpoint を安定性で割り当てる:ユーザー横断、Tool Schema、ローリング履歴、Session 単位
- 変動値は最後の安定境界より後ろへ置き、時刻で Tools 接頭辞を汚さない
- 権限は実行時に検査し、変更のたびに Schema を並べ替えたり削除したりしない
- 実際に送った Request からそのまま Fork し、追記だけを許し、意図的な書き換えをすべて計測する
公開ソースのスナップショット:実装例は 2026-07-27 に確認した Kocoro 公開
origin/mainのコミット4ec6772に基づきます。学ぶべき対象は不変条件であり、定数は最新ソースで再確認してください。
39.1 何も変えずに $0.67 を使った書き換えから始める
第 35 章は、一見小さな切り詰めの Bug で終わりました。大きすぎる User Message を、現在の履歴から導いた境界で切り詰めます。追加指示が一つ入ると履歴が伸び、境界が動き、同じ Message が別の byte 位置で切られます。
見える意味はほとんど変わりません。Cache には新しい接頭辞に見えます。
記録された事例では、この動く切断点だけで Message 全体が毎回新規 Cache Creation として課金され、追加 Turn ごとに約 $0.67 かかりました。精度も速度も改善していません。同じ論理入力を毎回違う byte でシリアライズしただけです。
Prompt Cache は Provider が偶然見つける最適化ではなく、Request Builder が守るべきインターフェースだと捉えるのが有効です。同じ論理状態から違う byte が生成されるなら、キャッシュ可能な Prompt ではなく Cache のくじ引きです。
39.2 Cache Key は最初の差分で終わる
連続する二つの Request を考えます。
request N: system | tools | history A B C | current turn
request N+1: system | tools | history A B C | current turn | follow-up
理想的な形です。Request N が Request N+1 の byte-for-byte な接頭辞なので、高価で安定した部分を再利用できます。
では、前方にある無害そうな値を一つだけ動かします。
request N: system | tools [a,b,c] | history ...
request N+1: system | tools [b,a,c] | history ...
^
reuse ends here
Tool は同じ、集合も同じ、モデルにはどちらの順序も理解できます。接頭辞 Cache にとっては、どれも関係ありません。
ずれの原因はありふれています。Map の反復、Skill 起動後の Schema Filter、現在時刻の System Prompt への挿入、{} と null の揺れ、Compaction 中の履歴 Message の変更などです。対策も地味です。決定的な順序、正規エンコード、安定領域と変動領域の明示、そして Object Equality ではなくシリアライズ済み byte のテストです。
39.3 四つの Breakpoint は割り当て問題
公開スナップショットで使える Cache Breakpoint は四つです。Kocoro が内容を配置し、プレーンテキストの <!-- cache_break --> Marker を一つ出力し、Cloud がその配置を Provider の cache_control Block に変換します。
| 歴史的な番号 | Wire 上の位置 | 安定境界 |
|---|---|---|
| BP #1 | system[0] | 同じ OS のユーザー間で共有する Persona と中核ルール |
| BP #2 | tools[-1] | 完全かつ決定順の Tool Schema 配列 |
| BP #4 | messages[-2] 内の最後の適格 Block | ローリングする会話接頭辞 |
| BP #3 | 現在の User Message の接頭辞 | Session 単位の指示、Sticky Context、ユーザー別 Tool Catalogue |
番号は歴史的なもので、配置順ではありません。Wire 上では BP #4 が BP #3 より先です。BP #3 は「最初の User Message」でもありません。永続化履歴から Scaffold を除くため、Session がどれだけ長くても Request 内の <!-- cache_break --> は常に一つです。
これは細部ではありません。Rolling Marker を間違った Message に置けば履歴の再利用を失います。現在の Marker を消して古い Marker を残すと、合わせたかった Block の byte 自体を変えるかもしれません。論理領域を五つにしても、Provider が五つ目の Slot をくれるわけではありません。どこかを移すか統合する必要があります。
設計上の教訓は、誰が byte を共有し、どれくらいの頻度で変わるかに従って境界を割り当てることです。
cross-user stable → session-stable → rolling history → current-turn volatile
すべての値は、いずれかの寿命に属します。どれか説明できない値は、やがて最も高くつく場所へ誤配置されます。
39.4 安定と変動は位置で決まる
System Block は、本当にそれを共有するユーザー間で byte-identical でなければなりません。ユーザー別 MCP 名、Deferred Tool の一覧、設定由来の値は置けません。Session 単位の接頭辞か、変動する末尾へ移します。
現在日、作業ディレクトリ、Memory Recall、生の User Text、Skill 一覧、言語指示は <!-- cache_break --> の後ろに置きます。生成された Turn では未キャッシュですが、次の Turn では Rolling History の境界に吸収できます。
ラベルより位置が重要です。System Prompt 内に <!-- volatile --> を置く方がきれいに見えても、記録された実装ではその byte は Tool Breakpoint より前にありました。分ごとに変わる時刻が、Tools Cache まで毎分失効させます。コメントに「volatile」と書いても byte は見えなくなりません。効くのは Wire 上の位置だけです。
39.5 権限で接頭辞の形を変えない
第 38 章では Discovery と Authorization を分離しました。同じ分離が Cache も守ります。
Active Skill が allowed-tools を宣言したとき、許可されない Schema を次の Model Call からすべて除く実装は魅力的です。安全そうに見えますが、Run の途中で Tools 配列が変わります。BP #2 が Miss し、後続のすべての byte も再利用を失います。
スナップショットは Tools 配列を安定させ、Call を実行する直前に activeSkillFilter を実行時評価します。拒否された Tool は Denial Result を返し、実行されません。Security の所有者は保たれ、Authorization は Schema 接頭辞を変更しません。
この区別は重要です。
- Deferral はモデルが最初に見る Schema を決める。
- Authorization は実行できる Call を決める。
- Caching は双方に宣言済みの byte 境界を守らせる。
三つを一つの「Tool Filtering」へまとめると、互いを壊し始めます。
39.6 Request を Fork し、再構築しない
Turn 後の Suggestion や Speculative Call は、Main Request の温かい接頭辞を使いたがります。高水準の状態から Main Request を再構築するのは罠です。既定値一つ、Schema の順序一つ、Thinking Budget 一つの違いで Fork は Cold になります。
より安全な形は BuildForkedRequest(main, opts) です。Dispatch 時に捕捉した正確な Request から始め、新しい Messages Slice を確保し、Thinking Pointer の指す値をコピーし、Fork 固有の Message だけを追加します。Tools Slice は共有のままなので Read-only として扱います。関数が明示した差分だけを許します。
返却後に MaxTokens を下げる、Temperature を変える、Tools を絞る、Thinking を Clamp するといった「最適化」も避けます。これらも Cache 契約の一部です。一回の Cold Call が安くても、親接頭辞を失えば総額は高くなり得ます。
これは第 34 章の一般原則でもあります。公開 Seam で本物の Artifact を捕捉してください。再構築した近似物は、Provider に送った byte を保った証拠になりません。
39.7 Hit だけでなく Drift を測る
Cache Miss は再利用に失敗した事実を示しますが、最初に違った byte の位置も理由も教えません。
そこで、SHANNON_CACHE_DEBUG=1 がキャッシュ調査用の計測を有効にしている場合、意図的な書き換えは処理名、メッセージ位置、新旧ハッシュを記録します。要求ログと直前の書き換えを結び付ければ、Tier 1 圧縮、観測窓の刈り込み、画像除去、問い合わせ時の予算処理のどれかを特定できます。書き換えイベントがないのに接頭辞ハッシュが変わるなら、非決定的な直列化を疑います。
帰属と Policy も分けます。cache_source は Desktop、TUI、Channel、Schedule、Helper、One-shot のトラフィックを分類し、コストと Cache Ratio を比較するための値です。このスナップショットでは TTL を選びません。帰属 Field を Policy Switch にすると、文書化されていない二つ目の Cache System ができます。
Cache Read、Cache Creation、Latency、User Turn 当たりの Model Call、Task Success を併せて追跡します。Hit Rate が高くても Call が多すぎる場合があります。Creation/Read 比が低くても、Task が完了しなくなった Workload を隠せます。Cache Metric は運用シグナルであって製品そのものではありません。
39.8 スナップショットの根拠
| 観測 | 4ec6772 のソース |
|---|---|
| 動く切断境界で追加 Turn ごとに約 $0.67 の新規 Creation が発生 | window.go L32 |
| 四 Breakpoint の割り当てと Wire 上の正確な位置 | cache-strategy.md L16 |
| ユーザー横断 BP #1 の不変条件とユーザー別ルーティング | cache-strategy.md L38 |
変動内容は cache_break より後ろに置く | cache-strategy.md L49 |
| byte 安定性のための正規化ルール | cache-strategy.md L83 |
| 単一 Rolling Marker の判断と実測 Trade-off | cache-strategy.md L97 |
| Forked Request の byte-equality 契約 | forkedrequest.go L34 |
| Skill Allowlist は実行時に検査 | loop.go L2709 |
| キャッシュ調査イベントが意図的な接頭辞書き換えを新旧ハッシュで記録 | gateway.go L320 |
これは特定時点の実装と Provider 配置を述べたもので、普遍的な Cache 契約ではありません。
39.9 よくある失敗
Wire byte ではなく Object をテストする。 二つの Map が Deep Equal でも、順序違いでシリアライズできます。実際の Request 表現を Snapshot してください。
権限のために Schema を Filter する。 実行は安全でも BP #2 は Cold です。Authorization と Schema Presentation を分けます。
「変動」データを安定 Breakpoint より前に置く。 名前には効果がありません。Tools より前の Timestamp は Tools を無効化します。
Fork を再構築する。 既定値はずれます。Dispatch 済み Request を捕捉し、追記だけにします。
構築後に Fork を変更する。 小さな出力上限でも Cache Key Field を変え、Fork の目的だった再利用を壊せます。
古い Marker をすべて残す。 Marker が多いほど再利用が増えるわけではありません。固定上限のもとで Slot を空ける操作が、合わせたい Block 自体を変更し得ます。
cache_source を TTL Policy にする。 このスナップショットでは帰属です。Policy は Cache Control の所有者が持ちます。
Hit Rate だけを見る。 コスト、遅延、Call 数、完了品質を合わせて初めて有効性が分かります。
要点
- キャッシュ可能性はシリアライズ契約です。 意味が同じ Request でも byte が違えば Miss します。
- Breakpoint は希少な設計資源です。 共有範囲と変化頻度で割り当てます。
- Authorization は実行時に属します。 Runtime の拒否を表現するために安定した Tools 配列を変えません。
- Dispatch 済み Artifact から Fork します。 追記だけにし、再構築と構築後の調整を避けます。
- 意図的な書き換えにはすべて帰属が必要です。 原因不明の Miss は、修正に必要な観測性が足りません。
次章の第 40 章では、同じ「本物の状態を永続化する」規律を、Process Crash を越えて生き残る Turn に適用します。