1. 為什麼「能運作」 ≠ 「能回本」
LLM 應用有個重要特性:除了主機代管等固定成本之外,通常還會出現與模型呼叫相關的部分請求的變動成本。
務必分清楚兩個世界:
- 當模型運行在ChatGPT 端(使用者在 ChatGPT 與你的 App 互動,而你的 App 呼叫 mcp-tools)—— token 費用由使用者的 ChatGPT 訂閱支付;
- 當你的 backend/MCP 伺服器自行呼叫 OpenAI API 或其他 LLM 服務——這些 token 就要你來付費。
正是在第二種情況,你會產生典型的 LLM 變動成本,且其大小取決於請求數量與「負荷」(tokens_in/tokens_out)。
典型場景:
- 你興高采烈把 GiftGenius 推上生產,表現飛快,使用者也很開心。
- 一個月後收到 OpenAI + 雲服務 + Stripe 手續費的帳單,卻突然發現,「成功成長」其實代表「每份禮物我們付出的成本比賣出的收入還高」。
FinOps 方法(FinOps)說:成本和 latency 或 error‑rate 一樣,都是指標。 應該記錄、彙總,並據此決策,而不是「在 Excel 裡猜」。
本講座的目標——讓你能回答這樣的問題:
- 「使用者 user42 的這次禮物推薦花了多少錢?」
- 「這週工具 suggest_gifts 燒了多少錢,又帶來了多少訂單?」
而且答案不是憑空而來,而是根據日誌與指標。
2. ChatGPT App 的成本結構
先從支出地圖開始。沒有它,其他一切都只是亂抓數字。
LLM 成本(變動)
凡是與你的後端呼叫模型相關的:
- 從 MCP 伺服器或代理呼叫 OpenAI 模型: GPT-5.1 / GPT-5-mini / embeddings / rerank / vision / TTS/STT 等等。
- 額外的模型:用於搜尋的 reranking、用於推薦的 embedding、影像生成等。
要記住一個細微之處:當你透過 Apps SDK 搭建介面並只用內建的 ChatGPT 模型時,你不需要為 token 付費——由使用者(透過其 ChatGPT 訂閱)埋單。 但一旦你的 MCP 伺服器自行呼叫 OpenAI API(Agents、Responses API、embeddings 等),token 就會算在你的帳上。
基本概念:這類呼叫的成本與 tokens_in 與 tokens_out 成正比,並乘上每個 token 的單價。
呼叫 MCP 工具本身,對開發者而言不會產生 token 成本;只有在其處理器中你決定呼叫 OpenAI API 或其他 LLM 時,才會產生成本。
基礎設施
這是周邊的機器與服務:
- MCP 伺服器: Vercel / AWS / GCP / bare metal。
- 代理(Agents)(若以獨立服務運行)。
- 資料庫: Postgres/MySQL、向量資料庫、S3/物件儲存。
- 快取: Redis/KeyDB。
- 佇列與 worker: 例如用於背景生成、重算 feed 等。
這些成本多半是按月固定(或階梯性固定),因此通常以雲服務帳單的彙總資料來估算,而不是逐個請求計算。
支付與外部服務
GiftGenius 使用 ACP/Stripe,於是會有:
- 每筆成功付款的手續費(Stripe 約為數個百分比 + 固定費用)。
- 詐欺與拒付(chargeback)的損失。
- 外部 API 成本:e‑mail / SMS / push 通知、額外的分析等。
一開始只是小錢,但規模上來後就有感了,因此至少在日誌與報表層級將它們獨立出來是有用的。
備忘小表
| 類別 | 範例 | 粗略計算方式 |
|---|---|---|
| LLM | GPT‑5.1, GPT‑5‑mini, embeddings, rerank | |
| 基礎設施 | MCP, Agents, DB, Redis, 佇列, CDN | 按流量/期間將雲帳單分攤 |
| 支付與服務 | Stripe, e‑mail API, SMS, 分析 | 事件數 × 費率/手續費 |
我們的目標:把這些類別綁定到系統中的具體事件 (tool 呼叫、workflow、checkout),而不是只看最後的月度總額。
3. 在哪裡收集 usage 資料:三層
若要不是「每月一次」而是即時地計算 cost,必須把儀表化嵌入程式碼。位置就三個。
MCP 伺服器:每次工具呼叫
MCP 伺服器是 ChatGPT 呼叫你的 tools 的自然切入點。在這裡我們可以:
- 捕捉呼叫的開始/結束時刻。
- 量測 duration_ms(或 latency_ms)。
- 從 OpenAI 回應中收集 token(若 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;
}
即便只是透過估計文字長度來「粗估」 token,仍然比什麼都不做要好。
代理層(Agents SDK):workflow 步驟
如果你使用 Agents SDK,代理可能會連續呼叫多個工具。此處要記錄步驟脈絡:代理正要解決什麼子任務。
例如,在每次代理執行 tool 的時候,可加入 workflow_name 與 step_name 欄位: 「尋找點子」、「依預算篩選」、「準備 checkout」。
如此便能之後不只針對工具,還能針對情境步驟做報表: 也許 80% 的成本都耗在某個沒什麼用的「額外補充步驟」上。
代理周邊的一個小「hook」範例:
// 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,
}));
}
在 runner 中這樣使用:
// agents/giftAgent.ts
logAgentStep({
requestId: run.requestId,
workflow: 'gift_selection',
step: 'rank_candidates',
toolName: 'rerank_gifts',
});
Commerce:checkout 與金流
在 commerce 層,我們關心以下事件:
- checkout_started — 開始購買。
- checkout_success — 付款成功。
- checkout_failed — 發生錯誤(帶上錯誤代碼/型別)。
還需要附上:
- amount、currency。
- 與同一個工作階段的 request_id,對應 tool_invocation。
這樣我們就能回答:「這次購買花了我們 N 分美分的 LLM 成本,帶來 M 美元的營收」。
簡單的 checkout 事件處理器範例:
// 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. 用於 cost 的結構化日誌(與 M17 的關聯)
關鍵點:不要寫任何「自由格式」的文字日誌 console.log("Tool suggest_gifts used 123 tokens")。 一切都用 JSON。
在模組 17 中我們已經約定,以 JSON 形式記錄請求,包含 request_id、user_id、tool_name 等基本欄位。 現在在此基礎上加入 cost 欄位。
與成本相關的日誌必備欄位:
- timestamp、level。
- event(tool_invocation、agent_step、checkout_success 等)。
- request_id、trace_id——用於關聯同一 workflow 的事件鏈。
- 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。
- 把「昂貴」的請求與較高的延遲或錯誤率做關聯,從而決定優先優化哪些項目。
如果你已在 M17 做了基礎的 logger.info({...}), 加入 cost 欄位並不是引進新框架,只是多幾個屬性而已。
5. 如何在程式碼中粗估 LLM 成本
公式一點也不可怕。我們只需要量級上的估計,而非精確到最後一分錢的帳單對齊。
從 OpenAI 回應中取得 usage
當你的 MCP 伺服器呼叫 OpenAI Response API 時,通常會得到一個 usage 物件:
{
"usage": {
"prompt_tokens": 120,
"completion_tokens": 350,
"total_tokens": 470
}
}
用它很好算成本。不同模型對輸入/輸出每百萬 token 的單價不同。
最簡單的 TypeScript 估算函式:
// mcp/cost.ts
type Usage = { prompt_tokens?: number; completion_tokens?: number };
const PRICING = {
inputPerMillion: 2.5, // 每百萬輸入 token 的美元單價(示例)
outputPerMillion: 10.0, // 輸出 token 的單價
};
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 的最新價目為準,並放入設定。重點是此函式會在每次 tool 呼叫時執行,結果寫入日誌欄位 cost_estimate_usd。
如果 usage 不可用
有時你使用的 LLM 不會回傳 usage,或在真實呼叫前需要先做預估。可以:
- 用像 tiktoken 這樣的套件(或目標模型的等價庫)來估計 token 數。
- 採用歷史日誌的平均值(median_tokens_in/median_tokens_out)乘上單價。
估算長度的樣板程式:
// mcp/costEstimateFallback.ts
export function roughTokenEstimate(text: string): number {
// 粗略估計:1 個 token ≈ 4 個拉丁字母字元
return Math.ceil(text.length / 4);
}
這不是什麼火箭科學,但能避免讓 200000 token 的 prompt 進入便宜方案。
6. 關鍵 cost 指標
收集到的日誌只是原料。現在看看哪些彙總最有用。
cost_per_tool_call
這是什麼: 某個工具每次呼叫的平均成本。
為什麼要看:
- 能看見哪些工具特別昂貴。
- 可找出「又貴又沒用」者: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 個點子」或「推薦 → checkout → 成功購買」。
怎麼做:
- 在 workflow 結束時寫入事件 workflow_completed, 帶上 request_id、workflow_name 與成功旗標。
- 透過 request_id 關聯該 workflow 的所有 tool_invocation, 並加總它們的 cost_estimate_usd。
如此即可得到「一個成功任務的成本」——這是理解情境成本結構的關鍵。
cost_per_user / cost_per_tenant
對 B2B 情境,常見問題是:「每位使用者/每個團隊每月的成本是多少?」
計算方式:
- 將 tool_invocation 與其他 cost 事件按 user_id 或 tenant_id 分組。
- 在區間內(天、月)加總 cost_estimate_usd。
再與訂閱價格比較。若 cost_per_user 大幅接近方案價格, 就該考慮調價或最佳化用量(這會在本模組下一堂課中談到 pricing 與「成本 ↔ 品質」的實驗)。
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;
}
以及好用的 helper:
// telemetry/emitToolInvocation.ts
export function emitToolInvocation(e: ToolInvocationEvent) {
console.log(JSON.stringify(e));
// 實務上:送到 Logtail/Datadog/ELK 等等。
}
對每個工具(例如 suggest_gifts、rerank_gifts、fetch_catalog), 在 handler 結尾(或在 finally 區塊,確保出錯時也有日誌)呼叫 emitToolInvocation。
最簡儀表板:工具維度
儀表板上的最小表格(例如在 Metabase / Grafana / 任一 BI):
| 欄位 | 說明 |
|---|---|
|
工具名稱(suggest_gifts、checkout_create_session 等) |
|
此工具佔全部 tool_invocation 的比例 |
|
單次呼叫的平均成本(來自 cost_estimate_usd) |
|
success = false 的事件百分比 |
|
平均延遲 |
|
與此工具關聯的平均營收(若有) |
視覺上通常長這樣:上面是表格,下面是幾個圖表:
- 長條圖:X 軸為 tool_name,Y 軸為 avg_cost_per_call。
- 散佈圖:X = avg_cost_per_call,Y = error_rate 或 conversion_to_checkout。
這些圖能快速找出優化對象:昂貴、緩慢且沒有轉換——先從那裡下手。
把 cost 與 revenue 關聯起來的關鍵在於:我們將 checkout_* 與 request_id 一起記錄。 如此便能計算 avg_revenue_per_call:在發生 checkout_success 的情境中,營收總和除以該工具的呼叫次數。
8. 基礎設施成本攤提(別走極端)
LLM 成本很乾淨:每次呼叫都有 token,可以直接在日誌裡算成本。 基礎設施就沒那麼簡單:你拿到的是 Vercel、資料庫、Redis 等的月度帳單。
起步可以走簡單路線:
- 拿每月的基礎設施帳單總額(例如 200$)。
- 除以每月的 workflow 數量(workflow_completed)——得到近似的 infra_cost_per_task。
- 或除以活躍使用者數——得到 infra_cost_per_user。
然後把這些數字與我們用日誌詳算出的 LLM 成本相加——得到近似的總成本(按情境或按使用者)。
當應用長大後,可以做得更細(將費用按服務與工具分攤),但在前幾版做到這個程度,已足夠避免盲目行事。
9. GiftGenius 的一個小型端到端範例
把一切串成小故事。
使用者描述收禮者,ChatGPT 建議啟用 GiftGenius。接下來:
- 小工具啟動 workflow "gift_selection"。
- 你的後端決定使用 LLM 代理,讓禮物推薦更聰明。
- 代理執行 3 個步驟:
- analyze_recipient(用 LLM 分析描述)。
- suggest_gifts(我們的 MCP 工具)。
- rerank_gifts(用額外模型改善清單)。
- 使用者看到禮物卡片,並儲存了幾個點子。
- 點擊「購買」,啟動 ACP 與 checkout_create_session。
- 成功的 checkout_success,金額 79.00 USD。
日誌中會留下:
- 三個 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 成本:三個工具的 cost_estimate_usd 之和,假設為 0.19$。
- 基礎設施分攤約 0.03$ 每個 workflow。
- 合計成本為 0.22$。
- 交易營收——79$,再扣掉 Stripe 手續費等。
這已經是具體的單位經濟學,而不是「感覺 GPT‑4 很貴」。
10. 成本儀表化的常見錯誤
錯誤 #1:只看月度帳單,沒有細節粒度。
只看 OpenAI/雲的總帳單很誘人。但若沒有和 tool_name、user_id、workflow 的關聯,你就不知道錢到底花在哪裡。結果優化變成「盲目降級模型」,而不是對昂貴情境做精準改善。
錯誤 #2:把 cost 數據寫進無結構的文字日誌。
像 "Tool suggest_gifts used 123 tokens" 這樣的行無法高品質地彙總與篩選。到某個時間點你會發現必須遷移到 JSON,而這個搬遷會很痛苦。從一開始就用結構化日誌,包含 request_id、tool_name、tokens_in/tokens_out、cost_estimate_usd 等欄位。
錯誤 #3:忽視 cost ↔ commerce 事件的關聯。
只記錄 checkout_success,卻沒有 request_id 與 tool 呼叫的關聯——等於自願放棄理解哪些情境帶來利潤、哪些只是吃 token。別懶,請把 request_id 從小工具一路傳到 ACP。
錯誤 #4:試圖做「完美」計費,而不是務實估算。
有些團隊會執著於把 OpenAI 帳單到最後一個 token 都完美重現。現實上只要量級正確即可:情境成本是 0.02$ 還是 0.021$ 並不重要;重要的是它不是 2$。大膽用 usage 的近似或甚至粗略啟發式。
錯誤 #5:只看 cost,忘了品質。
有時看見漂亮的節省數字,就想全面切到最便宜的模型。如此可能把應用「優化」到使用者不想用了。成本必須與答案品質與轉換一起看——這個關聯會在本模組的下一堂課(關於 pricing 與「成本 ↔ 品質」實驗)中展開。
GO TO FULL VERSION