APIコストの急騰——誰もが直面する問題
先月、LLMプロバイダーからのAPI請求書を見て驚いた:ユーザー数がまだ少ないにもかかわらず、30日間で$340もかかっていた。数時間かけてログを分析した結果、2つの主な原因が判明した:60%以上のトークンが毎回のリクエストで繰り返されるシステムプロンプトによるもの、そしてバッチにまとめる代わりに個別に呼び出される大量の小さなリクエストだった。
Claude、GPT-4、Geminiなど、どのLLMもトークン単位で課金される——この問題はいずれ誰にでも降りかかる。アウトプットの品質を下げることなく、コストを約55%削減できた3つのテクニックを紹介する。
APIコストが急騰する3つの主な原因
LLM APIはトークン数(入力+出力)で課金される。実際には、次の3つがトークンを最も消費しやすい:
- 長いシステムプロンプトの繰り返し:毎回同じシステムプロンプトを送信しているにもかかわらず、その分のコストを毎回全額支払っている。
- 細かいリクエストの個別呼び出し:まとめる代わりに1件ずつAPIを呼び出しており、Batch APIの割引を無駄にしている。
- プロンプト内の「無駄な」トークン:余分な空白、繰り返しの指示、不要なコンテキスト。これらすべてが請求に上乗せされる。
テクニック1:プロンプトキャッシング——変化する部分だけに課金
仕組みはシンプルだ:プロバイダーがリクエスト間で変わらないプロンプトの部分を「記憶」する。初回送信時は通常料金を支払うが、次回以降はキャッシュ済み部分がオリジナル価格の10%(Anthropicの場合)しかかからない。わずかに聞こえるが、2,000トークンのシステムプロンプトを1日10,000回呼び出す場合、その節約額は非常に大きくなる。
Anthropic ClaudeはClaude 3.5以降でキャッシングをサポートしている。キャッシュしたい部分にcache_controlを追加するだけだ:
import anthropic
client = anthropic.Anthropic()
# 長いシステムプロンプト(例:2,000トークン)——キャッシュしたい部分
system_prompt = """
あなたは15年の経験を持つ金融分析の専門家です...
[長い指示内容のすべてをここに]
"""
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
system=[
{
"type": "text",
"text": system_prompt,
"cache_control": {"type": "ephemeral"} # キャッシュのマーク
}
],
messages=[
{"role": "user", "content": "このQ1財務報告書を分析してください..."}
]
)
# キャッシュヒットを確認
usage = response.usage
print(f"Cache read tokens: {usage.cache_read_input_tokens}")
print(f"Cache write tokens: {usage.cache_creation_input_tokens}")
print(f"Regular input tokens: {usage.input_tokens}")
次のリクエストで同じシステムプロンプトを使用すると、その部分は再計算される代わりにキャッシュから読み込まれる。キャッシュは最低5分間保持され、使用するたびに自動更新される。長いシステムプロンプトを共有する多くのユーザーを持つチャットボットやAIアシスタントでは、入力トークンを80〜90%節約できる可能性がある。
プロンプトキャッシングを使うべき場面
- システムプロンプトが1,024トークン以上(Anthropicのキャッシュ最小閾値)
- 複数のリクエストが長いプレフィックスを共有している場合——例:コンテキストとしてドキュメント全体を使用
- 固定ナレッジベースのRAG——ドキュメント部分をキャッシュし、毎回変わるのはユーザークエリのみ
テクニック2:バッチ処理——リクエストをまとめてコスト削減
1,000件に対してAPIを1,000回呼び出す代わりに、Batch APIはすべてを一度に送信して非同期で結果を受け取ることができる。Anthropic Message Batches APIは最大10,000リクエスト/バッチを受け付け、個別呼び出しの50%の価格で利用できる。唯一のトレードオフ:リアルタイムの結果が得られないことだ。
import anthropic
import json
client = anthropic.Anthropic()
# 処理が必要なリスト
items_to_process = [
{"id": "item_001", "text": "感情分類:'素晴らしい商品!'"},
{"id": "item_002", "text": "感情分類:'配送が遅すぎる。'"},
{"id": "item_003", "text": "感情分類:'普通で、特に際立ったところはない。'"},
# ... 数百〜数千件追加
]
# バッチリクエストを作成
requests = [
{
"custom_id": item["id"],
"params": {
"model": "claude-haiku-4-5-20251001", # シンプルなタスク向けの安価なモデル
"max_tokens": 50, # 短い出力に制限
"messages": [{"role": "user", "content": item["text"]}]
}
}
for item in items_to_process
]
# バッチを送信
batch = client.messages.batches.create(requests=requests)
print(f"Batch ID: {batch.id}")
print(f"Status: {batch.processing_status}")
# 後でクエリするためにbatch_idを保存(バッチ処理には数分〜数時間かかる場合がある)
with open("batch_ids.json", "a") as f:
json.dump({"batch_id": batch.id, "count": len(requests)}, f)
# バッチ完了後に結果を取得するための別スクリプト
client = anthropic.Anthropic()
batch_id = "msgbatch_xxx" # 作成時のID
batch = client.messages.batches.retrieve(batch_id)
if batch.processing_status == "ended":
results = {}
for result in client.messages.batches.results(batch_id):
if result.result.type == "succeeded":
results[result.custom_id] = result.result.message.content[0].text
print(f"処理完了:{len(results)}件")
Batch API使用時のヒント
- シンプルなタスク(分類、抽出)には小さなモデルを使用——
claude-sonnetの代わりにclaude-haiku max_tokensを実際の出力に合わせて制限——50トークンしか必要ないのに4096を設定しない- リアルタイムが不要な場合はアプリを非同期設計に:夜間処理、データパイプライン
テクニック3:不要なトークンの削減
最もシンプルなテクニックだが、最も見落とされがちなものでもある。あるコードベースをレビューした際、500トークン以上が空白、余分なコメント、繰り返しの指示だけで占められているプロンプトを発見した。500トークン×数百万リクエスト=決して小さくない額になる。
3.1 システムプロンプトの圧縮
# BAD — 冗長で余分な空白が多い(〜120トークン)
system_prompt_bad = """
あなたは賢くて役立つAIアシスタントです。
ユーザーの質問に答える手助けをします。
回答する際は、以下が必要です:
- 明確かつ完全に回答する
- わかりやすい言語を使用する
- 不確かなことは言わない
"""
# GOOD — 簡潔で、同じ意味、〜70%のトークン節約(〜35トークン)
system_prompt_good = "役立つITアシスタント。簡潔かつ正確に回答。不確かな場合は認める。"
3.2 会話履歴のトリミング
長い会話履歴を持つチャットボットはよくあるトークンの罠だ。ユーザーが2時間チャットし、各リクエストで全履歴を送信すると、セッション終盤には最初の10〜20倍のトークンを消費することになる。直近のNメッセージだけを保持すれば十分だ:
def trim_conversation_history(messages: list, max_messages: int = 10) -> list:
"""トークンを削減するために直近のNメッセージのみを保持する。"""
if len(messages) <= max_messages:
return messages
# 最初のメッセージは常に保持(通常は重要なコンテキスト)
return [messages[0]] + messages[-(max_messages - 1):]
def count_tokens_estimate(text: str) -> int:
"""トークンの推定:日英混合コンテンツでは〜3文字 = 1トークン。"""
return len(text) // 3
# 送信前にチェック
conversation = load_conversation_history(user_id)
total_tokens = sum(count_tokens_estimate(m["content"]) for m in conversation)
if total_tokens > 50_000: # 警告閾値
conversation = trim_conversation_history(conversation, max_messages=6)
3.3 タスクに合ったモデルを選ぶ
多くの人がすべてのタスクにデフォルトで最強のモデルを使っているのをよく見かける。しかし、テキストの感情分類にOpusは必要ない。簡単なリファレンス:
- シンプルな分類、感情分析、抽出:Claude Haiku(Opusより約20倍安価)
- コンテンツ作成、要約、一般的なQ&A:Claude Sonnet(品質とコストのスイートスポット)
- 複雑な推論、コードレビュー、深い分析:Claude OpusまたはSonnetの最新版
正しい方向に最適化しているかを測定する
計測なしの最適化は闇雲な作業だ。筆者は3つのメトリクスを追跡している:リクエストあたりの総トークン数、キャッシュヒット率、機能あたりの推定コスト。最初からロギングを追加しよう——「衝撃的な」請求書が届いてから始めるのでは遅い:
def log_api_usage(response) -> dict:
usage = response.usage
cache_read = getattr(usage, 'cache_read_input_tokens', 0)
cache_write = getattr(usage, 'cache_creation_input_tokens', 0)
total_input = usage.input_tokens + cache_read + cache_write
cache_hit_rate = cache_read / total_input if total_input > 0 else 0
# Claude Sonnet 4.6の料金(Anthropicのページで実際の価格を確認してください)
cost = (
usage.input_tokens * 3 / 1_000_000 # 通常入力:$3/MTok
+ cache_write * 3.75 / 1_000_000 # キャッシュ書き込み:$3.75/MTok
+ cache_read * 0.30 / 1_000_000 # キャッシュ読み取り:$0.30/MTok
+ usage.output_tokens * 15 / 1_000_000 # 出力:$15/MTok
)
print(f"コスト: ${cost:.4f} | キャッシュヒット: {cache_hit_rate:.1%} | 出力: {usage.output_tokens} tok")
return {"cost": cost, "cache_hit_rate": cache_hit_rate}
結果と実装の優先順位
これら3つのテクニックにより、API請求額が$340から月約$150に減少した——約56%の削減で、アウトプットの品質は変わらず、バッチ処理のおかげでレイテンシも改善した。
最適化を始めたばかりなら、以下の順序を推奨する:
- まずトークン削減から:プロンプトを圧縮し、余分な空白を削除——追加のコードは不要、今すぐできる
- プロンプトキャッシングを追加:システムプロンプトが1,024トークン以上なら、
cache_controlを追加するだけで完了 - バッチ処理に移行:リアルタイムが不要なユースケース——データ処理、大量コンテンツ生成
プロジェクトの最初からモニタリングを追加しよう。コストが閾値を超えたらアラートを設定する。機能ごとのトークン使用量を定期的にレビューする。この3つの小さな習慣は——スケールアップした際に——どんな最適化テクニックよりも多くを節約してくれる。

