
この記事をシェア
AIエージェントを本番で回し始めると、最初に困るのは回答の質より請求額、というケースが少なくありません。人間なら一度読めば済む資料を、エージェントはツールを呼ぶたびに丸ごと送り直します。失敗すればやり直し、考え込めば思考トークンが積み上がる。トークン課金は、こうした挙動をそのまま請求書に写します。以前のガードレール設計の記事では「暴走によるコスト爆発」を防御の観点で扱いました。この記事はその実装編で、コストを後から削るものではなく、最初に設計するものとして扱います。回すレバーはプロンプトキャッシュ、モデルルーティング、予算ガードレールの3つです。
数字はすべて、2026年9月時点のClaude API公式料金を使った試算です。私自身の実測値は、過去記事で公開したものだけを引用しています。自社の数字に置き換えて使えるよう、計算の前提は隠さずに書きます。
コストは「1タスク単価」で見る
エージェントの1回の呼び出しで送られるトークンは、おおむね次の4つに分かれます。
- ツール定義: 使えるツールの名前・説明・引数スキーマ。ツールが10個を超えると数千トークンになることも珍しくない
- システムプロンプト: 役割、手順、社内規約など
- 会話履歴: これまでの発話、ツールの呼び出しと結果
- 出力: 回答と、拡張思考を使う場合の思考トークン(出力単価で課金される)
厄介なのは履歴です。ターンごとに前のツール結果が積み上がり、しかも毎ターン全部を送り直すので、入力トークンの合計はターン数に比例するのではなく、ほぼ二乗で増えていきます。10ターンで終わるはずのタスクがリトライで20ターンになると、入力側のコストは2倍では済みません。
ここで、トークン数のまま管理するのをやめて1タスクあたりの金額に換算しておくことをおすすめします。「月に3億トークン使いました」と言われても、経営側はそれが高いのか安いのか判断できません。「問い合わせ1件の一次対応に18セント、月3,000件で約550ドル」なら、人手でやった場合と比べられます。
計算式は単純です。ポイントは分母を成功したタスク数にすることです。失敗したタスクの費用も、成功したタスクが背負っていると考えます。
成功1件あたり単価 = 期間中のLLM費用の合計 ÷ 成功したタスク数
成功率80%なら、1回の実行に14.6セントかかるタスクの成功1件あたり単価は約18セントになります。品質改善がそのままコスト改善になる、ということでもあります。成功率の測り方はエージェント評価の記事で書いたEvalsの仕組みがそのまま使えます。
プロンプトキャッシュを「並べ方」で効かせる
プロンプトキャッシュは、前回と先頭から完全に一致する部分(プレフィックス)の処理結果を再利用し、その部分を安い単価で課金する仕組みです。AnthropicのClaude APIでは cache_control、Amazon BedrockのConverse APIでは cachePoint で区切り位置を指定します。
単価の倍率は次のとおりです(Claude APIの公式料金ページ、2026年9月確認)。
| 操作 | 基本入力単価に対する倍率 |
|---|---|
| キャッシュ書き込み(TTL 5分) | 1.25倍 |
| キャッシュ書き込み(TTL 1時間) | 2倍 |
| キャッシュ読み出し | 0.1倍が標準。Opus 5.5は0.05倍、Fable 5.1は0.025倍 |
「キャッシュは1/10」と覚えている人も多いと思いますが、2026年秋のモデルではさらに安いものがあります。逆に、書き込みは通常より高い、という点は見落とされがちです。
変更頻度の低い順に並べる
キャッシュはプレフィックスの完全一致でしか効きません。そして処理順は tools → system → messages で固定されていて、前の層が変わると後ろの層もすべて無効になります。つまり、変わらないものほど前に置くのが唯一の原則です。
図:キャッシュに乗るプロンプトの並べ方
実務で崩れやすいのは次のような箇所です。
- システムプロンプトの先頭に「現在日時: 2026-09-28 10:15」を入れている。これだけで毎回全部が作り直しになる。日時は最後のユーザー発話側に移す
- ツール定義の順序が、辞書の走査順などで呼び出しごとに入れ替わる。中身が同じでも順序が変われば別物として扱われる
- 途中で思考の設定(有効/無効や予算)や
tool_choiceを切り替える。公式ドキュメント上、どちらもmessages層のキャッシュを無効にする - プレフィックスが最小トークン数に届いていない。Sonnet 5は1,024、Opus 5系は512、Haiku 4.5は4,096トークン未満だと、指定してもキャッシュされない(エラーにはならないので気づきにくい)
試算:キャッシュの前後で1タスク単価はどう変わるか
次の条件で、1タスクあたりの費用を計算しました。実測ではなく、公式単価からの試算です。
- モデル: Claude Sonnet 5(入力 $2 / 5分キャッシュ書き込み $2.50 / キャッシュ読み出し $0.20 / 出力 $10、いずれも100万トークンあたり)
- 1タスク10ターン。ツール定義+システムプロンプトで12,000トークン
- 1ターンごとに履歴が1,500トークン増え、出力は1ターン500トークン(思考を含む)
キャッシュなしだと10ターン分の入力は合計187,500トークンになります。キャッシュありでは、各ターンで前回までのプレフィックスを読み出し、新しく増えた1,500トークンだけを書き込む形になります。
| 構成 | 入力側 | 出力 | 1タスク合計 | キャッシュなし比 |
|---|---|---|---|---|
| キャッシュなし | $0.375 | $0.050 | $0.425 | 100% |
| キャッシュが正しく効く | $0.096 | $0.050 | $0.146 | 約34% |
| 毎回キャッシュが外れる(先頭に日時を入れた場合) | $0.469 | $0.050 | $0.519 | 約122% |
正しく効けば、入力側は約4分の1になります。一方で3行目を見てください。区切りを指定したままプレフィックスが毎回変わると、毎ターン書き込み単価(1.25倍)で全量を払うことになり、キャッシュを使わないより高くなります。「キャッシュを入れたのに請求が増えた」という相談は、たいていこのパターンです。
もう一つ読み取れるのは、キャッシュが効いたあとは出力が費用の3分の1を占めるようになることです。ここから先は入力をいくら削っても頭打ちで、次の打ち手は出力を減らすこと、つまり思考の量やモデルの選び方に移ります。
効いているかは usage で確かめる
キャッシュが効いたかどうかは、レスポンスの usage で必ず確認します。Claude APIなら cache_read_input_tokens と cache_creation_input_tokens、BedrockのConverse APIなら cacheReadInputTokens と cacheWriteInputTokens です。
ここで詰まりやすいのが input_tokens(Bedrockでは inputTokens)の意味です。キャッシュ利用時はキャッシュされなかった部分だけの数になります。この値だけを集計すると使用量を大幅に少なく見積もってしまうので、3つを足したものを総入力として扱ってください。キャッシュの細かい設定(TTLの使い分け、sticky routingなど)は、プロンプトキャッシュの節約ポイントの記事に詳しく書いています。
なお、Bedrock上のOpenAI GPT-5.6系も明示的なキャッシュ区切りに対応していて、書き込み1.25倍、読み出しは90%引き、TTLは既定で30分とされています(AWS公式ドキュメント、2026年9月確認)。モデルごとに条件が違うので、ルーティングで複数モデルを使う場合は、それぞれの条件を表にしておくと後で迷いません。
モデルルーティングは損益分岐で決める
全ステップを最上位モデルで回すのは、設計としては怠慢です。とはいえ「安いモデルに振れば安くなる」ほど単純でもありません。まず2026年9月時点の単価を並べます(100万トークンあたり、Claude API)。
| モデル | 入力 | キャッシュ読み出し | 出力 | 最小キャッシュ長 |
|---|---|---|---|---|
| Claude Opus 5.5 | $4 | $0.20 | $20 | 512 |
| Claude Sonnet 5 | $2 | $0.20 | $10 | 1,024 |
| Claude Haiku 4.5 | $1 | $0.10 | $5 | 4,096 |
Sonnet 5の $2 / $10 は、発売時に8月末までの導入価格と案内されていたものが、そのまま標準価格になりました。結果として、Haiku 4.5との価格差は2倍しかありません。数年前の「軽量モデルは10分の1以下」という感覚でルーティングを設計すると、見込みほど下がらないことになります。
安いモデルが逆に高くなる例
分類のような短い処理で、3,000トークンの固定プロンプトと300トークンの可変入力を送り、50トークンの答えを受け取るケースを考えます。定常状態(キャッシュが温まった状態)での1回あたりの試算です。
| モデル | 固定部分3,000 | 可変部分300 | 出力50 | 合計 |
|---|---|---|---|---|
| Sonnet 5(キャッシュ有効) | $0.00060 | $0.00060 | $0.00050 | $0.00170 |
| Haiku 4.5(4,096未満でキャッシュ不可) | $0.00300 | $0.00030 | $0.00025 | $0.00355 |
単価表では半額のHaikuが、この条件では約2倍の費用になります。理由は最小キャッシュ長で、Haiku 4.5は4,096トークン未満のプレフィックスをキャッシュしません。さらに、Claudeは4.7以降の世代で新しいトークナイザに切り替わっていて、公式には同じ文章で約30%多くトークン化されると書かれています。モデル間の比較は、トークン単価ではなく、同じタスクを流したときの1タスク単価で行うべき理由がここにあります。
エスカレーションの「二度払い」
よく使われるのが、まず安いモデルに処理させ、検証に落ちたら上位モデルでやり直す二段構えです。期待コストは次のように書けます。
二段構えの期待コスト = C安 + C検証 + p × C高
p: 上位モデルへ回す割合(エスカレーション率)
C高 だけで処理するより安くなる条件:
p < (C高 − C安 − C検証) ÷ C高
安いモデルの費用が上位の半分、検証が1割かかるなら、損益分岐はp = 40%です。エスカレーションが4割を超えるなら、最初から上位モデルに投げたほうが安い。しかも実際には、やり直しのときに上位モデル側のキャッシュが温まっていないため、C高は通常より高くつきます。二段構えを入れる前に、Evalsで「安いモデルがどれくらいの割合で合格するか」を測っておくのが先です。
私の環境で一番効いたのも、実はキャッシュではなく振り分けでした。Kimi K2.6の費用を1日$28から$8に下げた記事で公開した実測では、定期実行のcronジョブをローカルの軽量モデルへ移したことで1日$12減り、キャッシュ最適化は$3でした。「そもそもフロンティアモデルに投げる必要がない仕事」を見つけるほうが、単価を削るより大きく効くことがあります。ローカルLLMへの振り分けはOllamaの記事、振り分け判断そのものを安く行う方法はJevの記事が参考になると思います。
始めるなら、次の順が無難です。
- ステップの種類で固定的に振り分ける(抽出・分類・整形は軽量、計画・最終判断は上位)
- Evalsで軽量モデルの合格率を測り、損益分岐を超えていないか確認する
- それでも足りなければ、確信度や検証結果によるエスカレーションを足す
予算ガードレールを実装する
キャッシュとルーティングは平均単価を下げる手段です。一方で、事故になるのは平均ではなく外れ値、つまりループに入って止まらなくなった1セッションです。そこで上限を強制する仕組みを別に持ちます。
図:予算ガードレールの4層
AWS Budgetsだけでは止められない
最初に押さえておきたいのは、AWS Budgetsは暴走を止める道具としては遅すぎる、という点です。AWSの公式ドキュメントには、Budgetsの情報は1日に最大3回更新され、更新間隔は通常8〜12時間だと書かれています。さらに、利用から請求への反映にも遅れがあり、通知が届く前に閾値を超えることがあると明記されています。
予算アクションでIAMポリシーやSCPを自動適用することはできますが、エージェントが1時間で使い切る金額を翌朝止めても意味がありません。Budgetsは月次の突合と「止め損ねたときの最後の網」と割り切り、セッション単位の上限はアプリケーション側で持ちます。
DynamoDBで「仮押さえ→精算」する
セッションや日次の積算には、DynamoDBの条件付き更新が向いています。複数のLambdaが同時に書き込んでも、加算と上限チェックが1回の操作で行われるからです。
ただし、呼び出しが終わってから加算するだけだと、最後の1回で上限を大きく超えることがあります。そこで、呼び出し前に最悪ケースの金額を仮押さえし、終わったら実額との差を戻す形にします。DynamoDBの条件式では足し算ができないので、「上限 − 今回の仮押さえ額」をアプリ側で計算して渡します。
import boto3
from botocore.exceptions import ClientError
ddb = boto3.client("dynamodb")
TABLE = "agent_budget"
# 100万トークンあたりの価格(USD)。2026年9月時点のClaude API公開価格、TTL 5分のキャッシュ前提
PRICE = {
"claude-sonnet-5": {"in": 2.00, "cache_write": 2.50, "cache_read": 0.20, "out": 10.00},
"claude-haiku-4-5": {"in": 1.00, "cache_write": 1.25, "cache_read": 0.10, "out": 5.00},
}
class BudgetExceeded(Exception):
pass
def cost_micro_usd(model, usage):
"""usage から費用をマイクロドル(100万分の1ドル)の整数で返す。
トークン数 × (USD/100万トークン) がそのままマイクロドルになる。"""
p = PRICE[model]
return round(
usage.input_tokens * p["in"]
+ (usage.cache_creation_input_tokens or 0) * p["cache_write"]
+ (usage.cache_read_input_tokens or 0) * p["cache_read"]
+ usage.output_tokens * p["out"]
)
def worst_case_micro_usd(model, prompt_tokens, max_tokens):
"""全入力がキャッシュ書き込み、出力が max_tokens いっぱいになった場合の金額"""
p = PRICE[model]
return round(prompt_tokens * p["cache_write"] + max_tokens * p["out"])
def reserve(key, worst, limit):
"""呼び出し前に最悪ケースの金額を仮押さえする。上限を超えるなら例外"""
if worst > limit:
raise BudgetExceeded(key)
try:
ddb.update_item(
TableName=TABLE,
Key={"pk": {"S": key}},
UpdateExpression="ADD spent :w",
ConditionExpression="attribute_not_exists(spent) OR spent <= :cap",
ExpressionAttributeValues={
":w": {"N": str(worst)},
":cap": {"N": str(limit - worst)},
},
)
except ClientError as e:
if e.response["Error"]["Code"] == "ConditionalCheckFailedException":
raise BudgetExceeded(key)
raise
def settle(key, worst, actual):
"""呼び出し後に、仮押さえと実額の差分を戻す(差分は負の数になる)"""
ddb.update_item(
TableName=TABLE,
Key={"pk": {"S": key}},
UpdateExpression="ADD spent :d",
ExpressionAttributeValues={":d": {"N": str(actual - worst)}},
)
呼び出し側は、失敗しても仮押さえが残らないように try / finally で精算します。
key = f"session#{session_id}"
worst = worst_case_micro_usd(model, prompt_tokens, max_tokens)
reserve(key, worst, SESSION_LIMIT_MICRO_USD) # 例: 500_000 なら $0.50
actual = 0
try:
resp = client.messages.create(model=model, max_tokens=max_tokens, ...)
actual = cost_micro_usd(model, resp.usage)
finally:
settle(key, worst, actual)
prompt_tokens は、トークン数カウントAPIで数えるか、前回の usage の合計に今回の追加分の概算を足して求めます。多少多めに見積もっても、仮押さえなので精算時に戻ります。
日次やユーザー単位の上限も、同じテーブルに day#2026-09-28#user#123 のようなキーで積算すれば実現できます。セッションと日次を同時に仮押さえするなら、片方だけ成功する状態を避けるために TransactWriteItems でまとめてください。日次キーにはTTL属性を付けておくと、古い行が自動で消えます。
通信断などで settle 自体が失敗すると、仮押さえが残って上限に早く達します。安全側に倒れる失敗なので、私はそのままにしておき、日次の集計ジョブで請求額と突き合わせる運用が現実的だと考えています。
上限に近づいたときの3段階
いきなり止めると、利用者から見れば「途中で壊れた」のと同じです。消化率に応じて段階的に振る舞いを変えます。
| 消化率 | 挙動 | 注意点 |
|---|---|---|
| 70% | 運用担当へ通知(SNSやSlack) | ループ検知のログも一緒に送ると原因を追いやすい |
| 90% | 縮退運転。軽量モデルへ切り替え、思考量を下げ、履歴を要約して短くする | 切り替えた時点でキャッシュは作り直しになる |
| 100% | 停止。途中までの成果と経緯を残し、人間へ引き継ぐ | 「何ができて何が残ったか」を必ず返す |
90%の縮退は、見た目ほど安くならないことがあります。モデルの切り替えや思考設定の変更はキャッシュを無効にするので、残り数ターンで終わるセッションなら、そのまま上位モデルで走り切ったほうが安い場合もあります。縮退は「新しく始まるセッションから適用する」ほうが扱いやすいというのが、試算してみての私の見方です。
もう一つ、最も安上がりなガードレールは1回の呼び出しの層です。max_tokens の上限、1タスクの最大ターン数、同じツールを同じ引数で連続して呼んだら止めるループ検知。この3つは実装が数行で済み、暴走の大半はここで止まります。
機能を作る前に「コスト予算」を決める
ここまでの仕組みは、上限の値が決まっていて初めて意味を持ちます。私は機能開発の最初に、次の式で月額の上限を決めてから設計に入るようにしています。
月額上限 = 成功1件あたり単価 × 想定件数 × 安全係数
例: $0.18 × 3,000件 × 1.5 = $810 / 月
成功1件あたり単価は、プロトタイプで数十件流した実績から出します。安全係数は、ユーザーの使い方が読めない初期ほど大きめにとります。この数字があると、稟議でも「人手で1件いくらかかっていたか」と並べて説明できますし、セッション上限の値も「1件あたり単価の数倍」と根拠を持って決められます。
運用に入ったら、月次で次の点を見直します。
- 成功1件あたり単価の推移。上がっていたら、成功率の低下か、1件あたりのターン数の増加を疑う
- キャッシュ読み出しの比率。下がっていたら、誰かがプロンプトの先頭を変えていないか確認する
- モデルの価格改定と世代交代。今回のSonnet 5のように、予告されていた値上げが取り消されることもある
- 上限到達で止まったセッションの中身。ループなのか、本当に難しいタスクなのか
おわりに
エージェントのコストは、プロンプトの並べ方、どのステップをどのモデルに任せるか、どこで止めるか、という設計の結果として決まります。後から請求書を見て削るより、最初に1タスク単価と上限を決めておくほうが、結果的に機能開発も速く進みます。
この記事の試算は、公式単価と仮定したトークン量から出したものです。実際の単価はタスクの形で大きく変わるので、まずは自分の環境で10件ほど流し、usage を集計するところから始めてみてください。数字が出れば、どのレバーから回すべきかは自然に見えてきます。
※本記事の料金・仕様は2026年9月時点の公開情報に基づいています。料金表の数字はClaude API(ファーストパーティ)のもので、Amazon Bedrockの価格はリージョンやエンドポイント種別によって異なります。試算は仮定した条件による概算で、実測値ではありません。
