Digital Reactor
AIエージェント機械学習

エージェントに何を覚えさせるか:長いコンテキスト・RAG・Prompt Cachingをコストで選ぶ

エージェントに何を覚えさせるか:長いコンテキスト・RAG・Prompt Cachingをコストで選ぶ

はじめに

社内規程を読ませて問い合わせに答えるエージェントを作りたい、という相談を受けたとします。規程は全体で30万トークンあり、いまのモデルならそのままプロンプトに載る量です。載せてしまうか、検索して必要なところだけ引くか。この判断は、たいてい「どちらのほうが正確に答えるか」で議論が始まります。

ただ、月に何万回も呼ぶ機能なら、先に効いてくるのは金額と待ち時間です。しかも三つのやり方は、同じ情報を扱っていても代金の出どころが違うので、呼び出し回数が増えたときの伸び方が別々になります。毎回全文を渡す方法は回数に比例して増え、キャッシュは初回に割増を払って以降が安くなり、検索を挟む方法は引いた分しか払わない代わりに検索そのものの費用が回数ぶん乗ります。

単価を文字に置いて並べると、この三つの境目は簡単な式で書けます。先に結論を書くと、同じ前置きを2回以上使い回せる場面なら、キャッシュがいちばん安くなります。検索を挟むほうが安くなるのは、1回の質問で引いてくる量が資料全体のごく一部にとどまるときだけです。毎回全文を渡す方法が金額で勝つのは、1回きりの呼び出しで、しかも全部が必要なときに限られます。

対象読者:

  • 社内文書やコードベースを参照するエージェントを作っていて、コンテキストに何を載せるか決めかねている方
  • LLMを使った機能の月額を見積もりたい、または想定より高くて出どころを切り分けたい方
  • 検索基盤を作りに行く前に、そもそも必要かどうかを判断したい方

記事のポイント:

  • 三つのやり方の1回あたりコストを一つの式にまとめ、そこから境目を単価だけの関数として出します
  • キャッシュが得になる使い回し回数は書き込み割増と読み出し割引だけで決まり、資料の長さにも呼び出し回数の絶対値にも依存しません
  • 引く割合はコストの変数であると同時に精度の変数でもあり、増やせば当たるようになるわけではありません

何にトークン代がかかるかが、三つで違う

毎回全文を渡すやり方は、呼び出しのたびに資料の全トークンを入力として払います。実装は最も単純で、検索の取りこぼしが原理的に起きません。代金は呼び出し回数にまっすぐ比例します。

Prompt Caching は、前置きを一度サーバー側に置いておき、次回以降はその部分を割り引いた単価で読み出します。初回の書き込みには割増がかかります。照合は前方一致(prefix match)で、前置きの先頭から1バイトでも変われば、そこから後ろはすべて無効になります。キャッシュには保持時間があり、間隔が空けば書き込みからやり直しになります。

検索を挟むやり方(RAG)は、質問のたびに関連しそうな断片だけを引いて載せます。払うものは三つに分かれます。索引を作る一度きりの費用、検索1回ごとの費用(質問文の埋め込みとベクトル検索)、そして引いた分の入力トークンです。

三つを横並びの選択肢として扱うと、式が立ちません。検索を挟むかどうかは「何を載せるか」の話で、キャッシュするかどうかは「載せたものを使い回すか」の話だからです。検索を使う構成でも、システムプロンプト・ツール定義・共通の手順書といった固定部分はキャッシュできます。三つを並べるより、軸を二本立てたほうが見通しがよくなります。

一つの式にまとめる

コンテキストを、呼び出しをまたいで変わらない固定部分と、毎回変わる可変部分に分けます。参照しうる資料の全体を MM トークンとし、固定部分を M0M_0、可変部分は全体のうち ρ\rho の割合を引いてくるとして ρM\rho M トークンとします。単価は次のように置きます。

  • aa:入力トークン1つあたりの通常単価
  • ww:キャッシュ書き込みの割増率(w>1w > 1
  • rr:キャッシュ読み出しの割引率(0<r<10 < r < 1
  • kk:1回の書き込みを平均して何回読み出せるか
  • ss:検索1回の費用。以下、金額はすべて「全文を素で1回渡す代金 aMaM」を1とする単位で測ります
  • EE:索引を作る一度きりの費用(同じ単位)

1回あたりの入力コストは、固定部分と可変部分と検索の足し算になります。

CaM  =  M0Mw+(k1)rk  +  ρ  +  s\frac{C}{aM} \;=\; \frac{M_0}{M}\cdot\frac{w+(k-1)r}{k} \;+\; \rho \;+\; s

第1項の分子は、kk 回のうち1回が書き込みで残り k1k-1 回が読み出しであることを表しています。三つのやり方は、この式の特別な場合です。キャッシュを使わない状態は w=r=1w = r = 1(割増も割引もない)に対応し、第1項は kk によらず M0/MM_0/M になります。

やり方M0/MM_0/Mρ\rho1回あたりコスト
毎回全文を渡す1011
全文をキャッシュする10w+(k1)rk\dfrac{w+(k-1)r}{k}
検索して引いた分だけ載せる0ρ\rhoρ+s\rho + s
検索しつつ固定部分をキャッシュするm0m_0ρ\rhom0w+(k1)rk+ρ+sm_0\dfrac{w+(k-1)r}{k} + \rho + s

索引の費用 EE は回数に乗らないので、総額で見たときの切片になります。以下ではまず1回あたりで比べ、切片は最後に効かせます。

何回使い回せばキャッシュが安くなるか

全文をキャッシュしたときの1回あたりコストが、素で渡すコスト1を下回る条件を解きます。

w+(k1)rk<1k>wr1r\frac{w+(k-1)r}{k} < 1 \quad\Longleftrightarrow\quad k > \frac{w-r}{1-r}

右辺には MM も呼び出し回数の絶対値も出てきません。境目は書き込み割増と読み出し割引の二つだけで決まります。書き込みが1.25倍、読み出しが1割という比率なら k>1.28k > 1.28 となり、整数では2回目の読み出しから得になります。保持時間を延ばす代わりに書き込みが2倍になる設定なら k>2.11k > 2.11 で、3回目からです。

比率は提供元と設定で変わるので、自分の環境の値を入れて計算してください。以下の図は w=1.25w = 1.25r=0.1r = 0.1s=0.02s = 0.02E=0.05E = 0.05 で描いています。

左:呼び出し回数に対する総額。毎回全文は直線で伸び、キャッシュは初回に割増を払ってから寝た傾きになる。引く割合0.30の検索は5回目でキャッシュに追い抜かれる。右:実効使い回し回数に対する1回あたりコスト。使い回しが1回だと素で送るより高くつき、回数を重ねると読み出し割引に漸近する。

左のグラフでは、傾きの差がそのまま総額の差になります。毎回全文を渡す線は傾き1で、キャッシュの線は2回目以降が傾き r=0.1r = 0.1 です。回数を重ねるほど差は開き続けます。引く割合が0.30の検索は傾き0.32なので、初回こそ安いものの、5回目でキャッシュに追い抜かれます。

1回あたりのコストを表にすると、使い回しの回数がどれだけ効くかがはっきりします。

実効使い回し回数 kkw=1.25w = 1.25w=2.00w = 2.00
11.2502.000
20.6751.050
30.4830.733
50.3300.480
100.2150.290
200.1580.195

使い回しの回数は、間隔と更新で決まる

kk は呼び出した回数ではなく、1回の書き込みを何回読めたかです。この値は二つの理由で目減りします。

一つは保持時間です。キャッシュには期限があり、呼び出しの間隔が期限を超えると、次の呼び出しはまた書き込みから始まります。毎時1回のバッチで期限が数分なら、kk は常に1のままです。もう一つは前置きの更新です。前方一致で照合するので、載せている資料が変われば以降が無効になります。日次で更新する資料なら、1日ぶんの呼び出し回数が kk の上限になります。

図の右パネルが示すとおり、k=1k = 1 ではキャッシュの1回あたりコストが1.25となり、素で送るときの1を上回ります。割増だけ払って割引を一度も受け取っていない状態です。キャッシュを付けたのに請求が下がらない、むしろ上がったという場合、まずこの状態を疑ってください。判定は簡単で、レスポンスの cache_read_input_tokens が呼び出しを重ねても0のままなら効いていません。

キャッシュが効かなくなる書き方

前方一致という性質から、危ない書き方はだいたい決まっています。前置きの先頭に毎回変わる文字列が入っている形です。

# 危ない書き方:前置きの先頭に毎回変わる文字列が入っている
system = [{
    "type": "text",
    "text": f"現在時刻は {datetime.now()} です。\n\n{MANUAL}",
    "cache_control": {"type": "ephemeral"},
}]

安全なのは、バイト単位で不変な部分だけを前に置き、変わるものを後ろへ回す形です。

# 安全な書き方:不変な部分を前に置き、変わるものは区切りより後ろへ
system = [{
    "type": "text",
    "text": MANUAL,                        # 毎回まったく同じ
    "cache_control": {"type": "ephemeral"},
}]
messages = [{
    "role": "user",
    "content": f"現在時刻は {datetime.now()} です。\n\n{question}",
}]

resp = client.messages.create(
    model="claude-opus-5",
    max_tokens=4096,
    system=system,
    messages=messages,
)
print(resp.usage.cache_creation_input_tokens, resp.usage.cache_read_input_tokens)

同じ理由で無効化するものが他にもあります。ツール定義は多くの実装で前置きより前に置かれるので、利用者ごとにツールの集合を組み替えると、そこから後ろが全部無効になります。辞書をそのまま json.dumps してキー順が実行ごとに変われば、見た目が同じでもバイト列は別物です。モデルを途中で切り替えた場合も、キャッシュはモデルごとに持たれるので使えません。長い会話の途中でシステムプロンプトを書き換える設計も同じで、履歴の手前が変わるため、それまで積み上げたぶんが一度に無駄になります。

もう一つ、前置きが短いとそもそもキャッシュの対象になりません。最小の長さはモデルによって数百から数千トークンの幅があり、下回ると印を付けても静かに無視されます。数千トークン程度の指示文をキャッシュしたつもりで効いていない、というのはよくある行き違いです。

引く割合が読み出しの割引を下回ると検索が勝つ

検索を挟むほうが安くなる条件は、1回あたりコストの比較です。

ρ+s  <  w+(k1)rk\rho + s \;<\; \frac{w+(k-1)r}{k}

kk を大きくしていくと右辺は rr に近づくので、十分に使い回せる前提では条件が ρ<rs\rho < r - s に単純化します。読み出しが通常単価の1割で検索費が0.02なら、引いてよいのは全体の8%までです。冒頭の30万トークンの規程なら、1回に載せる量が2.4万トークン、ページ数にして十数ページを超えたあたりで、全文をキャッシュしたほうが安くなります。

実効使い回し回数と引く割合の平面で、どのやり方が最も安いかを塗り分けたもの。検索が最安の領域は下側の帯で、使い回しが増えるほど帯は薄くなり、下限は割引率から検索費を引いた0.08に漸近する。毎回全文を渡すのが最安なのは左上のごく細い帯だけ。

この境目は使い回しの回数によって動きます。k=2k = 2 までしか使い回せないなら引いてよい割合は0.66までありますが、k=10k = 10 なら0.20、k=20k = 20 なら0.14まで下がります。呼び出しが少なく間隔も空く用途では検索が有利で、同じ前置きを繰り返し叩く用途ではキャッシュが有利、という向きになります。

塗り分けた図では、毎回全文を渡すやり方が最安になる領域が左上のごく細い帯しかありません。1回きりで、かつ全部が必要という条件がそろったときだけです。それ以外の場面でこのやり方を選ぶ理由は、金額の外にあります。検索基盤を持たなくてよいことと、取りこぼしが起きないことです。

索引の費用 EE は総額の切片なので、呼び出しが十分多ければ判断を変えません。効いてくるのは資料が頻繁に入れ替わる場合で、更新のたびに差分ではなく全件ぶんの埋め込みが走る構成だと、切片が回数ぶん積み上がります。バッチの書き方ひとつでこれが起きることは、BigQueryで差分更新のつもりが毎回全件ぶん呼ばれていた話で書いたとおりです。

詰め込むほど当たるわけではない

ここまで ρ\rho をコストの変数として扱ってきましたが、精度の変数でもあります。そして向きが単純ではありません。

引く量を増やせば取りこぼしは減ります。同時に、関連しない断片が混ざる確率も上がります。旧版と新版の規程が両方コンテキストに入っていれば、どちらを根拠にするかはモデルが自分で決めることになります。この種のまちがいは切り分けが厄介で、検索のログを見ると正解の文書はちゃんと上位に来ているので、検索側に原因があるとは気づきにくくなります。上位の件数を5件から20件に増やした結果、以前は正しく答えていた質問で答えが変わる、という形で表面化します。

全部載せれば取りこぼしはありません。ただし、取りこぼしがないことと正しく答えることは別です。全部載せるというのは ρ=1\rho = 1 を選ぶことなので、紛れ込みの量としては最大になります。1段のエージェントが一度に抱える情報を減らすと精度が上がることがあるのは、タスクを何段に分けるかの判断でも同じ形で出てきます。

受託の現場では、取りこぼしを許容できるかどうかで先に切っています。規程の照会のように「該当する記述が見つかりませんでした」と返して人に回せる業務なら、引く量は絞ってよいと判断します。抜けが許されず、かつ資料の総量が数万トークン程度に収まるなら、全部載せてキャッシュするほうが設計も運用も軽くなります。

三つのうちキャッシュだけは精度に影響しません。モデルに渡るトークン列は同じで、変わるのは課金と前処理の扱いだけです。精度を動かさずに金額だけ下げられる手段は他にないので、検索を入れるかどうかを議論する前に、固定部分のキャッシュを済ませておくのが順番としては楽になります。

待ち時間はどこに乗るか

金額の話をしてきましたが、対話で使う機能なら待ち時間のほうが先に問題になります。三つは、時間の乗る場所も違います。

毎回全文を渡すと、入力の前処理(prefill)が長さに比例して伸びます。最初のトークンが返るまでの時間が MM で決まってしまうので、資料を厚くするほど反応が鈍くなります。キャッシュの読み出しはこの前処理を省けるため、待ち時間を縮める効果があります。呼び出しが少なくて金額の差が小さい用途でも、対話ならここが採用の理由になります。検索を挟む場合は、推論を始める前に検索の往復が入ります。ベクトル検索そのものは速いのですが、質問文の埋め込みで1往復、検索で1往復が加わり、結果を見て検索し直す設計にすると往復が積み上がります。

バッチ処理なら待ち時間は判断に入らないので、金額だけで決めて構いません。対話なら、引く割合を下げるとコストと待ち時間の両方が下がるので、検索側に寄せる理由が一つ増えます。

まとめ

三つのやり方の1回あたりコストは、固定部分をどれだけ使い回せるか kk と、毎回引く割合 ρ\rho の二つで書けます。キャッシュが得になる境目は (wr)/(1r)(w-r)/(1-r) 回で、書き込み割増と読み出し割引だけから決まります。検索が得になる境目は ρ<rs\rho < r - s で、十分に使い回せる前提なら引いてよいのは全体の1割弱です。毎回全文を渡すやり方が金額で勝つ場面はほとんど残らず、それでも選ぶなら理由は実装の軽さと取りこぼしのなさになります。

自分の環境で確かめるなら、まず1回の呼び出しで固定部分が何トークンあるかと、その前置きが平均して何回読み出されているかを測ってください。cache_read_input_tokenscache_creation_input_tokens の比がそのまま k1k-1 の実測値になります。この値が1を大きく下回っていれば、キャッシュする範囲を広げる前に、前置きの先頭に入っている可変の文字列と、呼び出しの間隔を確かめてください。

関連記事

← 技術ブログ一覧へ