CodeGym /課程 /ChatGPT Apps /成本控管與 cost 儀表化

成本控管與 cost 儀表化

ChatGPT Apps
等級 19 , 課堂 0
開放

1. 為什麼「能運作」 ≠ 「能回本」

LLM 應用有個重要特性:除了主機代管等固定成本之外,通常還會出現與模型呼叫相關的部分請求的變動成本

務必分清楚兩個世界:

  • 當模型運行在ChatGPT 端(使用者在 ChatGPT 與你的 App 互動,而你的 App 呼叫 mcp-tools)—— token 費用由使用者的 ChatGPT 訂閱支付;
  • 當你的 backend/MCP 伺服器自行呼叫 OpenAI API 或其他 LLM 服務——這些 token 就要你來付費。

正是在第二種情況,你會產生典型的 LLM 變動成本,且其大小取決於請求數量與「負荷」(tokens_in/tokens_out)。

典型場景:

  1. 你興高采烈把 GiftGenius 推上生產,表現飛快,使用者也很開心。
  2. 一個月後收到 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_intokens_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
tokens_in/out × price_per_token
基礎設施 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_idtenant_idrequest_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_namestep_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 — 發生錯誤(帶上錯誤代碼/型別)。

還需要附上:

  • amountcurrency
  • 與同一個工作階段的 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_iduser_idtool_name 等基本欄位。 現在在此基礎上加入 cost 欄位。

與成本相關的日誌必備欄位:

  • timestamplevel
  • eventtool_invocationagent_stepcheckout_success 等)。
  • request_idtrace_id——用於關聯同一 workflow 的事件鏈。
  • user_idtenant_id——之後可按使用者/公司彙總。
  • tool_name / service
  • tokens_intokens_outcost_estimate_usd
  • latency_mssuccess/error_code

在範例中我們將成本欄位命名為 cost_estimate_usd(以美元計價),並在程式與儀表板中始終如一使用這個名稱。

這樣的結構能夠:

  • 建立彙總:按 tool_nameuser_idworkflow 計算平均 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_idworkflow_name 與成功旗標。
  • 透過 request_id 關聯該 workflow 的所有 tool_invocation, 並加總它們的 cost_estimate_usd

如此即可得到「一個成功任務的成本」——這是理解情境成本結構的關鍵。

cost_per_user / cost_per_tenant

對 B2B 情境,常見問題是:「每位使用者/每個團隊每月的成本是多少?」

計算方式:

  • tool_invocation 與其他 cost 事件按 user_idtenant_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_giftsrerank_giftsfetch_catalog), 在 handler 結尾(或在 finally 區塊,確保出錯時也有日誌)呼叫 emitToolInvocation

最簡儀表板:工具維度

儀表板上的最小表格(例如在 Metabase / Grafana / 任一 BI):

欄位 說明
tool_name
工具名稱(suggest_giftscheckout_create_session 等)
% 流量
此工具佔全部 tool_invocation 的比例
avg_cost_per_call
單次呼叫的平均成本(來自 cost_estimate_usd
error_rate
success = false 的事件百分比
avg_latency_ms
平均延遲
avg_revenue_per_call
與此工具關聯的平均營收(若有)

視覺上通常長這樣:上面是表格,下面是幾個圖表:

  • 長條圖:X 軸為 tool_name,Y 軸為 avg_cost_per_call
  • 散佈圖:X = avg_cost_per_call,Y = error_rateconversion_to_checkout

這些圖能快速找出優化對象:昂貴、緩慢且沒有轉換——先從那裡下手。

把 cost 與 revenue 關聯起來的關鍵在於:我們將 checkout_*request_id 一起記錄。 如此便能計算 avg_revenue_per_call:在發生 checkout_success 的情境中,營收總和除以該工具的呼叫次數。

8. 基礎設施成本攤提(別走極端)

LLM 成本很乾淨:每次呼叫都有 token,可以直接在日誌裡算成本。 基礎設施就沒那麼簡單:你拿到的是 Vercel、資料庫、Redis 等的月度帳單。

起步可以走簡單路線:

  1. 拿每月的基礎設施帳單總額(例如 200$)。
  2. 除以每月的 workflow 數量workflow_completed)——得到近似的 infra_cost_per_task
  3. 或除以活躍使用者數——得到 infra_cost_per_user

然後把這些數字與我們用日誌詳算出的 LLM 成本相加——得到近似的總成本(按情境或按使用者)。

當應用長大後,可以做得更細(將費用按服務與工具分攤),但在前幾版做到這個程度,已足夠避免盲目行事。

9. GiftGenius 的一個小型端到端範例

把一切串成小故事。

使用者描述收禮者,ChatGPT 建議啟用 GiftGenius。接下來:

  1. 小工具啟動 workflow "gift_selection"
  2. 你的後端決定使用 LLM 代理,讓禮物推薦更聰明。
  3. 代理執行 3 個步驟:
  • analyze_recipient(用 LLM 分析描述)。
  • suggest_gifts(我們的 MCP 工具)。
  • rerank_gifts(用額外模型改善清單)。
  1. 使用者看到禮物卡片,並儲存了幾個點子。
  2. 點擊「購買」,啟動 ACP 與 checkout_create_session
  3. 成功的 checkout_success,金額 79.00 USD。

日誌中會留下:

  • 三個 tool_invocation(各自有 tokens_in/tokens_outcost_estimate_usdlatencyMs)。
  • 若干 agent_stepworkflow = "gift_selection",以及 step_name
  • checkout_startedcheckout_success,伴隨 amount=7900currency="USD"

透過 request_id 將這些關聯後,我們可以說:

  • 情境的 LLM 成本:三個工具的 cost_estimate_usd 之和,假設為 0.19$。
  • 基礎設施分攤約 0.03$ 每個 workflow。
  • 合計成本為 0.22$。
  • 交易營收——79$,再扣掉 Stripe 手續費等。

這已經是具體的單位經濟學,而不是「感覺 GPT‑4 很貴」。

10. 成本儀表化的常見錯誤

錯誤 #1:只看月度帳單,沒有細節粒度。
只看 OpenAI/雲的總帳單很誘人。但若沒有和 tool_nameuser_idworkflow 的關聯,你就不知道錢到底花在哪裡。結果優化變成「盲目降級模型」,而不是對昂貴情境做精準改善。

錯誤 #2:把 cost 數據寫進無結構的文字日誌。
"Tool suggest_gifts used 123 tokens" 這樣的行無法高品質地彙總與篩選。到某個時間點你會發現必須遷移到 JSON,而這個搬遷會很痛苦。從一開始就用結構化日誌,包含 request_idtool_nametokens_in/tokens_outcost_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 與「成本 ↔ 品質」實驗)中展開。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION