1. なぜ「動く」≠「採算が取れる」なのか
LLM アプリには重要な特徴があります。ホスティングの固定費に加えて、モデル呼び出しに伴う一部のリクエストに対する変動費がしばしば発生することです。
次の2つの世界を区別することが大切です。
- モデルがChatGPT 側で動く場合(ユーザーは ChatGPT 上のあなたの App と対話し、App は mcp-tools を呼ぶ)— トークン代はユーザーの ChatGPT サブスクリプションで支払われます。
- あなたの backend/MCP サーバーが自分で OpenAI API や他の LLM サービスを呼ぶ場合 — そのトークン代はあなたが支払います。
まさに後者の場合に、典型的な LLM の変動コストが発生します。これはリクエストの数や「重さ」(tokens_in/tokens_out)に依存します。
典型的なシナリオ:
- あなたはうれしげに GiftGenius を本番リリースし、すべてが快調に動き、ユーザーは満足しています。
- 1か月後、OpenAI+クラウド+Stripe の手数料の請求書が届き、「順調な成長」が実は「売上よりも1件のギフトあたりのコストのほうが高い」ことを思い知らされます。
FinOps の考え方では、コストは latency や error‑rate と同じメトリクスです。 Excel での当て推量ではなく、ログ化・集計し、それに基づいて意思決定すべきです。
この講義の目的は、次のレベルの問いに答えられるようにすることです。
- 「ユーザー user42 のこの特定のギフト提案はいくらかかったのか?」
- 「この1週間でツール suggest_gifts はいくら燃やし、どれだけの注文を生んだのか?」
そして、その答えが空想ではなく、ログとメトリクスから導かれるようにすることです。
2. ChatGPT App におけるコスト構造
まずはコストの地図から始めましょう。これがなければ、残りは数字の場当たり的な寄せ集めになります。
LLM コスト(変動費)
これは、あなたのバックエンドからのモデル呼び出しに関わるすべてです。
- MCP サーバーまたはエージェントからの OpenAI モデル呼び出し: GPT-5.1 / GPT-5-mini / embeddings / rerank / vision / TTS/STT など。
- 追加モデル: 検索のリランキング、レコメンド用のエンベディング、画像生成など。
重要な注意点があります。Apps SDK で UI を構築し、ChatGPT の組み込みモデルのみを使っている場合、トークン代はあなたではなくユーザー(ChatGPT サブスクリプション)に課されます。 しかし、あなたの MCP サーバーが自ら OpenAI API(Agents、Responses API、embeddings など)を呼び始めた瞬間、そのトークンはあなたの課金になります。
基本的な考え方: そのような呼び出しのコストは tokens_in と tokens_out に比例し、トークン単価を掛けたものです。
MCP ツールの呼び出し自体は、トークンという観点では開発者にとって無料です。費用が発生するのは、そのハンドラ内で OpenAI API やほかの LLM を呼ぶときだけです。
インフラ
周辺のハードウェアやサービス全般です。
- MCP サーバー: Vercel / AWS / GCP / bare metal。
- エージェント(別サービスとして動かしている場合)。
- データベース: Postgres/MySQL、ベクトル DB、S3/オブジェクトストレージ。
- キャッシュ: Redis/KeyDB。
- キューとワーカー: 例: バックグラウンド生成、フィード再計算など。
これらのコストは多くの場合月次固定(もしくは段階的固定)なので、通常は各リクエスト単位ではなく、クラウド請求の集計データとして計上します。
決済および外部サービス
GiftGenius には ACP/Stripe があるため、次のようなものが発生します。
- 各成功決済に対する手数料(Stripe は数 % 程度+固定額)。
- 不正とチャージバックによる損失。
- 外部 API の費用: e‑mail / SMS / push 通知、追加のアナリティクス等。
初期は微々たる額でも、スケールすると効いてきます。少なくともログやレポートのレベルでは切り出しておくと有益です。
メモ用の小さな表
| カテゴリ | 例 | ざっくりの計算方法 |
|---|---|---|
| LLM | GPT‑5.1, GPT‑5‑mini, embeddings, rerank | |
| インフラ | MCP, Agents, DB, Redis, キュー, CDN | プロバイダーの請求をトラフィック/期間で按分 |
| 決済とサービス | Stripe, e‑mail API, SMS, アナリティクス | イベント件数 × 単価/手数料 |
私たちの目標は、これらのカテゴリをシステム内の具体的なイベント (ツール呼び出し、ワークフロー、チェックアウト)に結びつけることであり、月次の合計額だけを眺めないことです。
3. usage データをどこで取るか: 3 層
コストを「月に一度」ではなくリアルタイムに把握するには、コードにインストルメンテーションを組み込む必要があります。場所は3つだけです。
MCP サーバー: 各ツール呼び出し
MCP サーバーは、ChatGPT があなたのツールを呼び出す自然な経路です。ここで次のことができます。
- 呼び出しの開始/終了タイミングを捕捉。
- duration_ms(または latency_ms)を計測。
- OpenAI のレスポンスからトークンを収集(MCP からモデルを呼ぶ場合)するか、少なくとも見積もり。
- ログを紐づけるために user_id、tenant_id、request_id/trace_id を付与。
GiftGenius における概念的なログイベント tool_invocation は次のようになります。
{
"timestamp": "2025-11-20T12:34:56Z",
"level": "info",
"event": "tool_invocation",
"request_id": "abc123",
"user_id": "user42",
"service": "mcp-giftgenius",
"tool_name": "suggest_gifts",
"tokens_in": 120,
"tokens_out": 350,
"cost_estimate_usd": 0.045,
"latency_ms": 320
}
次に、同じものを TypeScript の型とコード片で示します。
// types/telemetry.ts
export interface ToolInvocationLog {
event: 'tool_invocation';
requestId: string;
userId?: string;
toolName: string;
tokensIn?: number;
tokensOut?: number;
costEstimateUsd?: number;
latencyMs: number;
}
// mcp/logger.ts
export function logToolInvocation(payload: ToolInvocationLog) {
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
level: 'info',
...payload,
}));
}
次は MCP ツール(仮に suggest_gifts)のハンドラをラップします。
// mcp/tools/suggestGifts.ts
export async function handleSuggestGifts(ctx: Context, input: Input) {
const started = Date.now();
const llmResult = await callGiftModel(input); // ここで OpenAI を呼ぶ
const duration = Date.now() - started;
const { prompt_tokens, completion_tokens } = llmResult.usage ?? {};
const costEstimate = estimateCost(prompt_tokens, completion_tokens);
logToolInvocation({
event: 'tool_invocation',
requestId: ctx.requestId,
userId: ctx.userId,
toolName: 'suggest_gifts',
tokensIn: prompt_tokens,
tokensOut: completion_tokens,
costEstimateUsd: costEstimate,
latencyMs: duration,
});
return llmResult.output;
}
トークン数をテキスト長の概算で「だいたい」で算出するだけでも、何もしないよりずっと良いのです。
エージェントレイヤー(Agents SDK): ワークフローのステップ
Agents SDK を使っている場合、エージェントは複数のツールを連続して呼ぶことがあります。ここでは、エージェントがどんなタスクを解こうとしているのかというステップの文脈をログすることが重要です。
たとえば各ツール呼び出しのたびに、エージェントのランナーから workflow_name と step_name を付与します。 例: 「アイデア探索」「予算によるフィルタリング」「チェックアウト準備」。
これにより、ツール単位だけでなくシナリオのステップ単位でもレポートを構築できます。 たとえば、全コストの 80% が役に立たない「追加の補足ステップ」に消えている、といった発見が可能になります。
エージェントの小さな「フック」の例:
// agents/logStep.ts
export function logAgentStep(data: {
requestId: string;
workflow: string;
step: string;
toolName: string;
}) {
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
level: 'info',
event: 'agent_step',
...data,
}));
}
そしてランナーからこう使います。
// agents/giftAgent.ts
logAgentStep({
requestId: run.requestId,
workflow: 'gift_selection',
step: 'rank_candidates',
toolName: 'rerank_gifts',
});
コマース: チェックアウトとお金
コマース層では次のイベントに関心があります。
- checkout_started — 購入開始。
- checkout_success — 決済成功。
- checkout_failed — エラー(コード/タイプ付き)。
そして次を紐づけます。
- amount, currency。
- request_id(同じセッション。tool_invocation と一致)。
こうすると「この購入には LLM コストが N セントかかり、収益は M ドルだった」と答えられるようになります。
簡単なチェックアウトイベントのハンドラ例:
// api/commerce/logCheckout.ts
export function logCheckoutEvent(e: {
type: 'checkout_started' | 'checkout_success' | 'checkout_failed';
requestId: string;
userId?: string;
amountCents?: number;
currency?: string;
errorCode?: string;
}) {
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
level: 'info',
service: 'commerce',
...e,
}));
}
4. コスト向け構造化ログ(M17 との関連)
要点: 「自由形式」のテキストログは厳禁です。 つまり console.log("Tool suggest_gifts used 123 tokens") のような形ではなく、 すべて JSON にしましょう。
モジュール 17 ではすでに、request_id、user_id、tool_name などの基本フィールドを持つ JSON でリクエストをログする方針を決めました。 この上にコスト関連のフィールドを追加します。
コスト関連ログに必須のフィールド:
- timestamp, level。
- event(tool_invocation、agent_step、checkout_success など)。
- request_id、trace_id — 1 つのワークフローのイベント連鎖を結びつけるため。
- user_id、tenant_id — ユーザー/組織での集計のため。
- tool_name / service。
- tokens_in、tokens_out、cost_estimate_usd。
- latency_ms、success/error_code。
例では、コストのフィールド名を cost_estimate_usd(米ドルでの推定コスト)とし、コードとダッシュボードの両方でこの名前に統一します。
この構造により、次のことが可能になります。
- tool_name、user_id、workflow ごとの平均 cost_estimate_usd といった集計。
- 「高コスト」なリクエストと高いレイテンシやエラーの相関を見つけ、優先的に最適化すべき箇所を決めること。
すでにモジュール 17 で logger.info({...}) のような基本形を実装しているなら、 cost フィールドの追加は新しいフレームワークではなく、オブジェクトにプロパティが数個増えるだけです。
5. コードでの LLM コストの概算
数式は恐れるほどのものではありません。必要なのはオーダー感であり、最後の 1 セントまで課金と一致させることではありません。
OpenAI のレスポンスの usage を使う
あなたの MCP サーバーが OpenAI Response API を呼ぶと、通常 usage オブジェクトを受け取ります。
{
"usage": {
"prompt_tokens": 120,
"completion_tokens": 350,
"total_tokens": 470
}
}
これを使うとコストを算出しやすいです。モデルごとに入力/出力 100 万トークンあたりの価格が異なります。
TypeScript の最小の概算関数:
// mcp/cost.ts
type Usage = { prompt_tokens?: number; completion_tokens?: number };
const PRICING = {
inputPerMillion: 2.5, // 入力 100 万トークンあたりのドル(例)
outputPerMillion: 10.0, // 出力 100 万トークンあたりのドル(例)
};
export function estimateCost(
promptTokens?: number,
completionTokens?: number,
): number {
const inTokens = promptTokens ?? 0;
const outTokens = completionTokens ?? 0;
const inputCost = (inTokens / 1_000_000) * PRICING.inputPerMillion;
const outputCost = (outTokens / 1_000_000) * PRICING.outputPerMillion;
return Number((inputCost + outputCost).toFixed(6)); // 少し丸める
}
ここでの価格はあくまで例です。実際の値は最新の OpenAI 料金表から取得し、コンフィグに置いてください。 重要なのは、この関数を各ツール呼び出し時に呼び、結果をログの cost_estimate_usd フィールドに入れることです。
usage がない場合
サードパーティの LLM を使っていて usage が返らない場合や、実際に呼ぶ前に抑止したい場合があります。そのときは次が考えられます。
- tiktoken のようなライブラリや対象モデル用の類似ツールでトークン数を推定。
- 過去ログの平均値(ツールごとの median_tokens_in/median_tokens_out)を取り、単価を掛ける。
長さを見積もるスタブコード:
// mcp/costEstimateFallback.ts
export function roughTokenEstimate(text: string): number {
// おおまかな推定: 1 トークン ≈ ラテン文字 4 文字
return Math.ceil(text.length / 4);
}
ロケット科学ではありませんが、たとえば 200000 トークンのプロンプトを安価なプランに通さない、といった制御ができます。
6. 主要なコスト指標
集めたログは原材料です。ここから実用的なアグリゲーションを見ていきます。
cost_per_tool_call
何か: 特定ツールの 1 回あたりの平均コスト。
なぜ必要か:
- 特に高コストなツールが見える。
- avg_cost_per_call が高く、シナリオ成功へのコンバージョンが低い「高いのに役立たない」候補を見つけられる。
ログからの算出方法:
- 期間内の event = "tool_invocation" を取得。
- tool_name でグルーピング。
- 各グループの avg(cost_estimate_usd) と、p95(95 パーセンタイル)なども併せて算出。
cost_per_successful_task(または cost_per_workflow)
Task/workflow とは、ユーザーレベルで完結したシナリオです。
- GiftGenius であれば「ギフト選定+カード表示+ユーザーが N 個のアイデアを保存」や「選定 → チェックアウト → 成功決済」など。
やること:
- ワークフロー完了時に workflow_completed イベントを request_id、workflow_name、成功フラグ付きで記録。
- request_id を介して、そのワークフローに属するすべての tool_invocation をひも付け、 それらの cost_estimate_usd を合計。
こうして「1 つの成功タスクにいくらかかったか」を得られ、シナリオの原価を理解する鍵になります。
cost_per_user / cost_per_tenant
B2B シナリオでは「1 人のユーザー/1 チームが月にいくらかかっているか」が重要になることがよくあります。
算出:
- tool_invocation などのコストイベントを user_id または tenant_id でグルーピング。
- 期間(1 日、1 か月)内の cost_estimate_usd を合計。
その後、サブスクリプション価格と比較します。もし cost_per_user がプラン価格に著しく近づいているなら、 価格改定か usage 最適化の検討時期です(この点は次の講義で、価格設定と「コスト ↔ 品質」の実験として扱います)。
7. 例: GiftGenius の tool_invocation フォーマットとダッシュボード
ここで、計画の演習で行ったとおりに、ログイベントを設計し、ツール別の最小ダッシュボードを作ります。
GiftGenius の tool_invocation イベント形式
以前は MCP ツール用の最小ログを見ました。ここではもう少し詳細な tool_invocation イベントを設計します。ダッシュボードや本番でも使える形です。考え方は同じで、サービス、エラー、モデルの紐づけのためのフィールドが増えるだけです。
まずは TypeScript の型から。
// telemetry/events.ts
export interface ToolInvocationEvent {
timestamp: string;
level: 'info' | 'error';
event: 'tool_invocation';
service: 'mcp-giftgenius';
requestId: string;
traceId?: string;
userId?: string;
tenantId?: string;
toolName: string;
modelId?: string;
tokensIn?: number;
tokensOut?: number;
costEstimateUsd?: number;
latencyMs: number;
success: boolean;
errorCode?: string;
}
そして使いやすいヘルパー:
// telemetry/emitToolInvocation.ts
export function emitToolInvocation(e: ToolInvocationEvent) {
console.log(JSON.stringify(e));
// 実運用では: Logtail/Datadog/ELK などに送信
}
各ツール(例: suggest_gifts、rerank_gifts、fetch_catalog)のハンドラ末尾(または finally ブロック)で emitToolInvocation を呼び、エラー時でもログが残るようにします。
最小のツール別ダッシュボード
ダッシュボード(Metabase / Grafana / 任意の BI)の最小テーブル例:
| カラム | 説明 |
|---|---|
|
ツール名(suggest_gifts、checkout_create_session、…) |
|
このツールに割り当てられた tool_invocation の全体に占める割合 |
|
1 回の呼び出しの平均コスト(cost_estimate_usd に基づく) |
|
success = false のイベント割合 |
|
平均レイテンシ |
|
このツールに結びついた平均収益(ある場合) |
見た目はたいてい次のようになります。上にテーブル、下に 2 つのグラフ:
- バー チャート: X 軸に tool_name、Y 軸に avg_cost_per_call。
- 散布図: X = avg_cost_per_call、Y = error_rate または conversion_to_checkout。
この種のグラフは最適化候補を素早く見つけるのに役立ちます。「高コストで遅く、コンバージョンしない」— まずはそこからです。
コストと収益の紐づけには、checkout_* を request_id と一緒にログしていることが効きます。 すると、avg_revenue_per_call を、そのツールの呼び出し回数で、checkout_success が起きたシナリオにおける収益合計を割って算出できます。
8. インフラコストの配賦(やりすぎない)
LLM コストは美しい構造です。各呼び出しにトークンがあり、ログ内で直接コストを算出できます。 インフラはそう簡単ではありません。Vercel、データベース、Redis などの月次請求があるだけです。
初期はシンプルな方法で構いません。
- 月次のインフラ請求合計(仮に 200ドル)を取る。
- ワークフロー数(workflow_completed)で割り、概算の infra_cost_per_task を求める。
- あるいはアクティブユーザー数で割り、infra_cost_per_user を求める。
その後、詳細に算出した LLM コストとこれらの数字を合算すれば、シナリオやユーザーの概算の完全な原価が得られます。
アプリが成長したら、サービスやツール単位にもう少し丁寧に配賦してもよいでしょう。しかし最初の段階では、盲目的に進まないためにこのレベルで十分です。
9. GiftGenius の小さな end‑to‑end 例
すべてをミニストーリーにまとめます。
ユーザーが贈り先を記述し、ChatGPT が GiftGenius を有効化することを提案します。続いて:
- ウィジェットがワークフロー "gift_selection" を起動。
- あなたのバックエンドは、より賢くギフトを選ぶために LLM エージェントを使うことに決めます。
- エージェントは 3 ステップを実行:
- analyze_recipient(LLM による説明の分析)。
- suggest_gifts(自作 MCP ツール)。
- rerank_gifts(リスト改善のための追加モデル)。
- ユーザーはギフトカードを見て、いくつかのアイデアを保存。
- 「購入」をクリックし、ACP と checkout_create_session が起動。
- checkout_success が成功し、金額は 79.00 USD。
ログに残るものは次のとおりです。
- 3 つの tool_invocation(各イベントに tokens_in/tokens_out、cost_estimate_usd、latencyMs)。
- いくつかの agent_step(workflow = "gift_selection"、step_name)。
- checkout_started と checkout_success(amount=7900、currency="USD")。
request_id でこれらを結びつけると、次のように言えます。
- シナリオの LLM コスト: 3 つのツールの cost_estimate_usd の合計。仮に 0.19ドル。
- インフラの按分(集計から): 1 ワークフローあたり約 0.03ドル。
- 合計、原価は 0.22ドル。
- トランザクションの売上は 79ドルから Stripe 手数料などを差し引いたもの。
これはもはや「GPT‑4 は高そう」に基づく印象論ではなく、具体的なユニットエコノミクスです。
10. コスト計測でありがちな落とし穴
落とし穴 1: 月次請求だけを見て、粒度を持たない。
OpenAI/クラウドからの合計請求だけを見るのは誘惑です。しかし tool_name、user_id、workflow との紐づけがなければ、お金がどこで使われているのか分かりません。結果として、最適化は「闇雲なモデル格下げ」になり、高コストなシナリオをピンポイントで改善する機会を逃します。
落とし穴 2: コストデータを構造なしのテキストログに書く。
"Tool suggest_gifts used 123 tokens" のような行は、質の高い集計やフィルタリングができません。いずれ JSON へ移行すべきだと気づき、その移行は痛みを伴います。最初から request_id、tool_name、tokens_in/tokens_out、cost_estimate_usd といったフィールドを持つ構造化ログにしましょう。
落とし穴 3: コスト ↔ コマースイベントの紐づけを無視。
checkout_success を request_id やツール呼び出しと結びつけずにログするのは、どのシナリオが利益を生んでいて、どれがトークンを消費しているだけなのかを理解することを自ら放棄するのと同じです。ウィジェットから ACP まで、request_id を通しで運ぶ手間を惜しまないでください。
落とし穴 4: 実用的な見積もりではなく「完璧な」課金再現を目指す。
一部のチームは、OpenAI の課金を最後のトークンまで完璧に再現しようとして沼にはまります。現実にはオーダー感で十分です。シナリオが 0.02ドルか 0.021ドルかは本質ではありません。重要なのは 2ドルではないということ。usage による近似や、粗いヒューリスティクスの使用を恐れないでください。
落とし穴 5: コストだけを見て品質を忘れる。
節約のきれいな数字を見ると、どこでも最安モデルに切り替えたくなることがあります。そうすると、ユーザーが使わなくなるレベルまでアプリを「最適化」してしまいかねません。コストは回答品質やコンバージョンとセットで見る必要があります — この結びつきは同モジュールの次の講義、価格設定と「コスト ↔ 品質」の実験で扱います。
GO TO FULL VERSION