0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LLMのAPI代に家計簿をつけたら、いちばん高い費目は「毎回同じ自己紹介」だった ― プロンプトキャッシュ・effort・Batch APIで月9.6万円を2.5万円にした仕訳の記録

0
Posted at

Cozy Sunlit Budget Planning Workspace.png

ご利用明細(2026年10月分)
  10/31  ANTHROPIC          USD 644.32
         円換算 約96,600円(1ドル150円で概算)

日曜の朝、カードの明細を開いて、コーヒーを置きました。

ひと月で、だいたい9万6千円。高いか安いかで言えば、仕事の道具としては払えない額ではありません。困ったのは、そこではなかったんです。何に使ったのか、自分で説明できなかった。

子どものころ、母は毎晩、台所のテーブルで家計簿をつけていました。レシートをノートに貼って、費目ごとに赤ペンで丸をつける。あるとき「なんでそんな面倒なことするの」と聞いたら、こう返ってきました。

「お金は消えるんじゃないの。何に使ったか、忘れるだけ」

というわけで、この週末は LLM の API 代に家計簿をつけてみました。レシートを集めて、費目を決めて、仕訳して、反省して、来月の予算を立てる。母のノートと同じ順番です。

この家計簿の数字について
実際のログにはお客さんの案件が混ざっているので、記事の数字は、構成とおおよその量を似せた合成データで再現したものです。生成スクリプトと集計スクリプトは記事の中にすべて載せていて、手元で同じ数字が出ます。単価は 2026年11月8日時点の Anthropic 公式の価格ページによるものです。使う前に、必ず最新の価格を確認してください。

1ページ目:レシートを集める(usage を捨てない)

家計簿でいちばん大事なのは、レシートを捨てないことです。

LLM の API でいうレシートは、レスポンスについてくる usage です。Claude の Messages API なら、1回の呼び出しごとに、こんな数字が返ってきます。

フィールド 中身 単価のイメージ
input_tokens キャッシュされなかった入力 基本入力の単価
cache_creation_input_tokens キャッシュに書き込んだ入力 5分なら1.25倍、1時間なら2倍
cache_read_input_tokens キャッシュから読んだ入力 0.1倍(Opus 5.5 と Sonnet 5.5 は0.05倍)
output_tokens 出力(思考トークンを含む) 出力の単価

自分はこれまで、返ってきたテキストだけを使って、usage は捨てていました。レシートを受け取らずにレジを離れていたようなものです。そりゃあ、月末に何もわからないわけです。

なので、まずは API を呼ぶところに、レシートを貼る処理を1つだけ足しました。

import json
import time

import anthropic

client = anthropic.Anthropic()


def ask(tag, **kwargs):
    """API を呼んで、usage を1行の JSON として家計簿に貼る。"""
    res = client.messages.create(**kwargs)
    text = "".join(b.text for b in res.content if b.type == "text")
    with open("usage.jsonl", "a", encoding="utf-8") as f:
        f.write(json.dumps({
            "tag": tag,                    # 費目(用途)。これがないと仕訳できない
            "model": kwargs["model"],
            "t": int(time.time()),
            "batch": False,
            "visible_chars": len(text),    # 画面に出た文字数(あとで効いてくる)
            "usage": res.usage.model_dump(),
        }, ensure_ascii=False) + "\n")
    return res, text

ポイントは tag です。家計簿で「食費」「日用品」と書き分けるのと同じで、用途ごとに名前をつけておかないと、あとで仕訳できません。API キーを用途ごとに分けるのでもいいのですが、自分は1行で済むこちらにしました。

2ページ目:費目を決めて、仕訳する

この1か月、API を使っていた用途は4つでした。

  • A 問い合わせ分類:不動産会社のお問い合わせフォームに来た文面を「内見希望」「賃料交渉」「その他」などに振り分ける。1日300件くらい
  • B 社内文書RAG:別のお客さんの社内文書の検索チャット。平日に1日100件くらい
  • C 物件要約(夜間):毎晩3時に、新着物件400件の紹介文を要約する
  • D 議事録要約:打ち合わせの文字起こしの要約。月24本

レシート(usage.jsonl)を読んで、用途ごと・費目ごとに金額を出すのが、次のスクリプトです。

#!/usr/bin/env python3
"""LLM API の「家計簿」。usage を保存した JSONL を、用途ごと・費目ごとに仕訳する。"""
import json
import sys
from collections import defaultdict

# 単位: USD / 100万トークン(2026-11-08 時点の公式価格ページより。使う前に必ず確認を)
PRICES = {
    "claude-opus-5-5":   {"in": 4.00, "w5m": 5.00,  "w1h": 8.00, "read": 0.20, "out": 20.00},
    "claude-sonnet-5-5": {"in": 2.00, "w5m": 2.50,  "w1h": 4.00, "read": 0.10, "out": 10.00},
    "claude-haiku-5-5":  {"in": 0.10, "w5m": 0.125, "w1h": 0.20, "read": 0.01, "out": 0.50},
}
# Haiku 5.5 だけは、プロンプトが10万トークンを超えると「リクエスト全体」がこの単価になる
HAIKU_LONG = {"in": 0.50, "w5m": 0.625, "w1h": 1.00, "read": 0.05, "out": 2.50}
LONG_THRESHOLD = 100_000

BATCH_DISCOUNT = 0.5   # Batch API は全費目が半額。キャッシュの割引とも重なる
YEN_PER_USD = 150      # 為替はざっくり固定


def unit_prices(rec):
    u = rec["usage"]
    prompt_len = (u["input_tokens"] + (u.get("cache_creation_input_tokens") or 0)
                  + (u.get("cache_read_input_tokens") or 0))
    if rec["model"] == "claude-haiku-5-5" and prompt_len > LONG_THRESHOLD:
        return HAIKU_LONG, True
    return PRICES[rec["model"]], False


def cost_of(rec):
    """1リクエストの費用を費目ごとに USD で返す。"""
    u = rec["usage"]
    p, is_long = unit_prices(rec)
    created = u.get("cache_creation") or {}
    w5m = created.get("ephemeral_5m_input_tokens") or 0
    w1h = created.get("ephemeral_1h_input_tokens") or 0
    m = BATCH_DISCOUNT if rec.get("batch") else 1.0
    raw = {
        "基本入力":     u["input_tokens"] * p["in"],
        "キャッシュ書込": w5m * p["w5m"] + w1h * p["w1h"],
        "キャッシュ読込": (u.get("cache_read_input_tokens") or 0) * p["read"],
        "出力":         u["output_tokens"] * p["out"],
    }
    return {k: v / 1_000_000 * m for k, v in raw.items()}, is_long

(集計して表にする main() は長いので省略しました。全体は記事の最後にまとめて置いています)

さて、10月分を仕訳した結果がこちらです。

用途                 回数      金額(円)     割合     ヒット率     出力tok/表示字
A 問い合わせ分類       9,336     53,483    55%      17%           9.3
C 物件要約(夜間)     12,400     35,308    37%       0%           1.2
B 社内文書RAG       2,271      7,616     8%       0%           0.6
D 議事録要約            24        241     0%       0%           0.8  ※10万超え 24件
合計                        96,648

費目別:
  基本入力          30,575 円  (32%)
  キャッシュ書込       35,582 円  (37%)
  キャッシュ読込          313 円  (0%)
  出力            30,177 円  (31%)

母なら、ここで赤ペンを持つところです。おかしな所が3つあります。

  1. いちばん高い費目が「キャッシュ書込」。キャッシュは節約のための仕組みなのに、書き込みにいちばんお金を払っている
  2. A の「出力tok/表示字」が 9.3。画面に出ているのは「内見希望」くらいの短いラベルなのに、1文字あたり9トークンも払っている
  3. D に「10万超え 24件」の印がついている

1つずつ、反省していきます。

反省その1:固定費「毎回同じ自己紹介」

A の問い合わせ分類は、毎回のリクエストの頭に、6,200トークンくらいの「自己紹介」がついていました。分類のルール、ラベルの定義、過去の問い合わせの例。中身は毎回同じです。

毎回同じなら、プロンプトキャッシュの出番です。実際、cache_control はちゃんとつけていました。なのにヒット率は17%。犯人は、system プロンプトの1行目でした。

現在時刻: 2026-10-14 10:32
あなたは不動産会社のお問い合わせを分類するアシスタントです。
……

「今日は何日か」を知っておいてほしくて、何年か前の自分が入れた1行です。

プロンプトキャッシュは先頭からの完全一致で効きます。並び順は tools → system → messages で、どこか1バイトでも変われば、そこから後ろは全部キャッシュが外れます。先頭に「分」まで入った時刻があるので、キャッシュが使えるのは同じ1分の中に来た問い合わせだけ。17%というのは、たまたま同じ分に重なった分でした。

もっと悪いことに、外れるたびに**書き込み代(基本入力の1.25倍)**を払っていました。キャッシュしないほうが安いくらいです。毎日、日付の違うポイントカードを新しく作っては、一度も使わずに捨てていたようなものでした。

直し方は単純で、変わる情報は後ろに回します。

SYSTEM = open("prompts/inquiry_classifier.md", encoding="utf-8").read()  # 日付は入れない

res, label = ask(
    "A 問い合わせ分類",
    model="claude-haiku-5-5",
    max_tokens=256,
    system=[{"type": "text", "text": SYSTEM, "cache_control": {"type": "ephemeral"}}],
    output_config={"effort": "low"},
    messages=[{
        "role": "user",
        "content": f"受付日時: {received_at:%Y-%m-%d %H:%M}\n\n{body}",
    }],
)

受付日時は、ユーザーのメッセージの先頭に移しました。分類に必要なのは「いつ届いた問い合わせか」なので、むしろこちらのほうが意味としても正しかったです。

確認は、コードを眺めるよりレシートを見るほうが確実です。何回か続けて呼んで、cache_read_input_tokens がずっと0なら、どこかに「毎回変わる何か」が紛れています。日時のほかによくあるのは、リクエストIDや、キーの順番が毎回変わる JSON、呼ぶたびに中身や並びが変わるツール一覧です。

それから、短すぎるプロンプトはキャッシュされません。最小の長さはモデルごとに違っていて、最新世代のモデルなら512トークンです(世代によっては4,096トークンのものもあります)。足りないときはエラーにならず、黙ってキャッシュされないので、ここもレシートで確かめます。

おまけ:電気の料金プランを選ぶように、TTL を選ぶ

B の社内文書チャットは、そもそも cache_control をつけ忘れていました(ヒット率0%)。つけるときに迷ったのが、キャッシュの寿命です。

  • 5分:書き込みは1.25倍。読まれるたびに寿命が5分延びる
  • 1時間:書き込みは2倍。そのかわり1時間もつ

B の利用は平日の1日100件くらいなので、間隔は平均6分ほど。5分のプランだと、ちょうど切れたところに次の質問が来ます。同じ合成データで比べると、こうなりました。

TTL B のヒット率 B の月額
キャッシュなし(10月) 0% 7,616円
5分 18% 6,787円
1時間 31% 5,841円

ヒット率が31%で止まっているのは、検索で取ってきた文書(毎回違う)が入力の大半を占めているからです。キャッシュできるのは、共通の system プロンプトの2,800トークンだけ。家計簿の「ヒット率」はトークンの割合で出しているので、ここが上限になります。

選び方の目安は、同じプレフィックスを使うリクエストの「開始から開始まで」の間隔です。ずっと5分未満なら5分、5分〜1時間のあいだに散らばるなら1時間。平均ではなく、間隔の分布で見るのがコツでした。電気の料金プランと同じで、平均の使用量より、使う時間帯のほうが効いてきます。

反省その2:見えない出費(思考トークン)

A の「出力tok/表示字」が9.3だったのは、思考トークンでした。

Claude Opus 5.5 は思考(thinking)を切れません。そして、思考の中身が画面に出なくても(デフォルトの display は "omitted" です)、思考したぶんは output_tokens に含まれて、出力の単価で請求されます。レシートには載っているのに、商品は袋に入っていない。家計簿でいえば「使途不明金」です。

しかも Opus 5.5 は、effort を指定しないと medium で動きます。「内見希望かどうか」を見分けるのに、毎回500トークン以上考えてもらっていたわけです。

深さは output_config の effort で決められます。分類や抽出なら low で十分なことが多いので、A では low にしました。visible_chars をレシートに貼っておいたのは、この比率を見るためです。表示した文字数にくらべて出力トークンが一桁多い用途は、まず effort を疑います。

逆に、コードを書かせるような難しい用途で effort を下げすぎると、やり直しが増えて、かえって高くつきます。1リクエストの値段ではなく、1つの仕事が終わるまでの値段で比べるのが大事でした。

反省その3:ブランド品を普段使いしていた

A では、最初からいちばん賢い Opus を使っていました。理由は「なんとなく安心だから」。普段の買い物にブランド品の財布を持ち歩くのは別にいいのですが、毎日の振り分けの仕事にまでそれを使っていたのは、さすがにやりすぎでした。

10月7日に出た Claude Haiku 5.5 は、10万トークン以下のプロンプトなら入力が100万トークンあたり 0.10ドル、出力が 0.50ドルです。Opus 5.5(入力4ドル、出力20ドル)の40分の1。Anthropic の発表では、平均すると Haiku 4.5 より75%ほど安く動くそうです。

とはいえ、「安物買いの銭失い」ということわざもあります。乗り換える前に、過去の問い合わせ200件を両方のモデルに分類させて、ラベルを突き合わせました。食い違ったのは3件で、どれも人が読んでも迷う文面でした。この程度なら、迷ったものだけ人に回す今の運用で吸収できます。

A について、どの手がどれだけ効いたのかを1段ずつ分けると、こうなります。

0. 10月のまま                  53,483 円
1. 日付をユーザーターンへ             24,547 円
2. + effort を low に        12,807 円
3. + Haiku 5.5 に切り替え          337 円

最後の1段の落差が大きすぎて、少し笑ってしまいました。ただ、ここで言いたいのは「安いモデルにしよう」ではありません。日付を1行動かすだけで半分以下になった、というほうです。モデルを替えるのは評価と相談が要りますが、日付の移動はお客さんに説明するまでもない、ただのバグ修正でした。

反省その4:急がない買い物を、コンビニで買っていた

C の物件要約は、毎晩3時に400件を順番に API に投げていました。誰も起きていない時間です。要約が載るのは朝9時のメール。つまり、6時間待っても誰も困らない。

こういう仕事には、Message Batches API があります。リクエストをまとめて預けると、非同期で処理してくれて、全部の費目が半額になります。たいていは1時間以内に終わり、長くても24時間です。

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"listing-{listing.id}",
        params=MessageCreateParamsNonStreaming(
            model="claude-opus-5-5",
            max_tokens=1024,
            system=[{"type": "text", "text": SUMMARY_RULES,
                     "cache_control": {"type": "ephemeral"}}],
            messages=[{"role": "user", "content": listing.description}],
        ),
    )
    for listing in new_listings
])

ハマりどころは1つだけで、結果は順番どおりに返ってきません。custom_id で突き合わせます。最初、リストの何番目かで対応づけようとして、物件Aの紹介文が物件Bに載る、という事故を起こしかけました(テスト環境で気づいたので、セーフです)。

バッチの割引は、キャッシュの割引とも重なります。ただ、同時に流れるバッチの中でのキャッシュヒットは「できればヒットする」くらいの扱いなので、今回の試算には入れていません。

反省その5:まとめ買いの罠(10万トークンの壁)

D の議事録要約は、金額としては月241円。家計簿の中では端数みたいなものです。でも、単価を見て、冷や汗が出ました。

Haiku 5.5 は、プロンプトが10万トークンを超えると、そのリクエスト全体の単価が5倍(入力0.50ドル、出力2.50ドル)になります。超えた部分だけではなく、全体です。キャッシュから読んだ部分も、プロンプトの長さに数えられます。

2時間の打ち合わせの文字起こしは、だいたい11万〜14万トークン。24本、全部が壁の向こう側でした。

文字起こしを前半と後半に分けて、それぞれ要約してから最後にまとめるようにしたら、241円が49円になりました。金額は小さいですが、同じ作りで長い文書を毎日何百本も流していたら、と考えるとぞっとします。送料無料ラインの逆、と覚えておくことにしました。

番外:内容量の「ステルス値上げ」に気をつける

もう1つ、今回の家計簿では起きていないけれど、書き留めておきたいことがあります。

スーパーのお菓子が、値段はそのままで1袋の量が減っていることがありますよね。LLM にも似た話があって、Claude 4.7 以降のモデルは新しいトークナイザーを使っていて、同じ文章でも、古いモデルよりおよそ30%多いトークン数になると、公式の価格ページに書かれています(どれくらい増えるかは中身によるそうです)。

古いモデルから乗り換えるとき、単価の表だけを比べると見誤ります。乗り換える前に、自分の実際のプロンプトでトークン数を数え直すのが確実です。

n = client.messages.count_tokens(
    model="claude-haiku-5-5",
    system=SYSTEM,
    messages=[{"role": "user", "content": sample_inquiry}],
).input_tokens

逆向きの話もあります。Sonnet 5 は、発売時の価格(入力2ドル、出力10ドル)が8月末までの期間限定とされていて、9月1日に3ドル/15ドルへ上がる予定でした。それが取りやめになって、今は2ドル/10ドルが正式な価格です。価格表は、思っているより動きます。家計簿をつける日に、価格表も一緒に見直すことにしました。

来月の予算:仕訳しなおした11月の見込み

ここまでの手を全部入れて、同じ合成データで11月分を試算しました(B は5分のキャッシュのままの数字です)。

用途                 回数      金額(円)     割合     ヒット率     出力tok/表示字
C 物件要約(夜間)     12,400     17,654    71%       0%           1.2
B 社内文書RAG       2,271      6,787    27%      18%           0.6
A 問い合わせ分類       9,336        340     1%      83%           1.8
D 議事録要約            48         49     0%       0%           0.8
合計                        24,830
用途 10月 11月(見込み) やったこと
A 問い合わせ分類 53,483円 340円 日付を後ろへ、effort low、Haiku 5.5
C 物件要約(夜間) 35,308円 17,654円 Batch API
B 社内文書RAG 7,616円 6,787円 キャッシュ(5分)
D 議事録要約 241円 49円 10万トークン未満に分割
合計 96,648円 24,830円

96,648円から24,830円へ、およそ4分の1です。B を1時間のキャッシュにすると、さらに1,000円ほど下がって23,884円になります。

おもしろいのは、表の並び順が変わったことです。10月にいちばん高かった A は最下位近くに沈んで、今度は C が全体の7割を占めています。家計簿は、次にどこを見ればいいかを教えてくれます。C は、要約のルール(1,800トークン)をバッチの中でキャッシュする手と、Sonnet で足りるかを試す手が残っています。それは来月の家計簿の宿題にします。

家計簿を続けるための5つのこと

  1. レシート(usage)を必ず捨てずに残す。 用途ごとの tag も一緒に
  2. 月に一度、仕訳する。 金額より先に「割合」と「ヒット率」を見る
  3. 変わるものは後ろに置く。 日時、ID、ユーザーごとの情報は、キャッシュの境目より後ろへ
  4. 待てる仕事は、バッチに回す。 「誰が、いつまでに見るか」を最初に決める
  5. 価格表も、月に一度は見直す。 単価、倍率、しきい値は、思っているより動く

Amazon の CTO の Werner Vogels は、2023年の re:Invent で「The Frugal Architect」という考え方を紹介しました。その1つめの法則が「コストを非機能要件にする」です。応答速度や可用性と同じように、コストも最初から設計の中で考えておく、という意味です。LLM のアプリでは、この話がいっそう身近になりました。プロンプトの1行の置き場所が、そのまま請求額に響くからです。

古い中国の『礼記』には、「入るを量りて出ずるを為す」という言葉があります。入ってくるお金をよく見積もってから、出ていくお金を決める。ただ、その前にもう一段あって、出ていったお金が何に使われたのかを知らないと、量りようがない。今回の家計簿でいちばん身にしみたのは、そこでした。

「うちのAPI代、何に使ってるのか説明できない」という方がいたら、まず usage を1か月ぶん貯めてみてください。一緒にレシートを広げる相談なら、いつでも乗ります。たぶん最初に見つかるのは、自分と同じ、system プロンプトの1行目の日付です。

付録:この記事のコード

  • simulate.py:ひと月分の usage ログ(合成データ)を作る。到着時刻を作って、キャッシュの寿命(最後に使った時刻+TTL)を追いかけ、書き込みか読み込みかを決める
  • ledger.py:家計簿本体。上に載せた関数に、集計して表示する main() を足したもの
  • stages_a.py:A の改善を1段ずつ分けて試算する
python3 simulate.py before > october.jsonl
python3 simulate.py after  > november.jsonl
python3 simulate.py after 3600 > november_b1h.jsonl   # B を1時間のキャッシュにした場合
python3 ledger.py october.jsonl
python3 ledger.py november.jsonl
python3 stages_a.py

乱数のシードは固定しているので、記事と同じ数字が出ます。キャッシュの動きはかなり単純にした模型で、本物の挙動(同時に来たリクエストどうしの書き込みの競合など)までは再現していません。数字そのものより、どの費目が効いているかの比べ方として使ってください。


あの日、母に「なんでそんな面倒なことするの」と聞いたあと、母は家計簿の最後のページを見せてくれました。毎月の欄外に、小さな字で1行ずつ、何かが書いてありました。

「10月 電気代が高いのは、こたつを出したから。でも、あったかかったので良し」

節約のためだけの帳簿じゃなかったんだな、と、今なら思います。払ったお金のうち、どれが納得のいく出費で、どれが知らないうちに出ていったお金なのか。それを見分けるためのノートでした。

自分の家計簿の11月の欄外には、こう書いておきます。

「C の物件要約に1.7万円。朝9時のメールを、毎日ちゃんと読んでくれている人がいるので良し」

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?