プロンプトキャッシュとコンテキスト設計 — 前方一致という一本の制約が、請求額を桁で変える
同じシステムプロンプトを毎回読み直させて、毎回その分を払っていませんか。プロンプトキャッシュは前方一致でしか効かない——この一点だけで、コンテキストに何をどの順で置くべきかが決まります。先頭に現在時刻を1つ入れただけで全滅する理由と、TTLを読み違えて逆に25%高くなる事故まで。
要点
- APIはステートレスで、毎回送り直す全トークンをモデルは先頭から読み直し、その計算に入力トークン料金を払っている。
- 因果的注意では、t番目のトークンのKとVは先頭からt番目までの入力だけで決まるため、キャッシュは前方一致でしか効かない。
- 料金は通常の入力1×、キャッシュ書き込み1.25×(既定TTL)/2×(長寿命TTL)、キャッシュ読み出し0.1×の3段で、削減の上限は10倍である。
- 変わりにくいものを先頭、変わりやすいものを末尾に置く。先頭に現在時刻を入れると以降すべてが不一致になる。
- アクセス間隔がTTLより長い経路でキャッシュを付けてはいけない。30分間隔で既定TTL(5分)だとキャッシュなしより25%高くなる。
毎朝、同じマニュアルを音読させている
新しい部下に仕事を頼むたび、300ページのマニュアルを最初のページから声に出して読み上げさせ、読み終わってからようやく用件を伝える。そんな職場があったら、誰でもおかしいと思うはずです。
LLM APIへのリクエストは、既定ではそれをやっています。APIはステートレスなので、前回の会話も、システムプロンプトも、ツール定義も、参照させたい社内ドキュメントも、毎回丸ごと送り直している。受け取ったモデルは、その全部を先頭から読み直します。ここでいう「読む」とは全トークンをAttentionに通して内部表現を作ることで、その計算に対して入力トークン料金を払っています。
プロンプトキャッシュは、この読み直しを省く仕組みです。「前回ここまで読んだ結果を取ってあるので、続きからどうぞ」とモデルに言えるようにする。ただし、この仕組みにはきわめて硬い制約が1つだけあります。それを理解しているかどうかで、同じアプリの請求額が桁で変わります。
なぜ「前方一致」でしか効かないのか
制約の正体は、Transformerの構造そのものにあります。
言語モデルの注意機構は因果的(causal)です。位置 のトークンは、自分より後ろを見ることができません。つまり、そのトークンの内部表現は、先頭から自分までの入力だけで決まります。
式(1)が言っているのは一言だけです。 番目のトークンのKとVは、1番目から 番目までの入力だけから作られる。 番目以降に何が来ようと、値は1ビットも変わらない。
ここから結論が自動的に出ます。2つのリクエストが先頭から トークン目まで完全に同一なら、その 個分のKとVは必ず同じ値になります。だから計算し直す必要がない。取っておいたものをそのまま使えます。
逆も自動的に決まります。先頭から数えて1トークンでも違いがあれば、その位置以降のすべての が変わるので、再利用できるのはその1つ手前まで。これが前方一致(prefix matching)です。中間一致も後方一致も、原理的に存在しません。プロンプトの真ん中のドキュメントを1本差し替えたら、その後ろは全部作り直しになります。
1回のリクエストの中でKVを使い回す話はKVキャッシュを1から理解するで扱いました。プロンプトキャッシュは、そのKVキャッシュをリクエストをまたいで生き延びさせたものだと思ってください。原理は同じで、寿命だけが違います。
料金表は3段になる
キャッシュを有効にすると、入力トークンの単価が3種類に分かれます。Anthropic APIの現行の倍率で言うと、こうです(正確な値は公式の料金表で確認してください)。
| 種別 | 単価 | いつ発生するか |
|---|---|---|
| 通常の入力 | 1× | キャッシュ対象外の部分 |
| キャッシュ書き込み | 1.25×(既定TTL)/ 2×(長寿命TTL) | 初回、またはミスしたとき |
| キャッシュ読み出し | 0.1× | ヒットしたとき |
「桁で変わる」の正体は最後の行、0.1×です。ヒットしている限り、その部分は10分の1で買えます。つまり削減の上限は10倍、それ以上にはなりません。
書き込みが1.25×と少し割高なのが効いてきそうに見えますが、損益分岐はすぐ来ます。同じprefixを2回使うだけで、 となり、キャッシュなしの より安い。寿命の内に2回使われるなら、もう元は取れているわけです。
会話が伸びると、費用は2乗で伸びる
キャッシュが本当に効いてくるのは、会話が積み上がる場面です。ここで何が起きているかを数えてみます。
システムプロンプトが2,000トークン、1ターンごとに(ユーザー発話+モデルの返答で)履歴が1,000トークンずつ伸びる、20ターンの会話を考えます。 ターン目の入力量は トークンです。20ターン分を足し上げると、
つまり「毎ターンの入力量を20ターン分ぜんぶ足すと23万トークンになる」ということです。会話が終わった時点で画面に載っている文章は21,000トークンぶんしかないのに、その11倍を買っていることになります。ターン数を とすると累計入力量は に比例するので、会話が長引くほどこの差は開きます。
同じ会話でキャッシュを効かせるとどうなるか。各ターンで新しく増えた分だけが書き込み(1.25×)で、それより前は読み出し(0.1×)です。書き込みの合計は会話全体の21,000トークンぶんが一度ずつなので 、読み出しは残り209,000トークンで 。合わせて約47,000、キャッシュなしの230,000に対しておよそ5分の1です。
置き方の原則: 静的なものを上、動くものを下
ここまでの原理から、コンテキスト設計のルールは1行に圧縮できます。変わりにくいものほど先頭に、変わりやすいものほど末尾に置く。
Anthropic APIの場合、リクエストは tools → system → messages の順に並びます。この順序自体は変えられないので、それぞれの中で並びを設計します。上から順に、こうなります。
- ツール定義(デプロイまで固定)
- システムプロンプト、ポリシー、口調やブランドの指示
- few-shot の例示
- 変わらない参照ドキュメント(製品マニュアル、規約、コードベースの一部)
- 会話履歴(追記のみ)
- 今回のユーザー発話、現在時刻、可変メタデータ
逆に、次の置き方はキャッシュを確実に全滅させます。どれも実際によくやります。
- システムプロンプトの冒頭に現在時刻を入れる。 毎リクエストで文字列が変わるので、以降すべてが不一致。1トークンの差で数万トークンが吹き飛びます
- ユーザー名やセッションIDを冒頭に入れる。 ユーザーの数だけキャッシュが分裂し、どれも温まりません
- RAGの検索結果をシステムプロンプトの上に差し込む。 質問ごとに中身が変わるので、その下のマニュアルもfew-shotも全部無駄になります
- 会話履歴を毎ターン要約して差し替える。 prefix が毎回書き換わるので、毎ターン全額です
- ツール定義を辞書から動的生成する。 JSONのキー順が揺れるだけで不一致になります
時刻がどうしても必要なら、末尾のユーザーメッセージ側に置きます。日付だけに丸める手もありますが、日付が変わった瞬間に全滅するのは同じなので、素直に下に置くのが正解です。
ブレークポイントは「範囲」ではなく「境界」
明示指定型のAPI(Anthropicの cache_control など)では、キャッシュしたい位置にマーカーを置きます。ここでよくある誤解が、「マーカーを付けたブロックだけがキャッシュされる」というものです。実際は逆で、マーカーはそこまでの全部を指す境界です。マーカーの数には上限があり、Anthropic APIでは4つまでです。
定石は2つ置くことです。
- 1つ目を静的部分の末尾(システム+固定ドキュメントの終わり)に。寿命が長く、全リクエストで共有できます
- 2つ目を会話履歴の末尾、直近1〜2ターン手前に。ターンが進むたびに前進します
こうすると「長寿命の土台」と「伸びていく履歴」を別々に使い回せます。マーカーが1つしかないと、履歴が伸びるたびに土台まで書き直すか、履歴を諦めるかの二択になります。
なお、マーカーを使わず自動で前方一致を取る実装もあります(一定長以上のプロンプトを勝手にキャッシュする方式)。その場合も設計の作法はまったく同じで、単に指定が不要なだけです。
最低長とブロック境界
短いプロンプトはそもそもキャッシュされません。モデルごとに最低トークン数が決まっていて(おおむね1,000〜2,000トークン台)、公式ドキュメントに記載があります。500トークンのシステムプロンプトが効かない、という相談はたいていこれです。
さらに、一致判定は多くの実装でトークン単位ではなくブロック単位です。vLLMなら既定16トークンのブロックごとにハッシュを取るので、末尾の端数はキャッシュに乗りません。数トークンの差で挙動が変わって見えるときは、この粒度を疑ってください。
効いているかは、推測せず usage を見る
キャッシュが効いているかどうかを体感で判断するのは無理です。レスポンスの使用量に必ず出るので、そこを見ます。
resp = client.messages.create(
model=MODEL,
system=[
{"type": "text", "text": POLICY}, # 固定
{"type": "text", "text": MANUAL,
"cache_control": {"type": "ephemeral"}}, # ← ここまでを1区間に
],
messages=history + [{"role": "user", "content": q}], # 可変は末尾
max_tokens=1024,
)
u = resp.usage
print(u.cache_creation_input_tokens, # 書き込んだ量(初回だけ大きい)
u.cache_read_input_tokens, # ヒットした量(2回目以降ここが伸びる)
u.input_tokens) # どちらにも乗らなかった量
2回目のリクエストで cache_read_input_tokens が0のままなら、prefix が1トークン以上変わっています。組み立て後の文字列を2回分ダンプして、先頭から差分を取る。当てずっぽうでプロンプトをいじるのではなく、どこで一致が切れたかを突き合わせで特定する。ここは必ず機械的に潰せます。
TTLを読み違えると、有効にしたせいで高くなる
キャッシュには寿命(TTL)があります。既定は数分程度(Anthropicなら5分で、ヒットするたびに延長される)、明示すればより長いTTLも選べますが、書き込み単価が上がります。判断基準はそのprefixは寿命の内に何回使われるかだけで、ここを読み違えると請求額はむしろ上がります。
固定prefixが20,000トークン、30分に1回叩かれる処理を、1日48回で計算してみます。
- キャッシュなし: 単位
- 既定TTL(5分): 間隔がTTLより長いので毎回ミス。全部が書き込み扱いで 単位 → 25%高い
- 長寿命TTL(1時間・2×): ヒットで延長されるので生き続ける。 単位 → 約7分の1
同じ設定でも、TTLの選び方だけで7分の1にも1.25倍にもなります。アクセス間隔がTTLより長い経路では、キャッシュを付けてはいけない、が実務のルールです。夜間バッチのように1日1回しか走らない処理は、その典型です。
自前でホスティングしている場合
自分でモデルを立てている場合も同じ仕組みが使えます。vLLMの automatic prefix caching、SGLangの RadixAttention は、どちらもリクエストをまたいでKVブロックを共有します。API課金がないぶん、効果は金額ではなく時間に出ます。プレフィル(入力を読む工程)を丸ごと飛ばせるので、TTFT(最初の1トークンが出るまでの時間)が縮む形です。
効く条件は完全に同じで、前方一致・ブロック境界に加えてGPUメモリの空きが要ります。空きブロックが足りなければ古いキャッシュから追い出されるので、同時実行数を上げすぎるとヒット率が落ちる。負荷試験ではスループットと一緒にヒット率も見てください。
現場ではこう使う
誰が、いつ触るか。 LLMアプリのバックエンド担当が、コンテキストの組み立て順を決めるときが勝負どころです。設計の最初にやる仕事で、後から直すとプロンプト管理・RAG・履歴保存の全レイヤに波及します。次に、推論基盤の運用担当が請求額の異常を追うとき。「先月から急に高い」の原因が、システムプロンプトへの1行追加でキャッシュが割れていた、というのは定番です。
触るパラメータとツール名。 API側は cache_control / ephemeral / TTL指定、そしてレスポンスの cache_creation_input_tokens と cache_read_input_tokens。自前ホスティングなら vLLM の --enable-prefix-caching、--block-size、gpu_memory_utilization、および prefix cache hit rate のメトリクス。ダッシュボードには最低限、キャッシュヒット率とリクエストあたりの入力トークン内訳(通常/書き込み/読み出し)を出しておきます。
知らないと事故になる落とし穴。
- 先頭に時刻・ID・ランダム要素。 全滅するのに、アプリは正常に動くので気づきません。請求書で気づきます
- TTLより長い間隔で叩く経路にキャッシュを付ける。 前節の通り、25%高くなります
- ユーザーごとにprefixが違う設計。 共通部分を上、個別部分を下へ寄せるだけでヒット率が跳ねます
- 毎ターンの履歴要約。 要約するなら「区切りのいいところで1回だけ」やって、その後は追記のみに戻す
- ヒット率100%前提のコスト試算。 見積もりは必ず実測ヒット率で出し直す。推論コスト削減の実務で扱った他の手段と、順番を比べて決めます
- キャッシュで出力が変わると思い込む。 変わりません。キャッシュは計算の再利用であって、モデルの挙動は同一です。ここを混同すると、品質劣化の犯人探しで時間を溶かします
レビューで問われる筋。 「プロンプトの真ん中のドキュメントを1本差し替えたら、何トークンぶんが再計算になるか」。答えは差し替えた位置から末尾まで全部です。この一問に即答できれば、可変なものを下へ寄せる理由まで説明できます。
まとめ
- キャッシュが前方一致でしか効かないのは、因果注意の下では が先頭から までの入力だけで決まるから。原理からの帰結であって、実装の都合ではない
- 読み出しは0.1×なので削減の上限は10倍。同じprefixが寿命内に2回使われれば、書き込みの1.25×はもう回収できている
- コンテキストは「変わりにくい順」に上から積む。先頭に置いた1つの可変値が、その下の全部を無効にする
- TTLとアクセス間隔が合っていないと、有効にしたぶんだけ高くなる。付けない判断も設計のうち
コンテキスト設計は、モデルを賢くする話ではなく同じ答えをいくらで買うかの話です。長文脈を前提にしたアプリほど効きが大きくなるので、文脈長そのものを伸ばす技術と合わせて長文脈LLMの技術も読んでおくと、どこに投資すべきかの見取り図が揃います。
コメント
コメントにはログインが必要です