体系 › 推論・高速化
·無料·14分で読めます

プロンプトキャッシュとコンテキスト設計 — 前方一致という一本の制約が、請求額を桁で変える

同じシステムプロンプトを毎回読み直させて、毎回その分を払っていませんか。プロンプトキャッシュは前方一致でしか効かない——この一点だけで、コンテキストに何をどの順で置くべきかが決まります。先頭に現在時刻を1つ入れただけで全滅する理由と、TTLを読み違えて逆に25%高くなる事故まで。

要点

  • APIはステートレスで、毎回送り直す全トークンをモデルは先頭から読み直し、その計算に入力トークン料金を払っている。
  • 因果的注意では、t番目のトークンのKとVは先頭からt番目までの入力だけで決まるため、キャッシュは前方一致でしか効かない。
  • 料金は通常の入力1×、キャッシュ書き込み1.25×(既定TTL)/2×(長寿命TTL)、キャッシュ読み出し0.1×の3段で、削減の上限は10倍である。
  • 変わりにくいものを先頭、変わりやすいものを末尾に置く。先頭に現在時刻を入れると以降すべてが不一致になる。
  • アクセス間隔がTTLより長い経路でキャッシュを付けてはいけない。30分間隔で既定TTL(5分)だとキャッシュなしより25%高くなる。
対象textタスクinference

毎朝、同じマニュアルを音読させている

新しい部下に仕事を頼むたび、300ページのマニュアルを最初のページから声に出して読み上げさせ、読み終わってからようやく用件を伝える。そんな職場があったら、誰でもおかしいと思うはずです。

LLM APIへのリクエストは、既定ではそれをやっています。APIはステートレスなので、前回の会話も、システムプロンプトも、ツール定義も、参照させたい社内ドキュメントも、毎回丸ごと送り直している。受け取ったモデルは、その全部を先頭から読み直します。ここでいう「読む」とは全トークンをAttentionに通して内部表現を作ることで、その計算に対して入力トークン料金を払っています。

プロンプトキャッシュは、この読み直しを省く仕組みです。「前回ここまで読んだ結果を取ってあるので、続きからどうぞ」とモデルに言えるようにする。ただし、この仕組みにはきわめて硬い制約が1つだけあります。それを理解しているかどうかで、同じアプリの請求額が桁で変わります。

なぜ「前方一致」でしか効かないのか

制約の正体は、Transformerの構造そのものにあります。

言語モデルの注意機構は因果的(causal)です。位置 tt のトークンは、自分より後ろを見ることができません。つまり、そのトークンの内部表現は、先頭から自分までの入力だけで決まります。

Kt=Wk ht,Vt=Wv ht,ht=f(x1,x2,…,xt)K_t = W_k\,h_t,\qquad V_t = W_v\,h_t,\qquad h_t = f(x_1, x_2, \dots, x_t)
(1)

式(1)が言っているのは一言だけです。tt 番目のトークンのKとVは、1番目から tt 番目までの入力だけから作られる。t+1t+1 番目以降に何が来ようと、値は1ビットも変わらない。

ここから結論が自動的に出ます。2つのリクエストが先頭から kk トークン目まで完全に同一なら、その kk 個分のKとVは必ず同じ値になります。だから計算し直す必要がない。取っておいたものをそのまま使えます。

逆も自動的に決まります。先頭から数えて1トークンでも違いがあれば、その位置以降のすべての hth_t が変わるので、再利用できるのはその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回使うだけで、1.25+0.1=1.351.25 + 0.1 = 1.35 となり、キャッシュなしの 2.02.0 より安い。寿命の内に2回使われるなら、もう元は取れているわけです。

会話が伸びると、費用は2乗で伸びる

キャッシュが本当に効いてくるのは、会話が積み上がる場面です。ここで何が起きているかを数えてみます。

システムプロンプトが2,000トークン、1ターンごとに(ユーザー発話+モデルの返答で)履歴が1,000トークンずつ伸びる、20ターンの会話を考えます。nn ターン目の入力量は 2,000+1,000(n−1)2{,}000 + 1{,}000(n-1) トークンです。20ターン分を足し上げると、

∑n=120(2,000+1,000(n−1))=40,000+1,000×190=230,000\sum_{n=1}^{20}\bigl(2{,}000 + 1{,}000(n-1)\bigr) = 40{,}000 + 1{,}000 \times 190 = 230{,}000

つまり「毎ターンの入力量を20ターン分ぜんぶ足すと23万トークンになる」ということです。会話が終わった時点で画面に載っている文章は21,000トークンぶんしかないのに、その11倍を買っていることになります。ターン数を NN とすると累計入力量は N2N^2 に比例するので、会話が長引くほどこの差は開きます。

FIG 1会話ターン数 n に対して、キャッシュなしの累計入力トークンは O(n²) で伸びる。n のスライダーを動かすと、O(n) との差がどこから「桁」になるかが目で見えます

同じ会話でキャッシュを効かせるとどうなるか。各ターンで新しく増えた分だけが書き込み(1.25×)で、それより前は読み出し(0.1×)です。書き込みの合計は会話全体の21,000トークンぶんが一度ずつなので 21,000×1.25=26,25021{,}000 \times 1.25 = 26{,}250、読み出しは残り209,000トークンで 209,000×0.1=20,900209{,}000 \times 0.1 = 20{,}900。合わせて約47,000、キャッシュなしの230,000に対しておよそ5分の1です。

置き方の原則: 静的なものを上、動くものを下

ここまでの原理から、コンテキスト設計のルールは1行に圧縮できます。変わりにくいものほど先頭に、変わりやすいものほど末尾に置く。

Anthropic APIの場合、リクエストは tools → system → messages の順に並びます。この順序自体は変えられないので、それぞれの中で並びを設計します。上から順に、こうなります。

  1. ツール定義(デプロイまで固定)
  2. システムプロンプト、ポリシー、口調やブランドの指示
  3. few-shot の例示
  4. 変わらない参照ドキュメント(製品マニュアル、規約、コードベースの一部)
  5. 会話履歴(追記のみ)
  6. 今回のユーザー発話、現在時刻、可変メタデータ

逆に、次の置き方はキャッシュを確実に全滅させます。どれも実際によくやります。

時刻がどうしても必要なら、末尾のユーザーメッセージ側に置きます。日付だけに丸める手もありますが、日付が変わった瞬間に全滅するのは同じなので、素直に下に置くのが正解です。

ブレークポイントは「範囲」ではなく「境界」

明示指定型のAPI(Anthropicの cache_control など)では、キャッシュしたい位置にマーカーを置きます。ここでよくある誤解が、「マーカーを付けたブロックだけがキャッシュされる」というものです。実際は逆で、マーカーはそこまでの全部を指す境界です。マーカーの数には上限があり、Anthropic APIでは4つまでです。

定石は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の選び方だけで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 のメトリクス。ダッシュボードには最低限、キャッシュヒット率とリクエストあたりの入力トークン内訳(通常/書き込み/読み出し)を出しておきます。

知らないと事故になる落とし穴。

  1. 先頭に時刻・ID・ランダム要素。 全滅するのに、アプリは正常に動くので気づきません。請求書で気づきます
  2. TTLより長い間隔で叩く経路にキャッシュを付ける。 前節の通り、25%高くなります
  3. ユーザーごとにprefixが違う設計。 共通部分を上、個別部分を下へ寄せるだけでヒット率が跳ねます
  4. 毎ターンの履歴要約。 要約するなら「区切りのいいところで1回だけ」やって、その後は追記のみに戻す
  5. ヒット率100%前提のコスト試算。 見積もりは必ず実測ヒット率で出し直す。推論コスト削減の実務で扱った他の手段と、順番を比べて決めます
  6. キャッシュで出力が変わると思い込む。 変わりません。キャッシュは計算の再利用であって、モデルの挙動は同一です。ここを混同すると、品質劣化の犯人探しで時間を溶かします

レビューで問われる筋。 「プロンプトの真ん中のドキュメントを1本差し替えたら、何トークンぶんが再計算になるか」。答えは差し替えた位置から末尾まで全部です。この一問に即答できれば、可変なものを下へ寄せる理由まで説明できます。

まとめ

コンテキスト設計は、モデルを賢くする話ではなく同じ答えをいくらで買うかの話です。長文脈を前提にしたアプリほど効きが大きくなるので、文脈長そのものを伸ばす技術と合わせて長文脈LLMの技術も読んでおくと、どこに投資すべきかの見取り図が揃います。

運営: TechAthletes · 編集方針 · 出典の付け方 · 訂正の記録

コメント

コメントにはログインが必要です