LLM アプリ運用 #1 デモと本番のあいだ — 運用の地図
LLM アプリ開発で機能を作り、AI エージェント開発と RAG 上級講座で品質を引き上げました。ところが、機能が動くことと、サービスを運用できることは別の問題です。ユーザーが増えると、コストはどこへ漏れているのか、応答はなぜときどき遅いのか、API がレート制限を返したとき何をすべきか、といった疑問が出てきます。このシリーズはその疑問たち、つまり LLM アプリの運用を全7回で扱います。
運用の五つの軸 #
LLM アプリ運用の関心事は五つに整理できます。このシリーズの地図でもあります。
| 軸 | 問い | 扱う回 |
|---|---|---|
| コスト | リクエスト一つにいくらかかり、どこで減らすか | 第2回(会計・ルーティング)、第3回(キャッシュ)、第4回(バッチ) |
| レイテンシ | ユーザーをどれだけ待たせるか | 第3回(キャッシュ)、第5回(タイムアウト) |
| 信頼性 | 制限と障害にどう持ちこたえるか | 第5回 |
| 品質 | 答えは良くなっているのか、悪くなっているのか | 第7回(評価の定常運用) |
| セキュリティ | 誰が自分のアプリを操ろうとしているか | 第6回 |
ふつうのバックエンド運用と重なるように見えますが、重心が違います。通常の API 呼び出しはコストが実質的に均一なのに対し、LLM 呼び出しはリクエストごとにコストが異なり、その差は数百倍にもなります。応答が正常に数十秒を超えることも、入力に混ざったテキストがアプリの動作を変えうることも、LLM 特有の事情です。そのため、運用の道具も LLM に合わせて作り直す必要があります。
すべての出発点 — リクエスト単位の計測 #
五つの軸のどこを改善するにしても、前提は同じです。今いくら使っているかをリクエスト単位で知ることです。幸い、材料はすべての応答に入っています。usage フィールドです。
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回で本格的に引きます。
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 コスト — トークン会計とモデルルーティング」では、この計測の上でコストを実際に減らし始めます。最大のレバーであるモデル選択を、作業の難易度別に体系化する方法です。