コストエンジニアリングの定義:「月末に請求書を見る」を日々の運用指標に変える

ほとんどのチームが AI のコスト問題に最初に気づくのは、ベンダーの月間請求書が経理のメールアドレスに届いた日です。その一枚に書かれているのは数字がひとつだけで、「先月いくらかかったか」。コストが上がったかどうかまでは分かります。「なぜ高くなったのか」「どの事業で高くなったのか」「さらに2倍になったら最初に潰れるのはどこか」には答えられません。やがて起こることはほぼ毎回同じです。会議を開いて、原因をいくつか当てて、散会して、来月また同じことをする。このループは半年続いても何も改善しません。請求書というデータ構造そのものが、原因追究を許していないからです。

コストエンジニアリングが最初にやるべきことは、節約ではなくデータ単位の入れ替えです。「金」を「原因を特定できるコストイベント」に落とし、「月次の総額」を「リアルタイム累積+ディメンション別の内訳+速度」に組み替えます。コストと、機能・テナント・モデル・チャネルといった対象との対応がついたとき、コストは財務的な結果から運用シグナルへ変わります。運用シグナルの特徴は、毎日確認でき、閾値を設定でき、行動を起動でき、責任者を特定できる、ということです。

混同されやすい境界を先に整理しておきます。コストエンジニアリングは推論最適化とは違います。推論最適化が扱うのは、同じ負荷の中で少ない計算力で同じ仕事を終える方法であり、作用域はエンジン・量子化・バッチスケジューリングの層にあります。コストエンジニアリングが扱うのは、製品のビジネス形態が推論コストと釣り合っているかどうかであり、作用域は会計・原因帰属・価格設定・予算・プロダクトデザインの層です。両者は互いに条件になります。コストエンジニアリングがなければ、ある推論最適化が結局いくら節約したのか判断できません。分母が曖昧になるからです。推論最適化がなければ、コストエンジニアリングは配分とブレーキ適用しかできず、構造的な改善はできません。本文では前者だけを扱いますが、後者の結論は繰り返し参照します。

1.1 請求書には七種類のお金が隠れている

ベンダーの請求書を分解して見ると、一見シンプルな LLM の請求書にも、性質のまったく異なる七種類の費用が含まれています。それぞれのコスト要因も、帰属のつけ方も、最適化の手法も違うのです。

  1. キャッシュにヒットしなかった入力 token。この部分は利用量に対して線形に伸び、コンテキストが長くなっても伸びます。最も制御しづらい部分です。
  2. プレフィックスキャッシュにヒットした入力 token。単価は通常、入力より明確に安くなります。コストは、安定したプレフィックスがどれだけ長いのか、TTL がどれだけ広いのか、リクエスト順序がどれだけ「集まっている」かによって変わります。
  3. 出力 token。これは「回答の長さ」と「モデルの自己回帰デコードコスト」の両方から影響を受けます。遅延への寄与は入力よりずっと大きいです。
  4. 推論・思考 token。思考チェーンを持つモデルは、ユーザーには見えないが課金対象になる token を大量に追加で生成します。従来の BI レポートでは、この部分が出力にまとめて扱われることが多く、「出力が高すぎる」というあらゆる分析が歪みます。
  5. ツールと外部サービスのコスト。検索、コード実行、DB クエリ、サードパーティ API。モデルベンダー由来ではありませんが、一つのタスクの実際のコストには同じように含まれます。ユニットエコノミクス(unit economics)がこれらを計算に入れないと、得られる毛利率は水増しになります。
  6. リトライとフォールバックのコスト。タイムアウト後の再試行、レート制限後のベンダー切り替え、構造化出力のパース失敗後の再生成。いずれも黙ってコストを倍にします。
  7. 生成以外の推論コスト。ベクトル化、リランキング、意図分類、ガードレールモデル、画像と音声。メインの対話モデルと同じ「推論予算」の概念を共有しますが、単価構造と最適化の手法はまったく異なります。

この七種類をひとつの合計にまとめると、まったく違うコスト要因をひとつのスカラーに押し潰してしまいます。コストエンジニアリングの第一の規律は、「AI コスト」という名前の数字を絶対にもたないことです。分項の累積だけを持ち続けます。ひとつの合計だけで管理しようとする手法は、最初のコスト異常で必ず機能しなくなります。ドリルダウンの経路がないからです。

1.2 コストの二つの出所:トラフィックと単価

総コスト = 単価 × 利用量。この書き方は意味のない一句に見えますが、コストエンジニアリングの中心フレームワークです。すべての最適化に道筋が二本しかないことを決めるからです。

  • 単価を下げる:より安いモデルへの交換、量子化、キャッシュ割引の獲得、処理の規模に合ったモデルサイズの選択。一度きりの、あるいは低頻度のエンジニアリング動作です。効果は予測でき、品質への影響も発生します。
  • 利用量を下げる:コンテキストの短縮、往復回数の削減、キャッシュヒット率の向上、オフラインで処理できる作業のバッチ化、必要に応じて小さいモデルへルーティングすること、大きいモデルへ差し戻した処理の再設計。これは継続的なプロダクト動作です。理論上は品質を損ないませんが、長期的な運用が要ります。

注目すべきは、利用量を下げるほうが、単価を下げるより価値が高く、副作用も小さいという点です。多くのチームはまずモデルの交換を考えます。しかし本当の問題は、一つのタスクが最初から必要とする以上の token を含んでいることにあります。検索結果が三つ重なって注入されていたり、履歴メッセージがトリミングされていなかったり、ツール定義が毎往復で再注入されていなかったり、不確かなタスクを何度も聞き直していたりです。これらは純粋な無駄であり、モデルを替えても解決しません。

1.3 「月末に請求書を見る」が必ず失敗する理由

月次レビューのモデルには、構造的な欠陥が三つあります。

  • 遅延性。コスト超過は第一週に生じています。気づかれるのは月末です。手を打てる余地はどこにもありません。
  • 帰属がない。総額の増加には、いくつかの原因があり得ます。大口顧客一社の利用拡大、段階的に公開した新しい機能、クローラのような流量の異常、新しく入った再帰的な呼び出し。これらはすべて、総額の中では同じ数字として現れます。
  • 分母がない。「今月は高い」という記述は絶対値です。必要なのは単位コストです。会話ごと、タスクの成功ごと、アクティブなテナントごとのコストです。そこまでを出して初めて、トレンドが「高くなった」のか「単に多く使っただけ」なのかを判断できます。

コストエンジニアリングの目標は、一言にまとめられます。どの時点でも二分以内に「先週、どの機能・どのテナント・どのモデルの単位コストが上がったのか。上がった理由は単価の変化か、利用量構造の変化か」に答えられることです。この応答速度に達しないなら、台帳の作り方がまだ間違っています。

まず台帳を正しく作る:コストイベントモデル

台帳はコストエンジニアリングの土台です。実装が失敗するのは、アルゴリズムが間違っているからではなく、いちばん下のレイヤーでモデリングの誤りを犯しているからです。「HTTP リクエスト一回」を「コストイベント一回」とみなしているからです。リトライやルーティングのフォールバック、ツールループがある構成では、この前提は必ず崩壊します。ユーザーの操作の一回の裏側には、課金対象の呼び出しが複数回あるからです。

2.1 実用最小限のコストイベント schema

コストイベント(cost event)が台帳の原子単位になります。定義はこうです。課金対象として可視であり、独立に帰属を割り当てられるモデル呼び出し一回。ユーザーの操作(trace)は複数のコストイベントから構成されます。セッション(session)は複数の trace から構成されます。この階層構造なら、「このやり取りにいくらかかったか」(trace 層)にも「このユーザーは今月いくらかかったか」(session 層)にも答えられます。

推奨する最小限のフィールドセットを次に示します。ポイントは三分割された token と、価格スナップショットです。

from dataclasses import dataclass, field
from decimal import Decimal
import time, hashlib

# 成本事件:一次"可归因的计费事实",而不是一次 HTTP 请求。
# 一次用户动作可能触发多次上游调用(重试、路由回退、工具循环),
# 所以账本粒度必须比请求更细,否则所有重试成本都会被静默吞掉。
@dataclass(frozen=True)
class CostEvent:
    event_id: str            # 幂等键:hash(trace_id, attempt, call_seq)
    trace_id: str            # 贯穿一次用户动作
    session_id: str
    tenant_id: str
    feature: str             # 功能维度:chat / summarize / embed / rerank ...
    route: str               # 逻辑路由名,如 "chat.default"
    model: str               # 实际服务的模型(可能被路由改写)
    provider: str

    # 三段式 token,比单一 total 更适合归因
    prompt_tokens: int
    cached_prompt_tokens: int   # 命中前缀缓存的输入部分
    output_tokens: int
    reasoning_tokens: int      # 思考型模型产生、用户看不到但照常计费的部分

    # 价格是"当次生效"的快照,而不是当前配置。
    # 供应商调价后,历史事件仍按当时价格计算,避免账目被追溯改写。
    price_snapshot: dict        # {"in": Decimal, "cached_in": ..., "out": ...}
    unit: str = "1k_token"

    attempt: int = 1            # 第几次尝试,>1 即为重试
    ts_ms: int = field(default_factory=lambda: int(time.time() * 1000))
    env: str = "prod"           # prod / staging / eval / replay
    labels: dict = field(default_factory=dict)

    @property
    def billable_input(self) -> int:
        # 未命中缓存的输入部分
        return max(self.prompt_tokens - self.cached_prompt_tokens, 0)

    @property
    def cost(self) -> Decimal:
        p = self.price_snapshot
        total = (
            Decimal(self.billable_input) * p["in"]
            + Decimal(self.cached_prompt_tokens) * p["cached_in"]
            + Decimal(self.output_tokens) * p["out"]
        )
        return total / Decimal(1000)

def make_event_id(trace_id: str, attempt: int, call_seq: int) -> str:
    """幂等键必须由业务侧稳定字段构成,不能用自增主键或时间戳,
    否则上游重投、消费端 at-least-once 都会造成重复计账。"""
    raw = f"{trace_id}|{attempt}|{call_seq}".encode()
    return hashlib.sha256(raw).hexdigest()[:24]

三つの設計判断を個別に説明しておきます。

第一に、token は一つの total ではなく三分割しなければなりません。ほとんどのモデル API が返す usage には、入力・キャッシュ入力・出力・推論 token が別々に記載されています。しかし多くのチームは保存時にこれを合計しています。その直接的な帰結は、キャッシュヒット率が算出できないこと、リトライによる増分が算出できないこと、推論コストが出力コストとして扱われることです。その後のすべての分析が、誤った土台の上に成り立ちます。

第二に、価格は参照ではなくスナップショットでなければなりません。イベントに model="xxx" しか保存していない場合、ベンダーが価格を変更した瞬間に、過去のコストはすべて再計算されます。昨日の比較も、先月との比較も、そのままでは意味を失います。スナップショットを保存するのはごく小さな追加コストですが、台帳の不変性と監査可能性を保証します。経理との突合では必須条件です。

第三に、attempt フィールドは第一級の市民でなければなりません。リトライ率はコスト健全性の中心指標の一つです。正常な状態では、リトライ比率がごく小さいはずです。あるモデルのある時間帯でリトライ率が上がったなら、通常は上流の品質が低下した、自分のタイムアウト設定が厳しすぎる、あるいは特定種類の不正リクエストが繰り返し発火しているということです。ログの中に隠れていてはいけません。最初にクエリできるフィールドであるべきです。

2.2 ディメンション設計:機能、テナント、モデル、チャネル

ディメンション設計の原則は、どのディメンションにも実在する意思決定が対応することです。対応する行動のないディメンションは足さないでください。ディメンションが増えすぎると、集約クエリは高くなり、誰も使いません。

  • feature(機能):最も重要で、同時に最も設計を間違えやすいディメンションです。コードモジュール名ではなく、プロダクトナビゲーションやユーザーの目に触れる機能単位に対応する必要があります。よくある誤りはサービス名で分類することです。「chat-service」の中に無料のお試しも有料の深掘り分析もあり、すべてが混ざっています。この設計だと「どの機能が赤字か」には決して答えられません。まさにそれが、プロダクトマネージャーが唯一気にかけている問いだからです。
  • tenant_id(テナント):ユニットエコノミクス(unit economics)の唯一の入口です。テナントのディメンションがなければ、総収入から総コストを引いた平均値しか算出できません。意思決定の価値がない数字です。テナントのディメンションも実際の認証コンテキストから取る必要があります。リクエストボディ内の、偽装できてしまうフィールドに依存してはいけません。
  • model / route(モデルとルート):この二つは区別してください。route は設定した論理ルート名、model は実際に選ばれたモデルです。分けて保存する理由はこうです。「複雑なタスク」を強いモデルへルーティングしたとき、ルート次元のコスト変化は事業構造の変化を示し、モデル次元の変化は価格や挙動の変化を示します。混ぜてしまうと、「今回の価格変更が誰に影響したか」すら調べられません。
  • provider(チャネル):複数ベンダーの環境で必須です。切り替えの利益と、障害期間中のコスト異常を評価するために使います。
  • env(環境):多くのチームで見落とされる項目です。見落とすと、評価トラフィック、オフライン再生、内部の dogfooding がすべて本番コストに混ざります。env ラベルを付けたコストレポートなら、「本番コスト」と「ベンダー請求金額」の差を一〜二つのクエリで説明できるはずです。このラベルがなければ、その差は永遠に説明できないブラックボックスになります。

2.3 課金精度と冪等性

台帳とベンダー請求書の間には必ず差額があります。どう扱うかは用途によって異なります。

  • 社外向けの経理基準:ベンダー請求書を基準とし、台帳は配分にだけ使います。台帳は「総量」の次元で請求書にできるだけ近づける必要があります。そうでなければ配分係数が誤り、配分結果もすべて誤ります。
  • 社内向けの運用基準:ディメンションを持つ台帳を基準とします。差額は、明示的な「未回収残高」科目で受け持ちます。各機能へ押し付けて、存在しないふりをしてはいけません。

実装では二つのエンジニアリング規律を守る必要があります。第一に、イベント書き込みは冪等でなければなりません。リトライの中でモデル呼び出しが重複報告されることがあり、メッセージキューは通常 at-least-once 配信になります。浮動小数点の累加は、長時間動かすとドリフトします。現実的な方法は、業務の冪等キーと正確な小数(Decimal)、あるいは整数化した最小の課金単位で累加することです。毎回過去の総和を計算し直すのではありません。第二に、台帳には「後付け計上」の経路が必要です。呼び出しが起きた時点ではまだわからないコストがあります。ストリーミング生成が途中でキャンセルされた、バッチ処理が後から集計された、非同期ツールコールバックが後続の呼び出しを生んだ。この種のイベントは、遅延計上と後付け計上時刻の記録に対応していなければなりません。さもないと、月末にコスト曲線が唐急に跳ね上がります。

内訳の切り方:同じ支出を三つの視点で読む

台帳が正しくできましたあとは、同じ支出を少なくとも三つの切り方で読む必要があります。切り方を混同するのは、コストをめぐるやり取りで最も頻度の高い間違いの源です。プロダクトは「うちの機能は安い」と言います。経理は「コストが上がった」と言います。どちらも間違ってはいません。ただ、異なる切り面を見ているだけです。

3.1 リクエスト単位の真実値:一回の呼び出しが実際にいくら使ったか

いちばん下層の、いちばん間違いにくい切り方です。ベンダーから返ってきた usage をそのまま使います。その役割は校正です。上位のすべての切り方は、ここへ収束できるはずです。ある機能のディメンションで合計したコストが、リクエスト単位の真実値と合致しないなら、イベントの欠落か重複があります。まずデータを直してから、解析の話をするべきです。

3.2 機能レベルの按分:trace を機能に割り当てる

ユーザーの操作は、純粋な生成だけとは限りません。意図判定(小さいモデルの呼び出し一回)、ベクトル検索(embedding のバッチ一回)、リランキング(rerank の呼び出し一回)、メインの生成(大きいモデルの呼び出し一回)、そして出力後のガードレールチェックを誘発しているかもしれません。機能レベルの按分が答えるべき問いはこうです。「ユーザーが「週報を生成する」というボタンを押した。このクリックの総コストはいくらか。中間ステップを含めて」というものです。

按分には二つのルートがあります。

  • 直接集約:各サブ呼び出しが、呼び出しの時点から最終的な機能タグを持ちます。最も正確ですが、呼び出しチェーンがコンテキストタグを確実に引き継げる必要があります。実装ではタグをリクエストヘッダやコンテキストオブジェクトに入れ、ミドルウェアに落とされないようにします。
  • 事後配分:trace で全イベントを集約し、ある重みで配分します。実装は簡単ですが、配分ルールを明示しておらずならず、そのルール自体が誤差の源になります。

実践上の推奨はこうです。メインのパスは直接集約、フォールバックは事後配分,かつ「フォールバック配分の割合」そのものを監視指標にします。この割合が上がり続けるなら、どこかの階段でタグの引き継ぎが切れています。多くの場合、新しく足したミドルウェアか非同期処理がタグを置き忘れているのです。

3.3 テナント単位と請求期間の整合

テナント単位の集計の正しさには、見落とされがちな前提があります。集計ウィンドウには、まだ閉じていないセッションを含める必要があるのです。ストリーミング出力は、ユーザーがページを閉じた後も、サーバー側で最後の数トークンを走り続けることがあります。非同期処理、一括バックフィル、webhook が起動する後続の呼び出しは、数時間、あるいは数日後にコストを生むことがあります。暦月で機械的に切ると、月次のテナント粗利には系統的な時間のずれが生じます。

対処は二つあります。一つは「未精算セッション」科目を導入し、ウィンドウ末尾で未完了のセッションのコストを別枠に留めて、次の期間に決算することです。もう一つは、請求期間をリクエストの開始時刻ではなく、セッションの終了時刻と定義することです。長いセッションにはこちらのほうが公平です。社外向けの経理報告では前者が多く、社内分析では後者のほうが妥当です。重要なのは、同じレポートの中で二つの切り方を混ぜないことです。

一回のセッションコスト:切り方、ライフサイクル、計算式

「一回のセッションはいくらか」という問いは、もっとも訊かれ、同時に最も答え間違えやすい問いです。答え間違える理由はほぼ常に、切り方を決めていないことに帰着します。誰が計算するのか、どこまで計算するのか、キャッシュを含めるのか、推論 token を含めるのか。

4.1 セッションはリクエスト一回ではない

一回のセッションの実際のコストは、少なくとも五つの部分からなります。

  • 今回の対話の全往復におけるすべてのモデル呼び出し(リトライされたもの、フォールバックされたものを含む)。
  • セッション内でトリガーされたすべての検索、embedding、rerank、意図識別などの補助呼び出し。
  • ツール呼び出し自体の外部コスト(検索 API、データベース、コードサンドボックス)。
  • ツールが戻ったあとにトリガーされる「生成継続」の呼び出し。この項目は、trace が元のリクエストと同じ連鎖の上にないため、まとめて落とされがちです。
  • セッションが副作用を生む場合(ベクトル DB への書き込み、長い文書の生成と保存)、token 以外のストレージとインデックスのコストも加わります。

「メインの対話の何回か」だけを数えるのは、もっともよくある切り方の誤りです。ツールを含む複雑なセッションを系統的に低く見積もらせ、プロダクトの設計で誤った判断をさせます。実際には Agent のシーンと普通の対話ではコストがまったく違い、ツールループがコストを倍、あるいは桁違いに押し上げます。

4.2 有効 token と課金 token

二つの指標を同時に持つようにしてください。それぞれの用途はまったく違います。

  • 課金 token:実際に発生して課金された量。突合と支出の制御に使います。リトライも、思考 token も、破棄された中間出力も含まれます。
  • 有効 token:実際にユーザーの成果として寄与した量。通常は、最終的に見える出力と、モデルが実際に消費したキャッシュされていない入力を指します。リトライによる増分、思考 token、バグによる無効な生成は含みません。

この二つの比(増幅係数)自体が、もっとも価値のある健全性の指標です。課金 token / 有効 token が長期に偏高なら、システムは無駄に生成しています。思考 token の比率が高すぎるなら、そのタスクはそこまで深い推論を必要としないのかもしれません。リトライ token の比率が上がっているなら、それはコスト戦略ではなく安定性の問題なので、工程側の介入が必要です。「増幅係数」を監視パネルに入れておくのは、総コストをじっと見るよりずっと役に立ちます。異常に対して敏感だからです。総コストの変化には売上規模という巨大なノイズが乗りますが、増幅係数は正規化後のシグナルですから。

4.3 セッションのステートマシンと後付け計上

セッションをステートマシンとしてモデル化することが、一回のセッションコストを正しく計算する前提です。典型的な状態は次のとおりです。

会话状态机(简化)——每个状态迁移都是一个可能产生成本事件的点

CREATED
  └─> CLASSIFYING      意图/路由判断(小模型,可能多次)
        └─> RETRIEVING   embedding + 向量库查询 + (可选)rerank
              └─> GENERATING   主模型流式生成
                    ├─> TOOL_CALL     模型请求调用工具 → 暂停
                    │     └─> TOOL_RESULT  工具返回 → 回到 GENERATING
                    │           (每次回路都是一次新的 GENERATING,成本叠加)
                    ├─> GUARDRAIL     输出后检查
                    └─> COMPLETED

异常分支:
  GENERATING --timeout--> RETRYING --> GENERATING      (attempt+1,成本叠加)
  GENERATING --client abort--> CANCELLED                 (已生成的 token 仍计费)
  GENERATING --overload--> FALLBACK_MODEL --> GENERATING(不同模型,价格快照不同)

ステートマシンがもたらす直接のエンジニアリング上の結論はこうです。コストイベントの書き込み地点は、HTTP handler ではなく、状態遷移に紐付けるべきだということです。状態遷移に紐付けなければ、ツールループ、タイムアウト後のリトライ、クライアントの中断という三種類のコストが漏れてしまいます。また、各セッションには明示的な cost_settled フラグが必要です。このフラグを持たないセッションは「未精算」であり、レポートの中で別に見えるようにしなければなりません。

ユニットエコノミクス:客単価、推論コスト、粗利

これまでの台帳が「 コストどこに使ったか」に答えるものだとすれば、ユニットエコノミクス(unit economics)が答えるのはもっと鋭い問いです。このプロダクトは儲かっているのか。誰の金で儲かっているのか。コストエンジニアリングが技術的な役割から事業的な役割へ昇格する地点であり、もっとも間違えやすい部分です。

5.1 総額の思考から分布の思考へ

もっともよくある誤りはこうです。月収から月コストを引いて粗利率を求め、それを経営会議に持っていく。この数字の問題は、利用者の分布に完全に左右されることにあります。収入が一様に分布し、コストがべき分布であれば(少数のヘビーユーザーが token の大部分を消費する)、総額の粗利率は、二つのまったく異なる事業形態に同じ答えを出してしまいます。一つは健全なサブスクリプション、もう一つは大口の顧客数件に曳きずられかけている周辺事業です。

正しい方法は、少なくとも利用者を何かの分位点で層に分け、層ごとに単位コストを計算することです。よく使う層の切り分けは、利用額(軽・中・重)、プランの階層、そして機能の利用プロフィール(ツールを使っているか、長いコンテキストか、バッチインターフェースを使っているか)です。各層は「この層の粗利はいくらか」に独立に答えられる必要があります。

ここで統計上の罠を一つだけ覚えておいてください。総収入を総 token で割って「典型的な利用者」のコストを表してはいけません。総 token の平均値は、長い裾の利用者によって強く引き上げられます。しかし典型的な利用者のコストは、中央値か分位点で捉えるべきです。コストエンジニアリングで「平均的な利用者」に言及する箇所は、それが算術平均なのか分位点なのかを明示しなければなりません。この二者はべき分布のもとで桁違いに違うことがあります。

5.2 貢献粗利と層別の明細

推奨されるコスト構造のモデルは三層で、外側から内側へ順に狭めていきます。

  • 第一層:収入 − 直接推論コスト = 推論粗利。いちばん粗い層で、すべてのテナントのすべての機能のすべてのコストを含みます。意味は総量の制約を提供することであり、意思決定には向きません。
  • 第二層:テナントで層分けしたあとの推論粗利。中心指標は「テナントごとの貢献粗利」、つまりそのテナントの収入から、それに帰属するすべての推論コスト(検索、リランキング、ガードレール、ツールを含む)を引いた値です。この層は価格設定とプラン設計の基礎になります。
  • 第三層:機能・シーンで層分けしたあとのタスク粗利。中心指標は「成功したタスク一つのコスト」です。この層はプロダクトデザインの入力になります。「どの機能に投資を続け、どの機能を絞り、どの機能を落として作り直すべきか」を教えてくれます。

三層のあいだは自己整合していなければなりません。第二層の各テナントの和は第一層に等しく、第三層の各機能の和は第二層に等しくなければなりません。合わない場合、差額は「未帰属」科目として明示的に露出させ、割合で均等に丸める方法は使わないでください。丸めてしまうと、下流のすべての分析が信頼できない基数の上に成り立ち、この種の誤りは数ヶ月後になってようやく発見されます。

5.3 混ぜてはいけない三つの比率

非技術者に報告するとき、よく混同される三つの比率があります。混同するとまったく逆の結論が得られてしまい、どの図にもはっきりと注記しなければなりません。

  • アクティブユーザー一人あたりのコスト:分母はアクティブユーザー数で、留存の変動から大きく影響を受けます。ユーザーが増える局面なら自然に下がり、「最適化が効いた」と誤読されやすいのです。
  • セッションあたりのコスト:分母はセッション数で、プロダクトのインタラクションデザインから大きく影響を受けます。一回で終わるタスクを三回に分けると、セッションあたりのコストは大きく下がりますが、タスクあたりのコストは上がる可能性があります。
  • 成功したタスクあたりのコスト:分母は「成功した」タスク数です。これは実際の経済性にもっとも近い指標で、失敗してリトライしたコストも分子に押し込めるからです。ただし、明文化された成功の判定基準が要ります。「成功」を定義すること自体が、技術ではなくプロダクトの問題だからです。

実践上の推奨はこうです。社内の日次ダッシュボードには成功したタスクあたりのコストを、経営報告には貢献粗利の絶対値とトレンドを、分母の切り方も明示して使います。どちらも要ります。ただし、混ぜて話すのはやめましょう。

5.4 コストフロア:価格がカバーすべき最悪のケース

成功したタスクあたりのコストが分かれば、価格設定には明確な制約が生まれます。鍵となる判断は、ある機能の価格は、そのコストの中央値に合わせるのか、コストの分位点に合わせるのかです。答えは分位点、より具体的に言えば高いほうの分位点です。理由は、コスト分布の長い裾が異常ではなく、プロダクトの形そのものだからです。長いコンテキストのタスク、複雑な多往復のタスク、ツールを有効にしたタスクは、もともとその機能の価値が最も高くなる形態です。それらの高コストには一部、価値の創出そのものも含まれており、押さえつけることはできません。

ここから出る帰結はこうです。コスト最適化は分布の本体を対象にするべきで、長尾を対象にするべきではありません。p50 のコストが高いなら、ほとんどの利用者が非効率に消費されているということです。構造、ルーティング戦略、往復の設計に系統的な問題があり、優先して直すべきです。p50 は適正でも p95 が高いなら、特定種類の使い方が予算を飲み込んでいるということです。そのときにもっとも適した手は、プロダクト側の層別化です。重負荷の使い方を、独立した高(「高すぎる」専用のプランのプラン、ないしは専用機能にします。平均値の最適化を続けるのではありません。この二種類の診断に対応する行動はまったく違い、混同すると最適化の方向が完全に狂います。

もう一つ、明示的に扱うべき問題が無料トラフィックのコストフロアです。無料トライアル、公開デモ、内部の dogfooding はいずれも推論リソースを消費し、そのコストは最終的に課金ユーザーが負担します。対策は三つあります。無料枠に硬い上限を設ける、機能ごとに無料と有料のコスト切り分けを分ける、あるいはそれを獲客コストの一部として CAC に算入することをそのまま受け入れる。受け入れることも、何の科目にも算入しないことも誤りです。前者なら粗利が水増しになり、後者なら無料戦略に関する判断材料が失われます。

コスト削減の手法と、効果の定量化フレームワーク

コスト削減の手法は多くあります。しかしほとんどのチームが、評価するときに同じ誤りを犯します。コストの変化だけを計算し、品質の変化を計算していない。token を節約したかわりにタスクの成功率が九割から七割に落ちたら、実際の単位コストは逆に上がる可能性があります。統一した評価フレームワークが必要です。まずフレームワークを立ててから、具体的な手法を一つずつ当てはめていきます。

6.1 汎用フレームワーク:コストと品質のトレードオフ

どのコスト削減手法を評価する場合も、中心となる指標は「コストを何パーセント削れたか」ではありません。成功したタスク一つあたりのコストの変化です。

from dataclasses import dataclass

@dataclass
class ArmResult:
    arm: str               # 方案名,如 "baseline" / "gpt-mini-routed" / "cached-v2"
    n: int                 # 评测样本数
    cost_total: float      # 方案总成本(绝对值,评测集固定时才可跨方案比)
    success: float         # 端到端任务成功率,不是 BLEU、不是人工主观分
    p95_latency: float
    escalation: float      # 转人工 / 二次求助 / 任务放弃率

def unit_cost_per_success(r: ArmResult) -> float:
    """单位成功任务成本 = 每次尝试成本 / 成功率。
    这是唯一可以跨方案、跨模型、跨缓存策略比较的指标。"""
    per_attempt = r.cost_total / max(r.n, 1)
    return per_attempt / max(r.success, 1e-6)

def marginal_change(base: ArmResult, cand: ArmResult) -> float:
    """返回相对基线的单位成本变化率,负数表示变便宜。"""
    b = unit_cost_per_success(base)
    c = unit_cost_per_success(cand)
    return (c - b) / b

def pareto_front(arms: list[ArmResult]) -> list[ArmResult]:
    """在"单位成本 - 成功率"平面上做帕累托筛选。
    一个方案只有同时更便宜且不掉成功率,才值得上线;
    只更便宜会掉成功率的方案,应当以质量损失为代价单独做产品决策。"""
    pts = sorted(arms, key=unit_cost_per_success)
    front, best = [], None
    for a in pts:
        if best is None or a.success > best:
            front.append(a)
            best = a.success
    return front

このフレームワークの使い方で重要な点が三つあります。

  • 評価セットは固定したうえで層分けする。全体の成功率は構造の変化を覆い隠します。ある方式が簡単なタスクでは成功率が大きく上がり、難しいタスクでは大きく下がったのに、正味では変化着陆していないことがあります。層分けされた評価でなければ、「あるタスク集合と別のタスク集合を交換した」だけの疑似最適化は見抜けません。
  • 最も難しい尾のサンプルを含める必要があります。評価セットがすべて中程度の難易度だと、ある削減手法が極端な入力(超長い文書、複雑な多段推論)で破綻することを知る機会がありません。恰恰、そこがコストが最も制御しづらくなる場所です。
  • 遅延をただの代償にしてはいけません。バッチ化、深い思考、より長いプロンプト圧縮の前処理は、いずれも遅延とコストを交換します。バッチ処理はインタラクティブな場面の遅延を秒単位から時間単位へ押し上げます。多くのプロダクトでは、これは受け入れられません。p95_latency と escalation は、コストと一緒に意思決定表に入れなければなりません。

6.2 プロンプトキャッシュ:まずヒット率の切り方を正してから、効果を語る

プレフィックスキャッシュは効果が最も直接的な手法の一つですが、同時に最も誤用されやすい手法でもあります。多くのチームは、請求書の「キャッシュ入力」の欄に金額が入るのを見て、キャッシュが効いている信じます。実際には、ほとんどのチームの本当の問題はキャッシュの単価が高いことではなく、ヒット率が低いことです。そしてヒット率が低い理由は、通常、設定ではなくシステム設計にあります。

このことをはっきりさせるには、まずヒット率の定義を固定しなければなりません。

# 命中率的三种口径,必须在报表里注明用的是哪一种
hit_token_rate = cached_prompt_tokens / prompt_tokens
#   "token 口径":被缓存命中的输入 token 占比。适合衡量提示工程效果。

hit_call_rate = count(events with cached_prompt_tokens > 0) / count(events)
#   "调用口径":有多少次调用吃到了任何缓存。适合衡量系统设计的聚集性。

hit_prefix_len = min(P, ...)   # 稳定前缀长度(token 数)
#   "前缀口径":真正被复用的稳定前缀有多长。
#   这个口径最接近收益来源:只有稳定前缀才产生可观的复用率。

# 三者的关系:
#  token 口径 ≈ 调用口径 × 单次命中的平均覆盖比例
# 所以"调用口径很高但 token 口径很低"= 缓存系统在工作,
# 但你的提示里绝大部分内容仍然在缓存边界之外被重算。

ヒット率が低い主な原因を、実際の発生頻度の高い順に並べると次のとおりです。

  • 安定したプレフィックスが壊れている。システムプロンプトの前に、タイムスタンプ、リクエスト ID、乱数、ニックネームなど、毎回変わる内容を挿入しています。キャッシュは「まったく同じプレフィックス」でしかヒットしません。1バイトでも違えば、その位置まで失効します。正しい方法は、すべての動的内容をプレフィックスの後ろに置くことです。そしてこのルールを lint チェックやテンプレート層の強制制約にします。
  • リクエスト順序が集まらない。キャッシュには容量と時間の窓があり、異なるテナント、異なる機能のリクエストが互いに押し出します。トラフィックが長く均一な裾なら、ヒット率は本質的に低くなります。このとき「テナント別・機能別のプール分け」は、あらゆるキャッシュのパラメータ調整よりも明らかに有効です。
  • 多ターンの会話がプレフィックスを延ばし続ける。複数ターンの場面では履歴メッセージが変化し続けるため、毎ターンのプレフィックスが前のターンと違ってしまいます。対処はキャッシュアンカーの分割です。変化しない長期部分(システムプロンプト、ツール定義、長期記憶)を独立したプレフィックスとして固定し、セッション内の短期履歴をその後ろに置きます。そうすれば長期部分は常にヒット可能な状態を保てます。
  • TTL とセッションのリズムが合っていない。キャッシュの生存時間が、同じプレフィックスを持つ二回のリクエストの間隔より短いなら、ヒット率はゼロに近づきます。これは実際のリクエスト間隔の分布から TTL の必要値を逆算して求めなければなりません。既定値をそのまま写すのではありません。

境界を明示しておく必要があります。キャッシュが最適化するのは「重複した入力」であって、「余計な入力」ではありません。もしあるタスクが三万 token にわたる重複資料を注入しているなら、正しい方法は先に不要な内容を消すことです。消した内容はキャッシュのコストも発生しませんし、ヒットによる収益も得られません。キャッシュは正しいプロンプト構造の上に載せる最適化であり、その代替手段ではありません。

さらにキャッシュ効果の帰属の捏造には注意が必要です。よくある誤りはこうです。キャッシュ機能を入れたあと、請求金が下がった分を全部キャッシュの功績として記録すること。これは三つの場合で効果が明らかに過大評価されます。同時期にプロンプトの簡略化もしていた(入力 token 自体が減っていた)。同時期にトラフィック構造が変わった(短いリクエストの比率が下がると、コストも下がる)。そして同時期にバッチインターフェースへ切り替えた(バッチ自体に独立した割引がある)。正しい帰属の方法は統制された比較を一度行うことです。評価セットを固定し、同じ時間窓で、キャッシュの on/off だけを切り替えて、同じタスク集合の「成功したタスクあたりのコスト」を比較します。この対照がなければ、「キャッシュがいくらか節約したか」という数字は宣伝文句にしかなりません。意思決定には入れられません。

最後に見落とされがちな点を挙げます。キャッシュの容量と並行性の間にはトレードオフが存在する。キャッシュが消費する GPU メモリやホストメモリは、推論リソースと競合します。GPU メモリが足りない場面では、キャッシュを大きくすると並行度が直接下がり、単位コストが押し上がる可能性があります。したがってキャッシュのパラメータ調整は、「ヒット率」と「スループット」の両方の指標で同時に観察しなければなりません。ヒット率だけを見ると、全体コストに有害な判断を下す可能性があります。

6.3 プロンプト圧縮:効果はタスク成功率で補正しなければならない

プロンプト圧縮(履歴の要約、構造化状態の抽出、検索結果のトリミング、冗長な例の削除)は、純粋な効果に見えます。入力 token が減れば、コストは自然に下がります。しかし隠れた代償があります。圧縮された情報が、まさにタスクが必要としていたものかもしれません。しかもこの代償が表れるまでには遅れがあります。圧縮後の一週間は問題なく見え、ある時点で、長い連鎖の情報を必要とするエッジケースがエラーを出し始めます。そしてそのケースが指標の上で見えるのは、成功率が緩やかに下がっただけなので、ほかの理由のせいにされがちです。

したがってプロンプト圧縮の検証規律はこうです。

  • 圧縮率は単独で報告し、コスト節減と混ぜないこと。入力 token の減少割合が圧縮率です。コストの節減はキャッシュのヒット状況にも左右されます。圧縮されたのがちょうど安定したプレフィックスなら、圧縮はむしろヒット収益を下げる可能性があります。
  • 成功率の回帰試験は必ず実施し、遅延して観察すること。圧縮の効果は、圧縮が有効になったあとのセッションで評価しなければなりません。圧縮前の履歴データと比べることはできません。評価ウィンドウは、少なくとも圧縮戦略の典型的な失効周期を覆う必要があります。
  • 「情報保全型」のタスクには保守的に。タスク自体が複数ターンの細部をすべて想起する必要があるなら、要約は誤りです。やるべきは構造化ストレージと必要に応じた検索であり、要約ではありません。圧縮の正しい形は「情報の取得方法を変える」ことで、「情報量を減らす」ことではありません。

6.4 量子化:精度の損失はどう業務指標に伝播するか

量子化(重みと活性化の数値精度を下げること)は、「単価を下げる」道筋に属します。その経済学は明快です。より小さいビット幅は、より少ないメモリ帯域幅と高い実効スループットを意味し、同じハードウェアの上でより多くの並行性を捌けるようになります。コストへの伝播経路はこうです。量子化 → 一 token 当たりの限界コストが下がる → 遅延目標のもとで最大並行性が上がる → 単位コストが下がる。

ただし、精度の評価は正しい経路に沿って行わなければ、結論は信頼できません。よくある誤りは、困惑度やベンチマーク順位の点数変化を直接見て、業務効果へ外挿することです。困惑度は平均的な意味での言語モデリング指標であり、業務が最も気にかけている能力(長い指示への追従、構造化出力の妥当性、指示制約の遵守、ツール呼び出しのパラメータ正答率)には敏感ではないのです。

正しい評価の向きは、タスクレベルの能力指標の集合であり、難易度の層ごとの差を見なければなりません。よくあるパターンはこうです。簡単なタスクの指標はほとんど動かないが、難しいタスクの指標は明らかに下がり、全体平均ではごく小さな変化にしか見えない。つまり量子化の効果は「長尾能力の明らかな退化の代わりに、限界コストを線形に下げる」ものである可能性が高い、ということです。やる価値があるかは、完全にプロダクトの意思決定です。正しい意思決定の入力は、層別のデータと層別の収入分布であり、全体の平均値ではありません。

6.5 小さいモデルへの振り分け:ルーターの誤判定の代償は非対称になる

モデルルーティング(簡単なタスクは小さいモデル、複雑なタスクは大きいモデル)は、構造的なコスト削減手法です。下げているのは「すべてのタスクの単価」ではなく「平均単価」だからです。その経済性は、きわめて明快なフレームワークで説明できます。

# 模型路由的经济学(用变量描述口径,不依赖任何具体价格)
#
# 设:
#   p        = 判定"任务简单"的概率
#   q        = 判定为简单、且小模型确实能答对的概率(判定的精度)
#   r        = 大模型单独完成该任务的正确率
#   s        = 小模型单独完成该任务的正确率(s < r)
#   C_small, C_large = 两种模型的单次尝试成本
#   C_route  = 路由器判定本身的成本(可用一个极小模型,通常接近 0)
#
# 期望成本:
#   E[cost] = C_route + p * (q * C_small + (1 - q) * C_large)
#                    + (1 - p) * C_large
#
# 期望正确率:
#   E[acc]  = p * (q * s + (1 - q) * r)   # 判错时回退大模型,acc 接近 r
#            + (1 - p) * r
#
# 关键结论 1:误判代价是不对称的。
#   简单任务被判为复杂 -> 多花钱,但结果正确(只是浪费)
#   复杂任务被判为简单 -> 省钱,但结果错误(业务损失 + 可能引发重试,
#                                    重试会再次调用大模型,总成本可能反而更高)
#   所以路由器的评估指标应该是"漏判率"(把复杂判成简单的比例),
#   而不是"总体分类准确率"。
#
# 关键结论 2:当 q 很低时,路由几乎不省成本。
#   因为每次判错都要再走一遍大模型,等于做了两次大模型调用。
#   评估路由方案时,漏判率必须换算成"期望额外调用次数"来看。

このフレームワークから、いくつかの実践上の判断を導けます。

  • ルーターの見落とし率は硬い指標です。単独で監視し、アラームを設定してください。システム内での位置づけは、最適化コンポーネントというより信頼性コンポーネントに近いものです。
  • 二択ではなくカスケードとして設計できます:小さいモデルが先に回答し、ルールと信頼度で通すかどうかを判定し、確信が持てないときは大きいモデルへ戻す。これなら問題は「分類の正答率」から「信頼度の校正」に変わります。たいてい後者のほうが正しく作りやすく、閾値を調整してコストと品質を交換するのも容易です。
  • 保守的な閾値が合理的な出発点です。コストの圧力が生じる前は、ルーティングの閾値は保守的に(分流を少なく)しておくべきです。誤判定による業務損失を計算に入れると、回収の閾値は直感よりはるかに高くなります。台帳が数週間分の実データを溜め、見落とし率が制御できる状態になってから、少しずつ緩めていきます。

6.6 バッチ処理と非同期化:遅延とコストを交換する前提条件

バッチ用インターフェースの中心的な価値は、「低遅延の単発リクエスト」を「高遅延のバッチリクエスト」に変え、より安い単位コストと交換することです。その利得のソースはこうです。バッチ内でリクエストが重みと KV スペースを共有し、スケジューリングのオーバーヘッドが減り、計算ユニットをより高密度に埋められることです。この種の最適化はモデルの挙動をまったく変えません。だから品質はニュートラルです。他のどの手法にもない、明快な利点です。

適用境界をはっきりさせておく必要があります。

  • ユーザーが待たない場面にしか使えません:オフライン要約、一括分類、評価の再生、夜間のインデックス作成、レポート生成。インタラクティブな対話、リアルタイムのカスタマーサポート、ユーザーが画面を見て結果を待つ場面には使えません。
  • キューとバックプレッシャの設計が必要です。バッチ用インタフェースの割り当ては通常、オンラインの割り当てとは独立しています。オンラインのトラフィックをバッチ用キューへ切り替えるなら、割り当てと遅延の約束の変化に注意してください。キューには明確な優先順位、滞留アラーム、超時廃棄の戦略がなければ、「一つの滞留タスクがバッチ全体を巻き込む」という事態が起こります。
  • 効果が本当に得られるかは、バッチウィンドウとタスク到着率のマッチングで決まります。トラフィックが疎なら、バッチが埋まらないまま効率の悪い単発リクエストに退化します。したがって正しい順序はこうです。まずトラフィックの特性(周期的に集中しているか)を見、それからバッチ処理を導入するかどうかを決める。「バッチのほうが安い」と見てすぐ切り替えるのではありません。

6.7 往復回数の削減:もっとも過小評価されているコスト削減

これまでの節で語ってきたのは「一回の呼び出しを安くする」ことでした。しかし最大のレバーは多くの場合、呼び出しの回数にあります。五往復を要していたユーザータスクを、二往復で終えられれば、コストは半分以下になるだけでなく、ユーザー満足度も通常は上がります(入力する文字が減り、待つ時間が減る)。

実行できる方向としては、これがあります。情報を逐次確認するのではなく、一度に集める。履歴セッションからユーザーの意図を推論して、毎回確認しなおさない。自由形式の問いかけの代わりに、構造化されたフォームや選択肢を使う。「確認してから実行」を「取り消せる実行を提示する」に変える。そしてシステムプロンプトの中で、「答える前に、必要な情報が揃っているかを確認せよ」と明確に制約する。

このレバーを測る指標はタスクあたりの往復回数の分布であり、平均値ではありません。同様に、往復回数のべき乗的な性質は明確で、大部分のタスクは一两回で終わり、少数が長いループに陥ります。平均値だけを見ると、「ほとんどのタスクが口数が多すぎる」と「少数のタスクが回っている」という、二つの正反対の問題を同時に覆い隠してしまいます。分位数を見るべきです。そして長いループの回数と終了条件は、単独で監視します。

予算、レート制限、アラート

台帳とユニットエコノミクスが「見えるもの」だとすれば、予算とアラートは「止められるもの」です。この層の目標は、毎リクエストの支出を精密に制御することではありません。制御が効かなくなるその前に、制御可能な方法でそれを断ち切ることです。

7.1 予算は配分の問題であり、総額の問題ではない

AI ビジネスに「月間のコスト上限」を設けることは、ほとんど意味がありません。誰かが、合計が上限に近づいたという理由だけで正しい決断をするわけではないからです。予算は配分のメカニズムです。よく使う配分の軸は三つです。

  • テナント単位の配分:各テナントに独立した上限、あるいはソフトな上限を設けます。使用量ベースの課金の場面に最適です。また、単一のテナントが全体を壊すことを防げる唯一の方法でもあります。ハードな上限は、顧客に事前告知なしでサービスを中断させてしまい、体験がとても悪くなります。より良い方法は段階的な戦略です。ソフトな閾値に達したら劣化させる(より安いモデルに切り替える、コンテキスト長を縮める、非コアのツールを閉じる)。ハードな閾値に達してから初めてレート制限をかけ、プロダクト側で明示的に伝えます。
  • 機能単位の配分:各機能に独自の予算と、単位コストの上限を設けます。機能どうしのコスト差が非常に大きいプロダクトに適しています。 たとえば、無料のトライアルの対話と、有料の深いレポートが共存しているようなものです。
  • モデル・チャネル単位の配分:あるルートが異常なときに、予算をすべて食いつくすことを防ぎます。この項目はよく見落とされますが、恰恰そこが突発事故の起きやすい場所です。

三つの配分は重ねることができます。実践では「チャネル → 機能 → テナント」の順に段階的に絞り、粒度を粗いものから細かいものへと変えていきます。これなら、どの層が発動しても、誰がなぜそうしているか分かります。

7.2 レート制限の五つの軸

RPM の制限だけでは足りません。最低限、次をカバーする必要があります。

  • 速度:毎分のリクエスト数、毎分の token 数。入力と出力は別々に設定します。コスト構造が違うためです。
  • 並行性:同時に走っているセッションの数。並行性は遅延とコストを決める結合変数です。速度を抑えるより並行性を抑えるほうが、遅延の約束を守るのに有効です。
  • 周期的な割り当て:テナントごとの日次・月次の token 上限。月次のコストを制御する主要な糸口です。
  • 単発の上限:単一リクエストの入力長の上限、単一セッションの累積 token 上限、単一セッションの最大往復回数。この三つの上限は「一点爆発」を防ぐ鍵であり、プロンプトインジェクションによる資源枯渇攻撃を防ぐための必須の防御線でもあります。
  • 優先順位:全体の予算が窮したとき、どのトラフィックを残すか。それにはコストシステムとスケジューリングシステムが連携している必要があります。それぞれが自分の粹だけを決めていてはいけません。

レート制限が掛かった後の劣化チェーンの設計は、レート制限そのものより重要です。理想的な劣化の順序はこうです。キャッシュヒット率の高い簡略化したプロンプトに切り替える → より小さいモデルに切り替える → 検索結果の数を減らす → 非コアのツールを閉じる → 失敗させるのではなく待たせる。どの段階の劣化も、事故の当日に臨時の場で決めるのではなく、事前に負荷試験で品質と遅延を検証しておくべきです。

7.3 アラート:予算の焼き付き速度

固定閾値のアラートには二つの問題があります。低めに設定するとノイズになり、高めにすると手遅れです。コストのアラートは焼き付き速度(burn rate)を使うべきです。これは実際の支出の進み具合を、時間の進み具合で割った値です。絶対的な予算額とは無関係なので、異なる事業規模に自ずと適応できます。

def burn_rate(used: float, budget: float, elapsed_fraction: float) -> float:
    """烧穿速率 = 花费进度 / 时间进度。
    = 1.0  : 完全按计划花钱(正常)
    > 1.0 : 花得比计划快,1.5 表示按当前速度会在周期的 2/3 处花完全部预算
    < 1.0 : 花得比计划慢(业务量不足,或用量被优化掉了)
    """
    if elapsed_fraction <= 0:
        return float("inf")
    return (used / max(budget, 1e-9)) / elapsed_fraction


def alert_level(used: float, budget: float, elapsed: float) -> str:
    r = burn_rate(used, budget, elapsed)
    if r >= 2.0:
        return "P0: 已确定会超支,立即定位并处置"
    if r >= 1.5:
        return "P1: 高概率超支,当日内定位"
    if r >= 1.2:
        return "P2: 偏离计划,纳入观察"
    if r <= 0.5 and elapsed > 0.5:
        return "P3: 花得太慢,检查用量上报是否丢失或路由是否被误降级"
    return "ok"


# 告警必须按维度分别计算,而不是只看全局:
#   全局 burn rate 正常 + 某租户 burn rate = 5.0  →  必须告警
#   全局 burn rate 正常 + 某模型单位成本突增    →  必须告警
# 建议的最小告警集:
#   1) 周期总预算烧穿速率(P0/P1/P2)
#   2) 按租户的烧穿速率 top-N(单租户拖垮)
#   3) 单位成功任务成本的同比/环比突变(效率退化,与总量无关)
#   4) 成本放大系数(账单 token / 有效 token)异常(重试/思考膨胀)
#   5) 账本与账单的差额率(数据管道故障的早期信号)
#   6) 缓存 token 命中率的突降(配置或流量结构变化)

五番目は強調する価値があります。データパイプラインの障害は、コストの低下に偽装されます。報告の経路が切れると、総コストは急落し、アラートは鳴らず、月末の突合で初めて一ヶ月分のデータ欠落に気づきます。「台帳と請求書の差額率」を第一級の監視指標にすることは、コストエンジニアリングの中で、インフラの障害を事前に見つけられる数少ないシグナルの一つです。

三番目も同様に重要です。単位コストの急変は、総額よりも早いシグナルです。総額には事業量の巨大なノイズが混ざるからです。事業量の成長による総額の上昇は健全であり、コスト効率の劣化による上昇は危険です。両者は分けてアラートしなければなりません。

7.4 硬い制約を実装する場所

予算の硬い制約は、最終的にはゲートウェイかスケジューリング層に落とさなければなりません。アプリケーションコード内の判断ではないからです。理由は二つです。アプリケーション層の判断は並行下では信頼できません(複数のリクエストが同時に「まだ残高がある」を読み、同時に通してしまいます)。それに加えて、内部サービス間の呼び出しもカバーできません。

推奨される構造はこうです。リクエストの入口で割り当てチェックと予約(楽観的な引落し)を行い、コストイベントが計上される時点で実際の清算(見積もりの誤差を修正)し、周期の境界でリセットと繰り越しを行います。予約と清算の二段構えは必要なのです。実際の使用量はリクエストを送出した時点では未知(特にストリーミング生成)であり、見積もりしかできないからです。しかし見積もりは後から修正できなければなりません。そうしないと、台帳は長いあいだ請求からずれてしまいます。

コスト帰属における九つのよくある落とし穴

この節は本文でもっとも実用的な部分です。下の九つの穴は、スケールして運用しているすべての AI プロダクトにほぼ現れます。そしてその大半は、ただ小さい偏差にとどまらず、方向性の誤った結論を生みます。

落とし穴 1:リトライによる増分が黙って消える

もっとも典型的な症状は、監視上のコスト曲線が完全に滑らかで、スパイクが一切ないのに、実際の請求が予想よりはるかに高いことです。原因は、リトライのリクエストが異なるサービスインスタンスを通り、元のリクエストと結び付く識別子が欠けていることにあります。結果として、リトライのコストは別の時点に、別の機能に、あるいは別のテナントに計上されてしまっています。

防御策は、trace_id がリトライの鎖の全体を通して引き継がれることです。ゲートウェイ内部のリトライ、SDK 層のリトライ、クライアントのタイムアウト再送、含めてそれらすべてです。attempt は 1 から始まって厳密に増加しなければなりません。レポートには「リトライコストの比率」という指標が必ず必要です。そしてこの指標はコストパネルの最初の画面に出すべきです。これは先行指標で、通常はコスト超過が起きるずっと以前に異常化します。

落とし穴 2:コンテキストの不可視な膨張

どのチームも「コンテキストは長いほど高価」だと知っています。しかしコンテキストの実際の内訳を測定する人はほとんどいません。現実のシステムにおける膨張は、通常は次のような目立たないソースから来ます。

  • ツール定義(function schema)が毎ターン入力に入り、ツールの数も機能の増加とともに増える。
  • 複数の検索結果が同時に注入され、互いに高度に重複している。
  • システムプロンプトの few-shot 例が、実際のトラフィックではまったく有効に使われていないのに、毎ターン課金されている。
  • 構造化出力の JSON Schema が、フィールドの増加とともに膨張する。
  • 失敗時のリトライで、直前の失敗した出力全体がコンテキストに一緒に持ち込まれる。
  • マルチ Agent の場面で、サブ Agent の完全な出力がそのままメイン Agent に渡され、層ごとに増幅される。

防御策はコンテキストを予算管理の対象として扱うことです。開発期にツールを使ってタスク種別ごとの入力 token 分布を統計し、業務の分位点(平均値ではなく)で上限を設定します。リリース後も、入力 token の分位点のトレンドを監視し続けます。平均値だけを見ないでください。入力 token 分布の長い裾は、平均値よりも強くコスト事故を予知します。

落とし穴 3:評価トラフィックが本番統計に混ざる

オフライン評価、A/B テスト、リグレッションテストセットの再生、人手によるアノテーション。これらのトラフィックが使うモデル設定、プロンプトテンプレート、パラメータは本番とはまったく違う可能性があります。しかし消費されるのは同じベンダーアカウントです。さらに隠されやすいのはこうです。内部の社員が本番環境で機能の dogfooding をしている。このトラフィックは収入を生まず、評価用の流量でもないのに、台帳の上では「prod」扱いになる。

防御策は、env ラベルをゲートウェイ層で統一的に注入することです。呼び出し側の良心を指望してはいけません。加えて定期的に「台帳と請求書」の差額帰属を行い、信頼できる差額の説明比率を磨り上げていきます。判断基準は単純です。もし本番コストとベンダー請求書のあいだに、二三のクエリでは説明できない安定した差額があるなら、それはタグ付けされていない流量があるということです。

落とし穴 4:ストリーミングの中断が全量課金になる

ユーザーが送信をクリックして、二秒待って、遅いと感じてページを閉じました。サーバー側ではまだ生成が続いているかもしれませんし、多数の token を生成してからようやく切断を検知したかもしれません。この部分の token も通常どおり課金されますが、多くの実装では、応答が正常に終了しなかったため記録されていません。その結果、台帳は本番コストを過小評価し続けます。そして過小評価の量は、ユーザーの不満の度合いと正の相関にあります。もっとも高く、かつもっとも体験の悪いトラフィックが、恰恰もっとも記録から漏れているのです。

防御策は、コストイベントを応答ストリームのキャンセルコールバックの中で計上することであり、成功終了時だけ計上することではありません。生成済みの部分は実際の token 数で精算します。そしてこの指標自体を監視する価値があります。「中断前に生成済みだった token の平均数」が突然上昇するのは、遅延体験の悪化やモデル出力が冗長になったことを示す初期サインです。

落とし穴 5:キャッシュヒット率の分母が間違っている

cached_prompt_tokens / prompt_tokens でヒット率を計算するとき、キャッシュ可能なプレフィックスをまったく持たないリクエスト(短いリクエスト、動的なプレフィックス)があれば、それらは分母に入ります。しかしヒットには決して貢献しません。結果として全体のヒット率が下げられます。するとキャッシュが機能していないと誤った判断を下します。実際には、そもそも構造的にキャッシュできないリクエストが一部あるだけです。

正しい方法は、分母に「構造上キャッシュできるはずのリクエスト」だけを含めることであり、さらにキャッシュできないトラフィックの比率自体を、一つの指標として別に観察することです。この二つの数字は並べて表示しなければなりません。そうしないと、キャッシュ最適化についての判断が、構造的な偏りによって継続的に誤誘導されます。

落とし穴 6:長いコンテキストが「節約したつもりが、実際には高くつく」

直感に反してよくある現象があります。「途中で切れて質問し直す」を避けるために長いコンテキストを有効にしたのに、総コストが逆に上がってしまった。理由は、ユーザーが確認を何往復も重ねる必要がなくなったため、成功率がとても高くなり、効率が上がったように見えるからです。しかし長いコンテキストの一回あたりのコストは非線形です。「何往復か質問する」という古い経路は、失敗したときだけ追加コストを払っていたのです。さらに悪いのは、多くの場面で長いコンテキストに押し込まれた文書の大半が使われていない、すなわち完全に無駄な課金になっていることです。

防御策は、長いコンテキストの機能には単独でユニットエコノミクスの分析を行い、「情報利用率」を補助指標として使うことです。注入されたコンテキストのうちに、実際に最終回答に影響した割合がどれくらいかを統計します。利用率が長期的に低いコンテキストは、無駄に全量を注入するのではなく、必要に応じた検索に変えるべきです。

落とし穴 7:A/B 実験の配分が間違っている

実験期間のコスト比較は、チームが誤った結論を出すもっとも成功率の高い場所です。よくある問題は次のとおりです。実験群と対照群のトラフィック配比が設計の前提と違う。実験トラフィックが特定種類の利用者に偏っている(通常はその機能に興味のある利用者)。実験期間に偶然、上流の価格変更やトラフィック構造の変化が重なった。そして両群のキャッシュヒット率が本質的に違う(新方式が旧方式と一部のキャッシュを共有し、コストが誤って希釈されている)。

防御策は、実験期間のコスト分析には総量ではなく単位コストを使うこと、トラフィック配比を補正すること、キャッシュのヒット状態を層の軸として扱うこと、そして実験トラフィックは通常、代表的なものではないことに気づくことです。長い裾の利用者に対する実験時の挙動は、本番とは往々にして逆になります。

落とし穴 8:タイムゾーンと請求期間のずれ

これは純粋に技術的な問題ですが、その影響は非常に深刻です。請求システムは UTC で区切っています。ビジネスシステムは現地時間で区切っています。テナントの支払期間は、さらに契約日から区切っています。「先月、どのテナントの推論コストが最高だったか」を答えようとするとき、実際にやっているのは三つの異なる時間窓の集合演算です。これに気づかないでいると、完全に誤ったランキングが出て、それに基づいて誤った価格設定やプランの決定まで下してしまいます。

防御策は、すべてのコストレポートで時間窓、タイムゾーン、区切り根拠を明示することです。テナント単位のレポートではセッションの完了時刻を基準に集約し、未精算の部分は別枠で立てます。

落とし穴 9:「コストが下がった」を最適化成功とみなす

コストが下がる理由は四つあります。利用量構造が変わった。事業量が落ちた。ルーティングが劣化した。あるいはデータ報告が壊れた。このうち一つ目の理由で、かつ単位品質指標が安定しているとき、はじめて最適化成功です。もっとも警戒を緩めさせやすい落とし穴です。よい知らせのように見えるからです。

防御策は、コスト改善のいかなる結論も三つの条件を同時に満たさなければならないということです。成功したタスクあたりのコストが下がっている。成功率が下がっていない。台帳と請求書の差額率が安定している。三つどれかが欠けるなら、結論のそばに「要検証」と記して、経営レポートには書きしないでください。

導入ロードマップと組織の分担

コストエンジニアリングを一度に作り上げる必要はありません。以下は依存関係で並べた道筋であり、各ステップは前のステップの成果物を入力とします。三つの段階を展開する前に、まず原則を明示しておきます。各ステップが設定だけを残してはならず、納品可能な成果物を残さなければならないということです。納品可能な成果物とは、実行できる突合スクリプト、固定の切り方を持つダッシュボード、価格設定の議論を支える数字のことです。成果物はチームをまたいで信頼を作る唯一の手段です。成果物がなければ、プラットフォームチームは「コストに問題ない」と言い、ビジネスチームは「コストが高すぎる」と言い、双方とも印象で議論するしかなくなります。そしてこの議論こそ、コストエンジニアリングが進まないもっともよくある理由です。

もう一つの原則は、台帳が正しくできる前に、最適化の目標を約束しないことです。多くのチームは「コストを何パーセント下げる」という四半期目標を立てますが、台帳が欠けている状態では、ほぼ確実に失敗します。チームは母数が分からないという問題もあります。達成しても、それが最適化のおかげだと証明できないという問題もあります(事業量構造が変わった可能性がある)。まず測定を固めてから、目標の話をするのは遅くありません。

9.1 三つの段階

第一段階:まずコストを見えるようにする。目標は二週間で「台帳が請求書に合う」状態にすることです。具体的なアクションは次のとおりです。ゲートウェイ層で trace、テナント、機能、環境のタグを統一的に注入する。コストイベント表を作り、冪等な書き込みを保証する。価格スナップショットのフィールドで履歴の不変性を保証する。「台帳と請求書」の日次突合を実装し、差額を明示的に露出させる。この段階では何の最適化もしません。まず土台を固めます。受入基準は、どの日の台帳と請求書の差額も説明でき、機能・テナント・モデルの三つの軸でドリルダウンできることです。

第二段階:ユニットエコノミクスを成立させる。目標は「どの機能、どのテナントが儲かっているか」に答えられるようにすることです。具体的なアクションは次のとおりです。切り方を「成功したタスクあたりのコスト」に統一する。テナント層と機能層の粗利レポートを作る。ビジネス側と合意した「成功」の定義を確立する。コストが異常な長い裾を特定する(高コストのテナント、長いコンテキストのタスク、往復回数の多いセッション)。この段階の成果は、プロダクトとビジネスがコストについてまったく違う認識を持つことがよくあります。通常は技術チームの見積もりよりも悲観的です。それ自体が価値です。受入基準は、会議の中で「どの機能を絞るべきか」をデータに基づいて決定できることです。

第三段階:コストを運用可能にする。目標は予算、レート制限、アラート、劣化チェーンをすべて整えることです。予算はまずソフトなアラートから始めます(アラートするだけで、遮断はしません)。二つの周期を実行して閾値が妥当かを確認してから、ハードな制約を有効にします。この段階の鍵となる規律は、ハードな制約の導入には劣化チェーンが伴い、その劣化チェーンは先に負荷試験で検証済みでなければならないということです。そうしないと、レート制限が可用性を直接叩き落とし、コストのために収入を失うことになります。受入基準は、コスト事故の演習を最初から発見、劣化、振り返りまで一通り実行できることです。

9.2 組織の分担:プラットフォームは切り方、プロダクトは構造

コストが制御できなくなる原因として技術能力の不足を挙げる方はまれです。ほとんどは誰もユニットエコノミクスに責任を負っていないことに原因があります。責任の分け目は次のように推奨します。

  • プラットフォーム/インフラチームは、台帳、課金の切り方、単価、レート制限、アラートの仕組みを担当します。役割はコストを帰属させ、制約できる状態にすることであり、業務コストの構造に責任を負うことではありません。
  • ビジネス/プロダクトチームは、ユニットエコノミクスと利用量構造を担当します。役割はコストを採算が合う状態にすること、すなわち、プロダクト設計、プラン設計、プロンプトとインタラクションデザインによって、コストを競争力のある水準まで下げることです。
  • 双方の共同が担当するのは、劣化チェーンの品質下限です。劣化は品質を落とします。このトレードオフは、どちら一方だけでは決められません。

この分担の裏側には現実的な理由があります。プラットフォームチームには、どの機能のコストが高すぎるかどうかを推測する動機がありません。プロダクトチームには、単価や計算資源の配分を変える力がありません。コストエンジニアリングが本質的に行うべきことは、この両者に共通の物差しを差し出すことです。

9.3 アンチパターンのチェックリスト

最後に、明確に避けるべき手法をいくつか挙げます。実践における出現頻度は非常に高いです。

  • 先に最適化してから測定する。台帳がないまま最適化すると、後からでは効果を証明できず、チームはコストエンジニアリングへの信頼を次第に失います。
  • 入力だけ最適化して、呼び出し回数を最適化しない。呼び出し回数のレバーは通常、一回あたりのコストより大きいので、それを無視することが最大の構造的な無駄です。
  • コストをモデルの属性として扱う。コストはシステムの属性です。同じ大きいモデルを使う場合でも、プロンプト構造、ルーティング戦略、セッション設計が違えば、コストはまったく違います。
  • 総量指標で効率を判断する。事業量の増加は効率のシグナルを埋もれさせます。すべての効率の結論は、正規化した指標で出さなければなりません。
  • 劣化チェーンのないハード予算を導入する。最初のトラフィックのピークで、収入ごと切り落としてしまいます。
  • ビジネス側に自分でタグを埋めてもらう。タグはシステムが信頼できる位置で注入しなければなりません。そうしないと、そのデータ品質はどの決定も支えません。

コストエンジニアリングの成熟のポイントは「請求書の金額が小さくなった」ことではなく、コストが、レイテンシや可用性と並ぶ日々の運用指標になったことです。定義があり、計測点があり、ダッシュボードがあり、アラートがあり、責任者がいて、予算制約があり、劣化の対応策があります。この段階に来ると、コストは月末の悪い知らせではなく、毎朝ダッシュボードを開けば最初に見える一つの数字になります。そのとき、コスト削減は個別の最適化キャンペーンに頼らずに、自然と発生します。