LLM アプリ運用 #1 デモと本番のあいだ — 運用の地図

読了 5分

LLM アプリ開発で機能を作り、AI エージェント開発RAG 上級講座で品質を引き上げました。ところが、機能が動くことと、サービスを運用できることは別の問題です。ユーザーが増えると、コストはどこへ漏れているのか、応答はなぜときどき遅いのか、API がレート制限を返したとき何をすべきか、といった疑問が出てきます。このシリーズはその疑問たち、つまり LLM アプリの運用を全7回で扱います。

運用の五つの軸 #

LLM アプリ運用の関心事は五つに整理できます。このシリーズの地図でもあります。

問い扱う回
コストリクエスト一つにいくらかかり、どこで減らすか第2回(会計・ルーティング)、第3回(キャッシュ)、第4回(バッチ)
レイテンシユーザーをどれだけ待たせるか第3回(キャッシュ)、第5回(タイムアウト)
信頼性制限と障害にどう持ちこたえるか第5回
品質答えは良くなっているのか、悪くなっているのか第7回(評価の定常運用)
セキュリティ誰が自分のアプリを操ろうとしているか第6回

ふつうのバックエンド運用と重なるように見えますが、重心が違います。通常の API 呼び出しはコストが実質的に均一なのに対し、LLM 呼び出しはリクエストごとにコストが異なり、その差は数百倍にもなります。応答が正常に数十秒を超えることも、入力に混ざったテキストがアプリの動作を変えうることも、LLM 特有の事情です。そのため、運用の道具も LLM に合わせて作り直す必要があります。

すべての出発点 — リクエスト単位の計測 #

五つの軸のどこを改善するにしても、前提は同じです。今いくら使っているかをリクエスト単位で知ることです。幸い、材料はすべての応答に入っています。usage フィールドです。

usage_logging.py
import json, time, logging

logger = logging.getLogger("llm")

def call_llm(**kwargs):
    start = time.monotonic()
    response = client.messages.create(**kwargs)
    latency = time.monotonic() - start
    u = response.usage
    logger.info(json.dumps({
        "request_id": response._request_id,        # API が付与するリクエスト ID
        "model": response.model,
        "input_tokens": u.input_tokens,
        "output_tokens": u.output_tokens,
        "cache_read": u.cache_read_input_tokens,    # 第3回で本格的に使う
        "cache_write": u.cache_creation_input_tokens,
        "latency_s": round(latency, 2),
        "stop_reason": response.stop_reason,
        "feature": kwargs.get("metadata", {}).get("feature", "unknown"),
    }))
    return response

呼び出しをこのラッパー一つに統一すること、それが第1回の実習のすべてです。押さえておくポイントが三つあります。

  • request_id を残します。応答の _request_id は API 側の追跡識別子です。障害や異常な応答を問い合わせるとき、この値があってはじめて両側のログをつなげられます。
  • 機能タグを付けます。「全体のコストが増えた」は情報になりませんが、「要約機能のコストが増えた」は情報です。呼び出しごとにどの機能の呼び出しかタグを残してこそ、コストを機能別に切り分けられます。
  • stop_reason も記録しますmax_tokens で切られた割合や refusal の割合は、品質問題の早期シグナルです。

トークンはすなわちお金です — 単価の構造 #

ログにたまったトークンをお金に換える換算表が必要です。覚えておく構造は二つです。モデルごとに単価が異なり、入力より出力が高いということです。

モデル入力(100万トークン)出力(100万トークン)
claude-opus-4-8$5.00$25.00
claude-sonnet-4-6$3.00$15.00
claude-haiku-4-5$1.00$5.00

(時期によって変わるので、公式の価格表を基準にします。)出力単価が入力の5倍という非対称が、運用感覚の基礎です。入力1万トークンに出力500トークンという典型的な RAG リクエストなら、トークン数では入力が95%ですが、コストでは出力が20%超を占めます。そして同じリクエストを haiku で処理できるなら、コストは5分の1になります。この二つのレバー(出力の長さ、モデル選択)を第2回で本格的に引きます。

cost_per_request.py
PRICE = {  # 100万トークンあたりのドル(入力、出力)
    "claude-opus-4-8": (5.00, 25.00),
    "claude-sonnet-4-6": (3.00, 15.00),
    "claude-haiku-4-5": (1.00, 5.00),
}

def cost_usd(model: str, input_tokens: int, output_tokens: int) -> float:
    inp, out = PRICE[model]
    return input_tokens / 1e6 * inp + output_tokens / 1e6 * out

この関数をログのパイプラインにつなげば、「機能別の日次コスト」のグラフが出てきます。運用ダッシュボードの最初のパネルです。

ベースライン — 運用の比較対象を作る #

RAG 上級講座の第1回で品質改善のベースラインを作ったように、運用にもベースラインが必要です。計測を一週間回すだけで、こんな数字が手に入ります。

  • 機能別のリクエストあたり平均・95パーセンタイルのコスト
  • リクエストあたり平均・95パーセンタイルのレイテンシ
  • 日次のトークン総量とコスト
  • stop_reason の分布(切り捨て率、拒否率)

以後のシリーズのすべての技法(キャッシュ、バッチ、ルーティング)は、このベースラインとの比較で効果を判定します。「キャッシュを入れたら良くなった気がする」ではなく「入力コストが41%下がった」と言えるようにする土台です。

よくつまずくところ #

  • 月末の請求書でコストを初めて見る — 請求書はどこで漏れたかを教えてくれません。リクエスト単位の usage ロギングがあってこそ原因を追跡できます。
  • 呼び出しコードが散らばっている — 計測のない呼び出しが一か所でも残ると、その機能は視界の外です。ラッパー関数で呼び出し経路を一つに集めます。
  • 平均だけを見る — LLM のコストとレイテンシは裾が長いです。平均が安定していても95パーセンタイルが揺れていれば、一部のユーザーはすでに悪い体験をしています。

まとめ #

今回は運用の地図を描き、計測の土台を作りました。

  • 運用の関心事はコスト・レイテンシ・信頼性・品質・セキュリティの五つの軸です。LLM はリクエストごとにコストが異なる点が、ふつうのバックエンドとの最大の違いです。
  • すべての呼び出しをラッパーに集めて、usage、レイテンシ、stop_reason、機能タグをリクエスト単位で残します。
  • 単価の構造(モデルの差、出力が入力の5倍)を知り、一週間の計測でベースラインを作ります。

次回の「LLM アプリ運用 #2 コスト — トークン会計とモデルルーティング」では、この計測の上でコストを実際に減らし始めます。最大のレバーであるモデル選択を、作業の難易度別に体系化する方法です。

X