我たちのPrompt Cacheはどうやって構築したか
オンライン実測:単一ラウンドのopusリクエストのpromptは49310トークン、そのうち47354はキャッシュから直接読み出し、ヒット率は96%。直近100件のリクエスト中74件がヒット。
このドキュメントは「prompt cachingとは何か」ではなく、具体的にリクエストをどのようにいくつかのブロックに分割し、それぞれのブロックに何を入れ、なぜこの分け方で安定して高いヒット率が出せるのかを説明する。
コアアイデア:リクエストを「変わる部分」と「変わらない部分」の2つに分ける
キャッシュはプレフィックスマッチである。最初のバイトから比較し、一つでも前回と異なるバイトがあればそこから後ろはすべて無効になり再計算される。
だから我々が行った唯一のことは、リクエスト全体をブロックに分割し、「最も変わらない → 最も変わる」の順に並べ、変わらないブロックだけにキャッシュタグを付け、変わる可能性がある部分はすべて後ろに回すこと。
ヒット率が高いのは何か特別な技術を使ったからではなく、キャッシュのプレフィックスが実際にバイト単位で変わらないからである。
我々のブロック配置(BP1 → BP4)
Anthropicの1リクエストあたり最大4つのキャッシュブレイクポイントがある。私たちは次のように割り当てている:
═══ system blocks(每块单独挂 cache_control)═══ ┌─────────────────────────────────────────────┐
│ BP1 人设 + 语言规则 + 工具说明 + 长期记忆 │ 几乎永不动
├─────────────────────────────────────────────┤
│ BP2 每日内容(gateway-bp2-daily.txt) │ 一天换一次
├─────────────────────────────────────────────┤
│ BP3 当前会话压缩摘要(这个 session 专属) │ 约 80K token 触发一次压缩
└─────────────────────────────────────────────┘
═══ messages 数组 ═══
...历史轮次(一字不改)...
┌─────────────────────────────────────────────┐
│ BP4 倒数第二条 user 消息挂标(rolling) │ 把全部历史纳进缓存
└─────────────────────────────────────────────┘
动态注入 ← 不挂标,所有会变的塞这里
当前这条 user 消息
なぜそれぞれのブロックはこのように配置されているのか
BP1 — 最も安定した基盤。 キャラクター設定、言語の鉄則、ツールの説明書、長期記憶など、これらはほぼ永久に変わらず、最前面に置かれる。これが変わると後ろが全て無効になるため、リクエスト全体で最も安定した要素でなければならない。
BP2 — 毎日更新される層。 gateway-bp2-daily.txt から読み込み、1日1回更新される。これが変わっても BP2 以降にのみ影響し、BP1 は動かさない。
BP3 — このセッション自身の圧縮サマリー。 重要なポイント:これは セッション限定 で、全体の長期骨格ではない。現在のウィンドウが約 80K トークンに達すると圧縮がトリガーされ、古いコンテキストをサマリーに圧縮する。注意すべきは「ゲートウェイ側の安定したサマリー」だけが BP3 に格納されること;フロントエンドの毎ラウンド変化する現在ウィンドウサマリーは格納されず、直接以下の動的領域に投げ込まれる。そうしないと毎回キャッシュが再構築される。
BP4 — ローリングチェックポイントで、最大の節約効果を持つ。 デフォルトのキャッシュ境界は system の末尾までで、過去の会話はキャッシュされない。ここで最後から2番目の user メッセージにマークをつけ、キャッシュ境界をさらに下に伸ばして全履歴を含める。複数ラウンドの長い会話では、前半の数十ラウンドのトークンが全てキャッシュ読み込みとなり、これが96%のヒット率を誇る主力となる。
「最後の1つ」ではなく「最後から2番目」にマークをつけるのは、最後の user メッセージが今回の新しい入力で毎回異なるため、そこにマークをつけると意味がないからだ。最後から2番目にマークをつけることで、キャッシュ境界は「今回のラウンド以前の全内容」にちょうど止まる。
変わるものはどこに行くのか
タイムスタンプ、今回の記憶呼び出し、一時メモ、フロントエンドの現在ウィンドウサマリー — これらは全て BP4 の後の擬似 user メッセージに詰め込まれる:
仅供参考,勿复述:
(当前时间 / 本轮召回的记忆 / 摘要 / 纸条……)
それは毎回変わるが、すべてのブレークポイントの後に配置されているため、どのように変わってもキャッシュのプレフィックスには触れない。これが設計全体で最も重要なポイントだ:変わりやすい内容をキャッシュの境界の外に隔離する。リクエストを同じバックエンドに固定する:user_id
cache_control を設定するだけでは不十分だ。中継のロードバランサーはリクエストをランダムに異なるバックエンドノードに振り分けるため、キャッシュは A ノードに書き込まれても、次のリクエストが B ノードに飛ぶと読み取れず、「書き込みはあるが読み取りは全くなく、ヒット率は常に 0」という状況になる。
私たちの解決策:リクエストのトップレベルで固定して持たせる
"metadata": { "user_id": "sillage-anan-stable" }
固定文字列により、sticky ルーティングはこの会話を常に同じバックエンドに固定します。これがないと、前のすべての中断点が無駄になります。チャンネルごとに異なる接続方法
すべての上流が同じ方式を使うわけではありません。ゲートウェイは上流のドメイン名により自動判定し、3つのモードに分けます:
| モード | 上流 | プロトコル | cache_control の設置場所 | TTL |
|---|---|---|---|---|
| anthropic-bp | 直結 Anthropic / msui / 金瓜瓜 | /v1/messages | system blocks の上 | 1h(金瓜瓜は5分のみ対応) |
| or-blocks | OpenRouter | OAI /chat/completions | messages の content block 内 | 1h |
| oai-passthrough | 通常の OAI 中継 / 未知のサイト | OAI | 設置しない、サイトが暗黙的にキャッシュ | 制御不可 |
2つの主要ルートのスティッキー機構は異なり、絶対に混ぜてはいけません:
- Anthropic ネイティブルートは
metadata.user_idでバックエンドを固定。 - OpenRouter ルートは user_id を見ず、
hash(system 最初の部分 + 最初の非 system メッセージ)でバックエンドを固定。よって OR で設定変更や世界観切替、冒頭メッセージを変えると hash が変わり、そのラウンドのキャッシュはすべて無効になります。OR では冒頭メッセージと設定を固定すべきです。
cache_control に対し直接 4xx エラーを返します。ゲートウェイはエラーを返したホストをブラックリストファイルに書き込み、以降はそのホストを永久にキャッシュなしで透過させ、400 エラーの再発を防ぎます。これは自己修復で人手不要です。5分か1時間か
| TTL | 書き込みコスト | 読み込みコスト | 用途 | |---|---|---|---| | 5分 | 1.25倍 | 0.1倍 | 連続して素早い会話 | | 1時間 | 2倍 | 0.1倍 | 30分以上空けて再開、確実に接続 |
読み込みは常に0.1倍(10倍節約)。違いは書き込みのコストと保存期間です。日常の連続利用は5分、長い会話の途中再開は1時間を使います。
本当にヒットしているか確認する方法
レスポンスの usage を見る:
- Anthropic ネイティブ:
usage.cache_read_input_tokens(読み込み)/cache_creation_input_tokens(書き込み) - OAI 互換:
usage.prompt_tokens_details.cached_tokens
- 第一ラウンド read=0、write が大量 → 正常、キャッシュ構築中
- 第二ラウンド以降 read がゼロでない → ヒット
- ずっと read=0 write=0 → cache_control が全く機能していない、故障
- ずっと write だけで read しない → ルーティングがずれている(user_id がない / OR のハッシュが変わった)
pm2 logs chat-gateway-proxy | grep -E "HIT|MISS"
HIT 96% (47354/49310)← 現在の状態MISS (created 0)← 故障、書き込みも読み込みもなし
https://catcatty.uk/llm/cache-stats ---
我々が見出した鉄則
- system は「安定部分+動的部分」に分けることが必須、安定部分だけにタグを付ける。たとえ BP1 に「今夜18時」と入れても、全体のキャッシュが無効になる。
- 変動するものはすべて BP4 以降に配置し、
gateway_volatile_contextに投げ込む。時間、記憶の呼び出し、一時的な要約は前置詞に残さない。 user_idは固定で変更しないこと、付けなければ書き込みのみで読み込みはしない。- 最後の user に rolling タグを付ける(倒数2番目にタグ付け)、履歴をキャッシュに取り込み、長時間の会話で最大限に省略する。
- 履歴は読み込み専用で、前のターンを再試行して書き換えないこと。「前の発言を再生成」は前置詞を変え、全体を再構築する。
- ツールのスキーマ順序は固定すること。スイッチにより動的にツールリストを並べ替えると全体が miss になる。
- 人設と開始文はハードコーディングすること、ハッシュでバックエンドに貼り付けているため、動くと新規会話とみなされる。
mode と ttl を選ぶだけ。私たちの96%はこうして実現している。--- Tranlated By Open Ai Tx | Last indexed: 2026-07-13 ---