CodeGym /Các khóa học /ChatGPT Apps /Kiểm soát chi phí và cost instrumentation

Kiểm soát chi phí và cost instrumentation

ChatGPT Apps
Mức độ , Bài học
Có sẵn

1. Vì sao “hoạt động” ≠ “có lãi”

Ứng dụng LLM có một đặc thù quan trọng: ngoài chi phí cố định cho hosting, chúng thường xuất hiện thêm chi phí biến đổi cho việc thực thi một số request do các lần gọi model gây ra.

Cần phân biệt hai thế giới:

  • khi model chạy phía ChatGPT (người dùng tương tác với App của bạn trong ChatGPT và app gọi mcp-tools) — người dùng trả tiền token bằng gói ChatGPT của họ;
  • khi backend/MCP‑server của bạn tự gọi OpenAI API hoặc dịch vụ LLM khác — bạn là người trả tiền cho các token này.

Chính ở trường hợp thứ hai bạn có chi phí LLM biến đổi cổ điển, phụ thuộc vào số lượng và “độ nặng” (tokens_in/tokens_out) của các request.

Kịch bản kinh điển:

  1. Bạn vui vẻ đưa GiftGenius lên production, mọi thứ chạy vèo vèo, người dùng hạnh phúc.
  2. Một tháng sau hóa đơn OpenAI + cloud + phí Stripe kéo đến, và bất ngờ phát hiện “tăng trưởng thành công” thực ra là “mỗi món quà bán được thì ta trả nhiều hơn khoản thu về”.

Cách tiếp cận FinOps (FinOps) nói: chi phí cũng là một chỉ số như latency hay error‑rate. Cần ghi log, tổng hợp và ra quyết định dựa trên đó, chứ không “đoán trong Excel”.

Mục tiêu của bài này — để bạn có thể trả lời các câu hỏi kiểu:

  • “Cú chọn quà cụ thể này cho người dùng user42 đã tốn bao nhiêu tiền?”
  • “Trong tuần này tool suggest_gifts đốt bao nhiêu tiền và mang về bao nhiêu đơn hàng?”

Và để câu trả lời không từ trên trời rơi xuống, mà đến từ log và metric.

2. Cấu trúc chi phí của ChatGPT App

Bắt đầu từ bản đồ chi phí. Không có nó, mọi thứ khác chỉ là thu thập số liệu hỗn loạn.

Chi phí LLM (biến đổi)

Đây là tất cả những gì liên quan tới các lần gọi model từ backend của bạn:

  • Gọi các model OpenAI từ MCP‑server hoặc agent: GPT-5.1 / GPT-5-mini / embeddings / rerank / vision / TTS/STT, v.v.
  • Các model bổ sung: reranking cho tìm kiếm, embedding cho gợi ý, sinh ảnh.

Một điểm tinh tế: khi bạn xây giao diện qua Apps SDK và chỉ dùng model tích hợp sẵn của ChatGPT, bạn không trả tiền token — người dùng trả (qua gói ChatGPT của họ). Nhưng ngay khi MCP‑server của bạn tự gọi OpenAI API (Agents, Responses API, embeddings, v.v.), token sẽ tính vào tài khoản của bạn.

Ý tưởng cơ bản: chi phí các lần gọi như vậy tỷ lệ với tokens_intokens_out, nhân với giá trên mỗi token.

Việc gọi MCP‑tool tự nó là miễn phí cho developer về phía token; chi phí chỉ phát sinh ở chỗ handler của nó khi bạn quyết định gọi OpenAI API hoặc LLM khác.

Hạ tầng

Toàn bộ phần máy móc và dịch vụ xung quanh:

  • MCP‑server: Vercel / AWS / GCP / bare metal.
  • Agents (nếu chạy như dịch vụ riêng).
  • Cơ sở dữ liệu: Postgres/MySQL, vector DB, S3/kho đối tượng.
  • Cache: Redis/KeyDB.
  • Hàng đợi và worker: ví dụ, cho sinh nền, tính lại feed, v.v.

Các chi phí này thường cố định theo tháng (hoặc bậc thang), nên thường được tính từ dữ liệu tổng hợp của hóa đơn dịch vụ cloud, chứ không theo từng request.

Thanh toán và dịch vụ bên ngoài

GiftGenius có ACP/Stripe, vậy sẽ có:

  • Phí cho mỗi thanh toán thành công (Stripe tầm vài phần trăm + một khoản cố định).
  • Tổn thất do gian lận và chargeback.
  • Chi phí API bên ngoài: e‑mail / SMS / push‑thông báo, analytics bổ sung, v.v.

Ban đầu là rất nhỏ, nhưng khi mở rộng sẽ cảm nhận rõ, nên ít nhất ở mức log và báo cáo cũng nên tách riêng.

Bảng nhỏ để ghi nhớ

Danh mục Ví dụ Cách tính gần đúng
LLM GPT‑5.1, GPT‑5‑mini, embeddings, rerank
tokens_in/out × price_per_token
Hạ tầng MCP, Agents, DB, Redis, hàng đợi, CDN Chia hóa đơn nhà cung cấp theo lưu lượng/khoảng thời gian
Thanh toán và dịch vụ Stripe, e‑mail API, SMS, analytics Số lượng sự kiện × biểu phí/hoa hồng

Mục tiêu của chúng ta: gắn các danh mục này với sự kiện cụ thể trong hệ thống (gọi tool, workflow, checkout), chứ không chỉ nhìn các tổng số cuối tháng.

3. Lấy dữ liệu usage ở đâu: ba lớp

Để tính cost không phải “mỗi tháng một lần” mà theo thời gian thực, cần nhúng instrumentation vào code. Có ba chỗ tất cả.

MCP‑server: mỗi lần gọi tool

MCP‑server là điểm tự nhiên mà ChatGPT gọi các tool của bạn. Ở đây ta có thể:

  • Bắt khoảnh khắc bắt đầu/kết thúc lời gọi.
  • Đo duration_ms (hoặc latency_ms).
  • Thu thập token từ phản hồi OpenAI (nếu MCP gọi model của ta) hoặc ít nhất ước lượng.
  • Đặt user_id, tenant_id, request_id/trace_id để liên kết log.

Sơ đồ sự kiện log tool_invocation cho GiftGenius trông như sau:

{
  "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
}

Giờ là cùng nội dung dưới dạng type TypeScript và một đoạn code.

// 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,
  }));
}

Và đây là wrapper quanh handler của MCP‑tool (giả sử suggest_gifts).

// mcp/tools/suggestGifts.ts
export async function handleSuggestGifts(ctx: Context, input: Input) {
  const started = Date.now();

  const llmResult = await callGiftModel(input); // gọi OpenAI ở đây

  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;
}

Ngay cả khi ước lượng token “bằng mắt” dựa trên độ dài văn bản, điều đó vẫn tốt hơn là không có gì.

Tầng agent (Agents SDK): các bước workflow

Nếu bạn dùng Agents SDK, agent có thể tự gọi nhiều tool liên tiếp. Ở đây quan trọng là log ngữ cảnh của bước: agent đang cố giải quyết nhiệm vụ gì.

Ví dụ, ở mỗi lần gọi tool của agent‑runner, có thể thêm các trường workflow_namestep_name: “tìm ý tưởng”, “lọc theo ngân sách”, “chuẩn bị checkout”.

Điều này cho phép sau đó xây báo cáo không chỉ theo tool mà còn theo bước của kịch bản: biết đâu 80% chi phí đổ vào một “bước làm rõ bổ sung” vô ích nào đó.

Ví dụ một “hook” nhỏ quanh agent:

// 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,
  }));
}

Và sử dụng nó từ runner:

// agents/giftAgent.ts
logAgentStep({
  requestId: run.requestId,
  workflow: 'gift_selection',
  step: 'rank_candidates',
  toolName: 'rerank_gifts',
});

Commerce: checkout và tiền

Ở lớp commerce, chúng ta quan tâm đến các sự kiện:

  • checkout_started — bắt đầu mua hàng.
  • checkout_success — thanh toán thành công.
  • checkout_failed — lỗi với mã/loại.

Và cần gắn kèm:

  • amount, currency.
  • request_id cùng phiên với tool_invocation.

Khi đó ta có thể trả lời: “Giao dịch này khiến ta tốn N cent chi phí LLM và mang về M đô doanh thu”.

Ví dụ một handler đơn giản cho sự kiện 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. Log có cấu trúc cho cost (liên hệ với M17)

Điểm mấu chốt: không dùng các log văn bản “tự do” kiểu console.log("Tool suggest_gifts used 123 tokens"). Mọi thứ ở dạng JSON.

Trong module 17, ta đã thống nhất ghi log request ở dạng JSON với các trường cơ bản như request_id, user_id, tool_name, v.v. Giờ ta bổ sung các trường cost lên trên đó.

Các trường bắt buộc cần có trong log liên quan đến chi phí:

  • timestamp, level.
  • event (tool_invocation, agent_step, checkout_success, v.v.).
  • request_id, trace_id — để liên kết chuỗi sự kiện của một workflow.
  • user_id, tenant_id — để tổng hợp theo người dùng/đội.
  • tool_name / service.
  • tokens_in, tokens_out, cost_estimate_usd.
  • latency_ms, success/error_code.

Trong ví dụ, ta sẽ gọi trường chi phí là cost_estimate_usd (chi phí theo USD) và giữ nguyên tên này trong code và dashboard.

Chính cấu trúc này cho phép:

  • Xây các tổng hợp: cost_estimate_usd trung bình theo tool_name, theo user_id, theo workflow.
  • Liên hệ các request “đắt đỏ” với độ trễ tăng hoặc lỗi, và quyết định ưu tiên tối ưu phần nào trước.

Nếu bạn đã có logger.info({...}) cơ bản trong M17, thì thêm các trường cost không phải framework mới, chỉ là vài thuộc tính bổ sung trong object.

5. Cách ước tính LLM‑cost trong code

Công thức không hề đáng sợ. Ta chỉ cần đúng bậc độ lớn, không nhất thiết khớp hóa đơn đến từng cent.

Lấy usage từ phản hồi OpenAI

Khi MCP‑server của bạn gọi OpenAI Response API, thường nhận được object usage:

{
  "usage": {
    "prompt_tokens": 120,
    "completion_tokens": 350,
    "total_tokens": 470
  }
}

Dựa vào đó rất tiện để tính chi phí. Các model khác nhau có giá khác nhau cho 1M token vào/ra.

Hàm ước tính đơn giản bằng TypeScript:

// mcp/cost.ts
type Usage = { prompt_tokens?: number; completion_tokens?: number };

const PRICING = {
  inputPerMillion: 2.5,   // USD cho 1M token đầu vào, ví dụ
  outputPerMillion: 10.0, // cho token đầu ra
};

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)); // làm tròn nhẹ
}

Giá ở đây chỉ là ví dụ; giá thực tế bạn lấy từ bảng giá hiện hành của OpenAI và đưa vào config. Quan trọng là hàm này được gọi ở mỗi lần gọi tool, và kết quả đi vào trường cost_estimate_usd trong log.

Nếu không có usage

Đôi khi bạn dùng LLM bên thứ ba không trả usage, hoặc cần kiểm soát sơ bộ trước khi gọi thật. Khi đó có thể:

  • Ước lượng token bằng thư viện kiểu tiktoken hoặc tương đương cho model của bạn.
  • Lấy giá trị trung bình từ log lịch sử (median_tokens_in/median_tokens_out cho tool) và nhân với giá.

Đoạn mã “giả lập” để ước lượng độ dài:

// mcp/costEstimateFallback.ts
export function roughTokenEstimate(text: string): number {
  // Ước tính thô: 1 token ≈ 4 ký tự Latin
  return Math.ceil(text.length / 4);
}

Không phải rocket science, nhưng cho phép, chẳng hạn, không cho phép một prompt 200000 token vào gói rẻ.

6. Các metric chi phí chính

Log thu thập được là nguyên liệu. Giờ xem các tổng hợp nào là sống còn.

cost_per_tool_call

Là gì: chi phí trung bình cho một lần gọi của một tool cụ thể.

Tại sao:

  • Thấy được những tool đặc biệt tốn kém.
  • Có thể tìm “đắt và vô ích”: avg_cost_per_call cao và chuyển đổi thấp trong kịch bản.

Cách tính từ log:

  • Lấy log có event = "tool_invocation" trong giai đoạn.
  • Nhóm theo tool_name.
  • Với mỗi nhóm, tính avg(cost_estimate_usd) và có thể cả p95 (bách phân vị 95 của chi phí).

cost_per_successful_task (hoặc cost_per_workflow)

Task/workflow — là kịch bản hoàn tất ở cấp người dùng:

  • Trong GiftGenius có thể là “chọn quà + hiển thị thẻ + người dùng lưu N ý tưởng” hoặc “chọn quà → checkout → thanh toán thành công”.

Cách làm:

  • Khi kết thúc workflow, ghi sự kiện workflow_completed với request_id, workflow_name và cờ thành công.
  • Qua request_id, “kéo” tất cả tool_invocation của workflow đó và cộng cost_estimate_usd của chúng.

Như vậy ta có “chi phí cho một tác vụ thành công” — chìa khóa để hiểu giá vốn của kịch bản.

cost_per_user / cost_per_tenant

Với kịch bản B2B, thường có câu hỏi: “Một người dùng/một đội tốn của ta bao nhiêu mỗi tháng?”

Cách tính:

  • Nhóm tool_invocation và các sự kiện cost khác theo user_id hoặc tenant_id.
  • Cộng cost_estimate_usd theo giai đoạn (ngày, tháng).

Sau đó so với giá gói. Nếu cost_per_user tiến sát giá gói, đã đến lúc tăng giá hoặc tối ưu usage (sẽ bàn trong bài tiếp theo về pricing và thí nghiệm “chi phí ↔ chất lượng”).

7. Ví dụ: định dạng tool_invocation và dashboard cho GiftGenius

Giờ làm điều trong bài tập kế hoạch: thiết kế sự kiện log và một dashboard tối thiểu cho tools.

Định dạng sự kiện tool_invocation cho GiftGenius

Trước đó ta xem log tối thiểu cho MCP‑tool. Giờ hãy thiết kế một sự kiện tool_invocation chi tiết hơn, có thể dùng trong production và dashboard: ý tưởng cũ, chỉ thêm các trường cho service, lỗi và liên kết với model.

Đầu tiên — type trong 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;
}

Và helper tiện lợi:

// telemetry/emitToolInvocation.ts
export function emitToolInvocation(e: ToolInvocationEvent) {
  console.log(JSON.stringify(e));
  // Thực tế: gửi tới Logtail/Datadog/ELK, v.v.
}

Cho mỗi tool (ví dụ, suggest_gifts, rerank_gifts, fetch_catalog) ta thêm lời gọi emitToolInvocation ở cuối handler (hoặc trong khối finally để có log ngay cả khi lỗi).

Dashboard đơn giản theo công cụ

Bảng tối thiểu cho dashboard (ví dụ, trong Metabase / Grafana / bất kỳ BI nào):

Cột Mô tả
tool_name
Tên tool (suggest_gifts, checkout_create_session, …)
% traffic
Tỷ trọng các tool_invocation rơi vào tool này
avg_cost_per_call
Chi phí trung bình cho một lần gọi (từ cost_estimate_usd)
error_rate
Phần trăm sự kiện có success = false
avg_latency_ms
Độ trễ trung bình
avg_revenue_per_call
Doanh thu trung bình gắn với tool này (nếu có)

Thường hiển thị như sau: phía trên là bảng, phía dưới — một vài biểu đồ:

  • Bar chart: tool_name trên trục X, avg_cost_per_call trên trục Y.
  • Scatter plot: X = avg_cost_per_call, Y = error_rate hoặc conversion_to_checkout.

Các biểu đồ này giúp nhanh chóng tìm ứng viên tối ưu: đắt, chậm và không cho chuyển đổi — ưu tiên xử lý trước.

Liên kết cost với revenue được hỗ trợ bởi việc ta log checkout_* cùng request_id. Khi đó ta có thể tính avg_revenue_per_call bằng tổng doanh thu chia cho số lần gọi của tool trong các kịch bản có checkout_success.

8. Hạch toán chi phí hạ tầng (vừa đủ dùng)

Với chi phí LLM thì đẹp: mỗi lần gọi có token, có thể tính chi phí ngay trong log. Hạ tầng thì không dễ như vậy: bạn có hóa đơn hàng tháng cho Vercel, database, Redis, v.v.

Ban đầu có thể đi theo lối đơn giản:

  1. Lấy tổng hóa đơn hạ tầng trong tháng (giả sử 200$).
  2. Chia cho số lượng workflow trong tháng (workflow_completed) — thu được infra_cost_per_task xấp xỉ.
  3. Hoặc chia cho số người dùng hoạt động — infra_cost_per_user.

Sau đó cộng các con số này với LLM‑cost (ta đã tính chi tiết theo log) — thu được giá vốn xấp xỉ đầy đủ cho kịch bản hoặc người dùng.

Khi ứng dụng lớn dần, có thể tinh chỉnh (phân bổ chi phí theo service và tool), nhưng với các phiên bản đầu, như vậy là đủ để không đi trong bóng tối.

9. Ví dụ end‑to‑end nhỏ cho GiftGenius

Gộp mọi thứ lại thành một câu chuyện mini.

Người dùng mô tả người nhận quà, ChatGPT đề xuất bật GiftGenius. Tiếp theo:

  1. Widget khởi chạy workflow "gift_selection".
  2. Backend của bạn quyết định dùng LLM‑agent để chọn quà thông minh hơn.
  3. Agent thực hiện 3 bước:
  • analyze_recipient (phân tích mô tả bằng LLM).
  • suggest_gifts (MCP‑tool của chúng ta).
  • rerank_gifts (model bổ sung để cải thiện danh sách).
  1. Người dùng xem thẻ quà và lưu một vài ý tưởng.
  2. Nhấn “Mua”, ACP được kích hoạt và gọi checkout_create_session.
  3. checkout_success thành công với số tiền 79.00 USD.

Những gì còn lại trong log:

  • Ba tool_invocation (mỗi cái có tokens_in/tokens_out, cost_estimate_usd, latencyMs riêng).
  • Vài agent_step với workflow = "gift_selection", step_name.
  • checkout_startedcheckout_success với amount=7900, currency="USD".

Dựa vào request_id ta liên kết tất cả và có thể nói:

  • Chi phí LLM của kịch bản: tổng cost_estimate_usd của ba tool, giả sử 0.19$.
  • Tỷ phần hạ tầng (từ tổng hợp) khoảng 0.03$ cho một workflow.
  • Tổng cộng 0.22$ giá vốn.
  • Doanh thu giao dịch — 79$ trừ phí Stripe và các phần khác.

Đây đã là unit economics cụ thể, chứ không phải “có vẻ GPT‑4 đắt”.

10. Các lỗi thường gặp khi làm cost instrumentation

Lỗi số 1: chỉ tính hóa đơn tháng và không có độ chi tiết.
Rất dễ sa vào nhìn mỗi tổng hóa đơn từ OpenAI/cloud. Nhưng nếu không liên kết với tool_name, user_id, workflow thì bạn không biết tiền tiêu ở đâu. Cuối cùng tối ưu hóa biến thành “hạ cấp model mù quáng” thay vì cải thiện đúng chỗ các kịch bản đắt đỏ.

Lỗi số 2: ghi dữ liệu cost vào log văn bản không có cấu trúc.
Các dòng như "Tool suggest_gifts used 123 tokens" không thể tổng hợp và lọc tốt. Rồi sẽ đến lúc bạn phải chuyển sang JSON, và việc di cư này sẽ đau đớn. Hãy làm log có cấu trúc ngay với các trường request_id, tool_name, tokens_in/tokens_out, cost_estimate_usd.

Lỗi số 3: bỏ qua liên kết cost ↔ sự kiện commerce.
Ghi checkout_success mà không có request_id và liên hệ với các lần gọi tool — nghĩa là tự nguyện từ bỏ khả năng hiểu kịch bản nào sinh lời, kịch bản nào chỉ ngốn token. Đừng ngại truyền request_id xuyên suốt từ widget tới ACP.

Lỗi số 4: cố làm billing “hoàn hảo” thay vì ước tính thực dụng.
Một số đội ngập chìm trong nỗ lực tái hiện billing OpenAI hoàn hảo đến từng token. Thực tế chỉ cần đúng bậc độ lớn: kịch bản tốn 0.02$ hay 0.021$ không khác biệt mấy. Quan trọng là không phải 2$. Đừng ngại dùng các ước tính gần đúng qua usage hoặc thậm chí heuristic thô.

Lỗi số 5: chỉ nhìn cost và quên chất lượng.
Đôi khi, thấy các con số tiết kiệm đẹp, ta muốn chuyển hết sang model rẻ nhất. Như vậy có thể “tối ưu” ứng dụng đến mức người dùng bỏ đi. Chi phí phải được xem cùng với chất lượng trả lời và chuyển đổi — đây sẽ là chủ đề của bài giảng tiếp theo trong module này — về pricing và thí nghiệm “chi phí ↔ chất lượng”.

Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION