LLM アプリ運用 #2 コスト — トークン会計とモデルルーティング

読了 6分

第1回の計測で、どこにいくらかかっているかが見え始めました。今度は減らします。コスト削減の手段はいろいろありますが効果の大きさが異なるので、この記事は大きなレバーから順に進めます。入力を減らし、出力を減らし、そして最大のもの、作業に合ったモデルへ送るルーティングです。

送る前に測る — count_tokens #

トークン会計の基本道具から押さえます。API には、リクエストを送らずにトークン数だけを数える count_tokens エンドポイントがあります。

count_tokens.py
count = client.messages.count_tokens(
    model="claude-opus-4-8",
    system=SYSTEM,
    messages=messages,
)
print(count.input_tokens)   # このリクエストの入力トークン数

使いどころは二つです。第一に、上限ゲートです。ユーザーがアップロードした文書が異常に大きい場合、送る前に分かるので拒否や分割ができます。第二に、設計段階の見積もりです。system プロンプトを直したり RAG のチャンク数を変えたりするとき、入力が何トークン増えたかをデプロイ前に把握できます。注意点を一つ挙げると、トークン化はモデルごとに異なるので、実際に使うモデルで数えないと正確になりません。

入力のダイエット — 積み上がるものを疑う #

第1回のログで入力トークンが大きい機能を開いてみると、犯人はたいてい本文ではなく、一緒に紛れ込んでくるものです。

  • 会話の累積 — チャットボットが会話全体を毎回送り直しています。LLM アプリ開発実践 第9回の要約・切り捨ての技法は、コストの技法でもあります。
  • RAG チャンクの過剰 — top_k を増やしたまま忘れると、毎リクエストがその分だけ重くなります。RAG 上級講座 第4回のリランキングは、品質とコストを同時に押さえる装置です。
  • ツール結果の肥大 — エージェントのツールが丸ごと返してくる JSON です。AI エージェント開発 第4回の結果上限がここでも働きます。

すでに他のシリーズで作った装置が全部コスト装置だった、というのがこの節の要点です。新しく作るものは少なく、第1回の計測でどの装置が外れているかを探す作業がほとんどです。

出力のダイエット — 単価5倍のトークン #

出力は入力の5倍の単価なので、同じトークン数を減らしても効果は5倍です。つまみは二つあります。

  • プロンプトで長さを指示します。「3文以内で」「JSON のみ出力し説明は省略」のような明示的な指示が最も効果的です。特に構造化出力を使う機能では、モデルが付ける前置きや補足は純粋なコストです。
  • max_tokens は安全網として使います。max_tokens は長さを誘導するパラメータではなく上限です。締めすぎるとエージェント 第1回で見た打ち切り(stop_reason: max_tokens)が増え、リトライのコストがかえって上乗せされます。第1回のログの打ち切り率を見ながら、余裕を持たせておきます。

モデルルーティング — 最大のレバー #

この記事の本題です。第1回の単価表で見たように、モデル間の単価差は5倍に達します。ところがアプリの中の作業は、すべてが同じ難易度ではありません。分類、ルーティング判断、要約のような作業は小さいモデルで十分で、複雑な推論やエージェント作業には大きいモデルが必要です。作業ごとにモデルを分けて送ることがモデルルーティングで、ほとんどのサービスで最大のコスト削減はここから生まれます。

model_routing.py
ROUTES = {
    # 単純な作業 — 速くて安いモデル
    "classify_intent":  {"model": "claude-haiku-4-5",  "max_tokens": 256},
    "rewrite_query":    {"model": "claude-haiku-4-5",  "max_tokens": 300},
    "summarize_doc":    {"model": "claude-sonnet-4-6", "max_tokens": 1024},
    # 中核の作業 — 品質優先
    "answer_question":  {"model": "claude-opus-4-8",   "max_tokens": 4096},
    "agent_task":       {"model": "claude-opus-4-8",   "max_tokens": 16000},
}

def call(task: str, **kwargs):
    route = ROUTES[task]
    return call_llm(model=route["model"], max_tokens=route["max_tokens"],
                    metadata={"feature": task}, **kwargs)

ルーティング設計の原則は三つです。

  • 品質基準は計測で決めます。「この作業は haiku で十分か」は勘ではなく評価セットで判定します。RAG 上級講座 第6回の評価パイプラインをモデル比較にそのまま使えます。精度が同じなら、安いモデルが正解です。
  • すでにルーティングしていた場所を探しますRAG 上級講座 第4回のクエリリライトが haiku だったように、補助作業はすでに候補です。第1回のログで機能別コストを見て、高いモデルへ送られている単純な作業から移します。
  • 境界線上の作業は上へ送ります。迷ったら高いモデルに置いておき、計測が十分になってから下げます。ルーティングの失敗モードはコストではなく品質低下で、そちらのほうが立て直しに手間がかかります。

effort — 同じモデルの中の調整つまみ #

モデルを変えずに調整できるつまみがもう一つあります。effort パラメータです。エージェント 第3回では品質の側面から紹介しましたが、運用の観点ではトークン使用量のつまみでもあります。effort を下げるとモデルがより簡潔に考えて答えるためトークンが減り、上げるとその逆になります。

effort_by_task.py
# 単純な抽出 — 低い effort でトークンを節約
response = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=1024,
    output_config={"effort": "low"},
    messages=[...],
)

ルーティングと足並みをそろえて、作業の定義にモデルと一緒に effort を置けば、調整ポイントが一か所に集まります。ただし、エージェントのように深い推論がステップ数を減らしてくれる作業では、effort を下げることが総コストを上げる場合もあると第3回で見ました。ここでも判定基準は計測です。

よくつまずくところ #

  • すべての作業を一つのモデルに送る — 「とにかく一番良いモデル」はデモのときの選択です。分類一回に5倍の単価を使うことが積み重なると請求書になります。
  • 品質検証なしにモデルを下げる — コストはすぐ見えますが、品質低下は遅れて見えます。下げる前に評価セットで比較し、下げた後に品質指標を監視する、という順序です。
  • max_tokens で出力を減らそうとする — 打ち切りは節約ではなく不良です。長さはプロンプトで指示し、max_tokens は安全網として置きます。

まとめ #

今回はコストのレバーを大きいものから引きました。

  • count_tokens で送る前に測り、入力に紛れ込んでくるもの(会話の累積、RAG チャンク、ツール結果)をダイエットします。
  • 出力は単価5倍です。長さはプロンプトで指示します。
  • 最大のレバーは作業の難易度別モデルルーティングです。品質基準は評価セットの計測で決め、effort でもう一段調整します。

ところで、まだ手を付けていない大きな塊が一つあります。毎リクエスト同じように繰り返される system プロンプトとツール定義です。次回の「LLM アプリ運用 #3 プロンプトキャッシング実践」で、その繰り返し分の価格を10分の1にします。

X