LLM アプリ開発 #12 コスト・評価・観測

読了 5分

第11回まで機能の作り方を扱いました。ところが作ったアプリを実際に動かすには、あと三つ必要です。費用を見積もって減らすこと、答えの品質を測ること、そして何が起きているかを観測することです。今回はこの運用の土台を整理します。

トークン費用を見積もる #

LLM の費用は、やり取りしたトークン量に比例します。呼び出す前に入力が何トークンかをあらかじめ確認するには、トークン計算の機能を使います。

count_tokens.py
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_tokensoutput_tokens で、一度の呼び出しがいくらだったかを追えます。

プロンプトキャッシュで費用を減らす #

RAG や長い system プロンプトのように、毎回の呼び出しで同じ内容が前半に繰り返されるなら、キャッシュで大きく費用を節約できます。一度キャッシュされた部分は、次の呼び出しでずっと安く処理されます。

prompt_caching.py
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 と呼びます。

llm_judge.py
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 に知らせるときに備えて、リクエストの識別子も一緒に記録しておくとよいです。

logging.py
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%割引されます。

batch.py
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 ボットを最初から最後まで作ってみます。

X