LLM アプリ開発 #12 コスト・評価・観測
第11回まで機能の作り方を扱いました。ところが作ったアプリを実際に動かすには、あと三つ必要です。費用を見積もって減らすこと、答えの品質を測ること、そして何が起きているかを観測することです。今回はこの運用の土台を整理します。
トークン費用を見積もる #
LLM の費用は、やり取りしたトークン量に比例します。呼び出す前に入力が何トークンかをあらかじめ確認するには、トークン計算の機能を使います。
import anthropic
client = anthropic.Anthropic()
result = client.messages.count_tokens(
model="claude-sonnet-4-6",
messages=[{"role": "user", "content": open("long_document.txt").read()}],
)
print(result.input_tokens) # 入力トークン数トークン数はモデルごとに異なる数え方になりうるので、実際に使うモデルを指定して測ります。応答を受け取ったあとは response.usage で実際の使用量を確認します。input_tokens と output_tokens で、一度の呼び出しがいくらだったかを追えます。
プロンプトキャッシュで費用を減らす #
RAG や長い system プロンプトのように、毎回の呼び出しで同じ内容が前半に繰り返されるなら、キャッシュで大きく費用を節約できます。一度キャッシュされた部分は、次の呼び出しでずっと安く処理されます。
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
system=[
{
"type": "text",
"text": large_shared_context, # 毎回同じ大きな文脈
"cache_control": {"type": "ephemeral"},
}
],
messages=[{"role": "user", "content": "質問"}],
)
print(response.usage.cache_read_input_tokens) # キャッシュから読んだトークンキャッシュは前半が一字一句同じときだけ適用されます。ですから変わらない内容(固定の指示、共有文書)を前に置き、毎回変わる内容(質問、時刻)を後ろに置きます。きちんと動くかは usage.cache_read_input_tokens で確認します。この値がずっと0なら、前半に毎回変わる何かが入ってキャッシュが壊れている合図です。
モデルで費用を調整する #
第2回で見た三つの等級を、費用の観点で見直します。すべての呼び出しに最も強力なモデルを使う必要はありません。作業の性格に合わせて等級を分ければ、費用が大きく減ります。
- 単純な分類、短い抽出 → 最も安い Haiku
- ほとんどの実務作業 → バランスの Sonnet
- 難しい推論、長いエージェント作業 → 最も強力な Opus
一つのアプリの中でも、段階ごとに違うモデルを使えます。たとえば分類は Haiku で、最終の答えの生成は Sonnet で分ける、という具合です。
答えの品質を評価する #
プロンプトを変えたとき、答えが良くなったかをどう知るのでしょうか。目でいくつか見るだけでは足りません。評価を自動化する一つの方法は、別の LLM 呼び出しに採点を任せることです。これを LLM-as-judge と呼びます。
def judge(question: str, answer: str) -> str:
prompt = f"""下の答えが質問に対して正確で役立つかを評価してください。
「良い」または「悪い」の一語だけで答えてください。
質問: {question}
答え: {answer}"""
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=10,
messages=[{"role": "user", "content": prompt}],
)
return next(b.text for b in response.content if b.type == "text")質問と期待する答えの組(評価セット)をあらかじめ作っておき、プロンプトやモデルを変えるたびにこの評価を回せば、変更が品質を上げたか下げたかを数字で比べられます。第5回で見た構造化された出力で採点結果を受け取れば、集計がさらに楽になります。
動作をのぞく #
LLM アプリは同じ入力でも答えが変わるので、問題が起きたとき何があったかを見るには記録が必要です。最低限、入力プロンプト、受け取った答え、使ったトークンを残します。問題を Anthropic に知らせるときに備えて、リクエストの識別子も一緒に記録しておくとよいです。
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=messages,
)
log({
"request_id": response._request_id, # 問題追跡用の識別子
"input_tokens": response.usage.input_tokens,
"output_tokens": response.usage.output_tokens,
"stop_reason": response.stop_reason,
})こうしてためた記録で費用の推移を見て、答えがよく切れるか(stop_reason)、特定の入力で問題が起きるかを追います。
バッチでさらに減らす #
急ぎでない大量の作業なら、バッチ API で費用を半分に減らせます。リアルタイムの応答ではなく、多くのリクエストを一度に預けて、結果を後で(ふつう1時間以内に)受け取る方式です。トークン料金が50%割引されます。
from anthropic.types.message_create_params import MessageCreateParamsNonStreaming
from anthropic.types.messages.batch_create_params import Request
batch = client.messages.batches.create(
requests=[
Request(
custom_id=f"item-{i}",
params=MessageCreateParamsNonStreaming(
model="claude-sonnet-4-6",
max_tokens=256,
messages=[{"role": "user", "content": text}],
),
)
for i, text in enumerate(texts)
]
)数千件のレビューを分類したり、文書をまとめて要約したりするように、すぐ答えが要らない作業によく合います。ユーザーが待つ対話型の呼び出しには合いませんが、バックグラウンドの一括処理では費用を大きく節約します。
よくつまずくところ #
- キャッシュが壊れるのに気づかない — system の前半に現在時刻やランダム値が入ると、毎回キャッシュが壊れます。
cache_read_input_tokensが0でないか確認します。 - 評価なしでプロンプトを直す — 評価セットなしに勘でプロンプトを変えると、ある場合は良くなり別の場合は悪くなるのを見逃します。小さくても評価セットを設けます。
- 何も記録しない — 記録がないと、問題が起きたとき再現も追跡も難しいです。最低限の使用量と識別子は残します。
まとめ #
今回は、アプリを運用するのに必要なコスト・評価・観測を整理しました。
- トークンをあらかじめ測り(
count_tokens)、繰り返す前半はキャッシュで減らし、作業に合うモデルの等級を選びます。 - LLM-as-judge と評価セットで、変更が品質に与えた影響を数字で見ます。
- 使用量とリクエストの識別子を記録して、費用と問題を追います。
これで機能と運用の土台がすべて集まりました。最後の記事「LLM アプリ開発 #13 実践プロジェクト」では、これまでのかけらを一つに束ねて、社内文書に答える Q&A ボットを最初から最後まで作ってみます。