·

Một AI agent chạy sai vòng lặp (loop) trong 20 phút có thể đốt hết ngân sách token của cả một tuần. Đây không phải là chuyện hiếm — nó là kịch bản xảy ra gần như chắc chắn với bất kỳ team nào vận hành agentic system ở production mà không có một hệ thống giám sát chi phí (cost monitoring) đúng nghĩa. Khác với các API truyền thống có chi phí gần như hằng số cho mỗi request, chi phí của agent phụ thuộc vào số bước suy luận (reasoning steps), số lần gọi tool, độ dài context tích lũy qua nhiều turn, và hành vi retry khi gặp lỗi. Một thay đổi nhỏ trong prompt, một tool trả về dữ liệu lớn hơn dự kiến, hoặc một bug khiến agent lặp vô hạn — tất cả đều có thể khiến chi phí tăng vọt gấp 10-50 lần so với bình thường, và nếu không có cảnh báo (alerting) real-time, team thường chỉ phát hiện ra khi nhận invoice cuối tháng.

Bài học này đi sâu vào cách xây dựng một stack giám sát chi phí hoàn chỉnh cho hệ thống dùng LLM — từ việc capture từng API call, tổng hợp (aggregate) chúng thành metric có ý nghĩa, thiết lập cảnh báo real-time, đến việc dùng công cụ giám sát có sẵn từ các nhà cung cấp (provider-native monitoring tools) và xây dashboard, quy trình phản ứng khi có sự cố (incident response) chi phí tăng bất thường.

Toàn cảnh Stack Giám sát Chi phí Token

Trước khi đi vào chi tiết từng lớp, cần hiểu bức tranh tổng thể: một hệ thống giám sát chi phí trưởng thành (mature) gồm 3 lớp xếp chồng lên nhau, mỗi lớp giải quyết một vấn đề khác nhau.

  • Lớp 1 — Instrumentation (đo đạc/gắn cảm biến): Bắt (capture) mọi lời gọi API tới LLM provider, ghi lại số token input/output, model được dùng, latency, và chi phí ước tính cho từng call. Đây là nguồn dữ liệu thô (raw data), không có instrumentation thì mọi thứ phía sau đều là mù thông tin.
  • Lớp 2 — Aggregation (tổng hợp): Biến hàng triệu dòng log event thô thành các con số có ý nghĩa cho việc ra quyết định — chi phí theo giờ/ngày, theo feature, theo team, theo user, theo model.
  • Lớp 3 — Real-time Alerting (cảnh báo thời gian thực): Khi metric tổng hợp vượt ngưỡng (threshold) đã định, hệ thống phải tự động báo cho người có trách nhiệm (on-call) — qua Slack, PagerDuty, email — trước khi thiệt hại lan rộng.

Ba lớp này tương tự như observability stack truyền thống (logging → metrics → alerting) nhưng áp dụng riêng cho chi phí token — một loại "chỉ số nghiệp vụ" (business metric) cực kỳ nhạy với thay đổi hành vi của agent, chứ không chỉ là chỉ số hạ tầng (infra metric) như CPU hay RAM.

Một sai lầm phổ biến là các team build agent đầu tiên rồi mới nghĩ đến giám sát chi phí sau — lúc đó việc thêm instrumentation vào một codebase đã phức tạp hóa (nhiều tool call lồng nhau, nhiều model khác nhau) sẽ tốn công gấp nhiều lần so với thiết kế nó ngay từ đầu.

Mẹo

  • Coi cost monitoring là một phần của "definition of done" khi thiết kế agent mới, không phải là việc làm thêm (afterthought) sau khi đã có sự cố.
  • Đừng chỉ giám sát tổng chi phí (total spend) — hãy giám sát theo chiều (dimension): feature, user, model, environment (dev/staging/production). Tổng chi phí tăng có thể là do tăng traffic hợp lệ; chi phí tăng bất thường theo một dimension cụ thể mới là dấu hiệu của bug hoặc lạm dụng (abuse).
  • Bắt đầu đơn giản (một bảng PostgreSQL và vài dòng Python) trước khi nhảy sang các nền tảng observability đắt đỏ như Datadog hay Honeycomb — với hầu hết team vừa và nhỏ, một stack tự xây đủ nhẹ đã đáp ứng được 80% nhu cầu.

Lớp 1: Instrumentation — Bắt lại Từng Lời gọi API

Instrumentation là việc chèn code ghi log vào đúng điểm mà ứng dụng gọi tới LLM provider (OpenAI, Anthropic, Google, AWS Bedrock...). Mục tiêu là với MỖI lời gọi API, ta ghi lại đủ thông tin để tính được chi phí chính xác đến từng cent, và biết chính xác lời gọi đó thuộc về request/user/feature nào.

Thông tin tối thiểu cần ghi cho mỗi call:

  • Timestamp
  • Model được dùng (vì mỗi model có đơn giá khác nhau)
  • Số input token, output token, và token được cache (nếu provider hỗ trợ prompt caching)
  • Latency (thời gian phản hồi)
  • Request ID / Trace ID để liên kết với log ứng dụng
  • Metadata nghiệp vụ: user_id, feature_name, agent_step_id, environment
  • Trạng thái (thành công/lỗi/retry)

Cách làm sai phổ biến nhất: nhiều team dựa vào dashboard "Usage" có sẵn của provider để lấy số liệu chi phí. Vấn đề là dashboard đó thường tổng hợp theo API key hoặc theo project, KHÔNG biết được request nào thuộc feature nào, user nào — nên vô dụng khi cần debug "tại sao chi phí feature X tăng gấp 3". Vì vậy, instrumentation ở tầng ứng dụng (application-level) là bắt buộc, dashboard của provider chỉ nên dùng để đối chiếu (cross-check) số liệu tổng.

Universal API Wrapper

Cách hiệu quả nhất để đảm bảo KHÔNG có lời gọi LLM nào "lọt lưới" không được ghi log là bắt buộc mọi lời gọi phải đi qua một wrapper class thống nhất, thay vì gọi SDK của provider trực tiếp rải rác khắp codebase. Dưới đây là một wrapper mẫu, viết bằng Python, hỗ trợ nhiều provider và tự động tính chi phí dựa trên bảng giá:

import time
import uuid
import logging
from dataclasses import dataclass, field
from typing import Optional

logger = logging.getLogger("llm_cost_tracker")

PRICING_TABLE = {
    "gpt-4o": {"input": 2.50, "output": 10.00, "cached_input": 1.25},
    "gpt-4o-mini": {"input": 0.15, "output": 0.60, "cached_input": 0.075},
    "claude-sonnet-5": {"input": 3.00, "output": 15.00, "cached_input": 0.30},
    "claude-haiku-4.5": {"input": 0.80, "output": 4.00, "cached_input": 0.08},
    "gemini-2.5-pro": {"input": 1.25, "output": 5.00, "cached_input": 0.31},
}


@dataclass
class UsageEvent:
    request_id: str
    timestamp: float
    provider: str
    model: str
    input_tokens: int
    output_tokens: int
    cached_tokens: int
    latency_ms: float
    status: str
    user_id: Optional[str] = None
    feature_name: Optional[str] = None
    agent_step_id: Optional[str] = None
    environment: str = "production"
    cost_usd: float = field(default=0.0)

    def compute_cost(self) -> float:
        rates = PRICING_TABLE.get(self.model)
        if not rates:
            logger.warning(f"No pricing entry for model={self.model}, cost=0")
            return 0.0
        billable_input = self.input_tokens - self.cached_tokens
        cost = (
            billable_input / 1_000_000 * rates["input"]
            + self.cached_tokens / 1_000_000 * rates.get("cached_input", rates["input"])
            + self.output_tokens / 1_000_000 * rates["output"]
        )
        return round(cost, 6)


class LLMCostTracker:
    """Universal wrapper — every LLM call in the codebase MUST go through
    this class so no usage event is ever missed."""

    def __init__(self, sink):
        # sink: object with a `.write(UsageEvent)` method — e.g. writes
        # to Postgres, Kafka, or a local queue for batch flushing.
        self.sink = sink

    def call(
        self,
        provider_client,
        model: str,
        messages: list,
        user_id: str = None,
        feature_name: str = None,
        agent_step_id: str = None,
        **kwargs,
    ):
        request_id = str(uuid.uuid4())
        start = time.perf_counter()
        status = "success"
        response = None
        try:
            response = provider_client.chat.completions.create(
                model=model, messages=messages, **kwargs
            )
        except Exception as exc:
            status = "error"
            raise
        finally:
            latency_ms = (time.perf_counter() - start) * 1000
            usage = getattr(response, "usage", None) if response else None
            event = UsageEvent(
                request_id=request_id,
                timestamp=time.time(),
                provider="openai",
                model=model,
                input_tokens=getattr(usage, "prompt_tokens", 0) if usage else 0,
                output_tokens=getattr(usage, "completion_tokens", 0) if usage else 0,
                cached_tokens=getattr(usage, "cached_tokens", 0) if usage else 0,
                latency_ms=latency_ms,
                status=status,
                user_id=user_id,
                feature_name=feature_name,
                agent_step_id=agent_step_id,
            )
            event.cost_usd = event.compute_cost()
            self.sink.write(event)  # non-blocking; queue and flush in batch
        return response

Điểm mấu chốt của thiết kế này: finally block đảm bảo usage event được ghi lại NGAY CẢ KHI call bị lỗi (vì một số lỗi vẫn phát sinh chi phí, ví dụ lỗi timeout sau khi provider đã xử lý xong). Bảng PRICING_TABLE nên được externalize (đưa ra ngoài, ví dụ config file hoặc bảng DB) và có job tự động đồng bộ khi provider công bố giá mới, tránh tình trạng tính sai chi phí do bảng giá lỗi thời (điều này xảy ra rất thường xuyên với các team không có ai chịu trách nhiệm theo dõi bảng giá).

Mẹo

  • Đặt wrapper này ở vị trí trung tâm (ví dụ một shared library nội bộ) và enforce bằng lint rule hoặc code review checklist — cấm gọi trực tiếp openai.chat.completions.create hay anthropic.messages.create ngoài wrapper.
  • Ghi log cả những call bị hủy giữa đường (ví dụ do timeout phía client) — token đã được provider xử lý trước khi timeout vẫn tính phí.
  • Với agent nhiều bước, gắn agent_step_id và một trace_id chung cho cả phiên chạy (session) để có thể roll-up chi phí toàn bộ vòng lặp agent, không chỉ từng call riêng lẻ.
  • Đừng viết log trực tiếp (blocking) vào database trong cùng request — dùng queue (Kafka, Redis Stream, hoặc đơn giản là async batch insert) để việc ghi log không làm chậm response tới người dùng.

Lớp 2: Aggregation — Biến Raw Event thành Metric Có Ý nghĩa

Có hàng triệu dòng UsageEvent mỗi ngày không giúp ích gì nếu không tổng hợp được. Lớp aggregation chịu trách nhiệm trả lời các câu hỏi nghiệp vụ: "Chi phí giờ này là bao nhiêu?", "Feature nào đang đốt tiền nhiều nhất?", "User nào có pattern bất thường?".

Có hai chiến lược aggregation phổ biến:

  1. Batch aggregation: Chạy job định kỳ (mỗi 5-15 phút) tổng hợp dữ liệu từ bảng raw events thành các bảng rollup (theo giờ, theo ngày). Đơn giản, dễ maintain, phù hợp cho dashboard và báo cáo, nhưng có độ trễ (latency) vài phút.
  2. Streaming aggregation: Dùng Kafka Streams, Flink, hoặc materialized view trong ClickHouse/TimescaleDB để có số liệu gần real-time (dưới 1 phút). Phức tạp hơn, nhưng cần thiết nếu muốn cảnh báo sự cố trong vòng vài giây/phút thay vì vài chục phút.

Với đa số team, batch aggregation mỗi 5 phút kết hợp với PostgreSQL là đủ tốt — chỉ nên đầu tư vào streaming khi đã chứng minh được rằng độ trễ 5 phút thực sự gây thiệt hại đáng kể (ví dụ agent có khả năng đốt hàng nghìn đô trong vài phút).

Aggregation với PostgreSQL

Giả sử bảng raw events tên llm_usage_events được insert liên tục từ LLMCostTracker ở trên. Ta có thể dùng PostgreSQL với extension pg_cron (hoặc một scheduler bên ngoài như Airflow/cron) để định kỳ tổng hợp dữ liệu vào bảng rollup:

-- Raw event table (append-only, partitioned by day for easy retention management)
CREATE TABLE llm_usage_events (
    request_id      UUID PRIMARY KEY,
    ts              TIMESTAMPTZ NOT NULL,
    provider        TEXT NOT NULL,
    model           TEXT NOT NULL,
    input_tokens    INTEGER NOT NULL,
    output_tokens   INTEGER NOT NULL,
    cached_tokens   INTEGER NOT NULL DEFAULT 0,
    cost_usd        NUMERIC(12, 6) NOT NULL,
    latency_ms      NUMERIC(10, 2),
    status          TEXT NOT NULL,
    user_id         TEXT,
    feature_name    TEXT,
    agent_step_id   TEXT,
    environment     TEXT NOT NULL DEFAULT 'production'
) PARTITION BY RANGE (ts);

-- Hourly rollup table used by dashboards and alerting jobs
CREATE TABLE llm_usage_hourly (
    hour_bucket     TIMESTAMPTZ NOT NULL,
    feature_name    TEXT NOT NULL,
    model           TEXT NOT NULL,
    environment     TEXT NOT NULL,
    call_count      BIGINT NOT NULL,
    error_count     BIGINT NOT NULL,
    total_input_tokens  BIGINT NOT NULL,
    total_output_tokens BIGINT NOT NULL,
    total_cost_usd  NUMERIC(14, 4) NOT NULL,
    avg_latency_ms  NUMERIC(10, 2),
    PRIMARY KEY (hour_bucket, feature_name, model, environment)
);

-- Aggregation query, run every 5 minutes for the current + previous hour bucket
INSERT INTO llm_usage_hourly
SELECT
    date_trunc('hour', ts)                          AS hour_bucket,
    COALESCE(feature_name, 'unknown')                AS feature_name,
    model,
    environment,
    COUNT(*)                                         AS call_count,
    COUNT(*) FILTER (WHERE status = 'error')          AS error_count,
    SUM(input_tokens)                                 AS total_input_tokens,
    SUM(output_tokens)                                AS total_output_tokens,
    SUM(cost_usd)                                      AS total_cost_usd,
    AVG(latency_ms)                                    AS avg_latency_ms
FROM llm_usage_events
WHERE ts >= date_trunc('hour', now()) - INTERVAL '1 hour'
  AND ts <  date_trunc('hour', now()) + INTERVAL '1 hour'
GROUP BY 1, 2, 3, 4
ON CONFLICT (hour_bucket, feature_name, model, environment)
DO UPDATE SET
    call_count = EXCLUDED.call_count,
    error_count = EXCLUDED.error_count,
    total_input_tokens = EXCLUDED.total_input_tokens,
    total_output_tokens = EXCLUDED.total_output_tokens,
    total_cost_usd = EXCLUDED.total_cost_usd,
    avg_latency_ms = EXCLUDED.avg_latency_ms;

Query này chạy re-aggregation cho khung giờ hiện tại và giờ trước (để bắt các event đến muộn do network delay), sử dụng ON CONFLICT ... DO UPDATE để tránh trùng lặp dữ liệu (idempotent). Từ bảng llm_usage_hourly, việc trả lời "chi phí feature checkout-agent trong 24h qua" chỉ còn là một SUM() đơn giản với index tốt, thay vì phải scan hàng triệu dòng raw event.

Nên kết hợp thêm một materialized view "burn rate" (tốc độ đốt tiền) so sánh giờ hiện tại với trung bình 7 ngày trước cùng khung giờ đó — đây chính là input cho lớp alerting ở phần sau.

Mẹo

  • Partition bảng raw event theo ngày (PARTITION BY RANGE (ts)) để việc xóa dữ liệu cũ (retention) chỉ là DROP PARTITION, nhanh hơn hẳn DELETE hàng loạt.
  • Luôn tổng hợp lại (re-aggregate) khung giờ trước, không chỉ khung giờ hiện tại — event có thể đến muộn vài giây đến vài phút do hàng đợi (queue) hoặc network.
  • Giữ raw events tối đa 30-90 ngày (đủ để debug/audit), nhưng giữ bảng rollup vô thời hạn — dữ liệu rollup nhẹ hơn hàng trăm lần và cần cho phân tích xu hướng dài hạn.
  • Nếu volume quá lớn cho PostgreSQL (hàng chục triệu event/ngày), hãy xem xét ClickHouse — nó được thiết kế riêng cho dạng truy vấn analytics tổng hợp này và có thể nhanh hơn PostgreSQL 10-50 lần ở quy mô lớn.

Lớp 3: Cảnh báo Real-time (Real-time Alerting)

Có metric tổng hợp là chưa đủ — nếu không ai chủ động nhìn dashboard 24/7, một sự cố có thể âm thầm đốt tiền hàng giờ trước khi bị phát hiện. Alerting là lớp biến metric thành hành động: khi một điều kiện xấu xảy ra, hệ thống tự động thông báo cho người phụ trách qua kênh phù hợp với mức độ nghiêm trọng.

Các loại Ngưỡng Cảnh báo (Alert Threshold Types)

Có 4 loại ngưỡng cảnh báo nên được thiết lập song song, vì mỗi loại bắt được một dạng sự cố khác nhau:

  1. Absolute threshold (ngưỡng tuyệt đối): Cảnh báo khi chi phí trong một khung thời gian vượt một số tiền cố định, ví dụ "chi phí giờ này > $500". Đơn giản, dễ hiểu, nhưng cần điều chỉnh thủ công khi traffic tăng trưởng theo thời gian.
  2. Rate-of-change threshold (ngưỡng tốc độ tăng): Cảnh báo khi chi phí tăng đột biến so với baseline, ví dụ "chi phí giờ này cao hơn 300% so với trung bình cùng giờ 7 ngày trước". Loại này nhạy với sự cố bất thường (anomaly) hơn absolute threshold vì nó tự điều chỉnh theo pattern traffic bình thường.
  3. Budget burn-rate threshold (ngưỡng tốc độ tiêu ngân sách): Cảnh báo khi tốc độ tiêu ngân sách tháng hiện tại dự báo (projected) sẽ vượt ngân sách trước khi hết tháng — ví dụ "với tốc độ hiện tại, ngân sách tháng sẽ cạn vào ngày 18, còn 12 ngày nữa mới hết tháng".
  4. Per-entity anomaly threshold (ngưỡng bất thường theo thực thể): Cảnh báo khi MỘT user, MỘT feature, hoặc MỘT agent session cụ thể có chi phí vượt xa mức bình thường của chính nó — ví dụ "user X thường tốn $0.05/ngày, hôm nay đã tốn $40". Loại này bắt được lỗi loop vô hạn hoặc hành vi lạm dụng (abuse) mà 2 loại trên có thể không phát hiện vì tổng thể vẫn "bình thường".

Việc chỉ dùng một loại ngưỡng, đặc biệt chỉ dùng absolute threshold, là nguyên nhân phổ biến khiến team bị "bất ngờ" — vì absolute threshold đặt quá cao sẽ bỏ lỡ sự cố nhỏ nhưng dai dẳng, còn đặt quá thấp sẽ gây "alert fatigue" (mệt vì cảnh báo giả liên tục) khiến người phụ trách bắt đầu bỏ qua cảnh báo.

Triển khai Cảnh báo với Slack và PagerDuty

Mức độ nghiêm trọng khác nhau nên đi tới kênh khác nhau: cảnh báo thông tin (informational) đi Slack channel để mọi người theo dõi, cảnh báo nghiêm trọng (critical) cần đánh thức người on-call thì phải qua PagerDuty (hoặc Opsgenie). Dưới đây là một ví dụ trigger logic kết hợp cả 4 loại ngưỡng, chạy như một job định kỳ mỗi 5 phút:

import requests
from datetime import datetime, timedelta

SLACK_WEBHOOK_URL = "https://hooks.slack.com/services/T000/B000/XXXXXXXX"
PAGERDUTY_ROUTING_KEY = "your-pagerduty-integration-key"

def evaluate_cost_alerts(db_conn, monthly_budget_usd: float):
    current_hour_cost = get_current_hour_cost(db_conn)
    baseline_avg = get_avg_cost_same_hour_last_7_days(db_conn)
    projected_month_end = project_month_end_spend(db_conn)
    anomalous_entities = detect_per_entity_anomalies(db_conn, z_score_threshold=4.0)

    alerts = []

    # 1. Absolute threshold
    if current_hour_cost > 500:
        alerts.append(("critical", f"Hourly spend ${current_hour_cost:.2f} exceeded $500 absolute cap"))

    # 2. Rate-of-change threshold
    if baseline_avg > 0 and current_hour_cost > baseline_avg * 3:
        pct = (current_hour_cost / baseline_avg - 1) * 100
        alerts.append(("warning", f"Hourly spend is {pct:.0f}% above 7-day baseline (${baseline_avg:.2f})"))

    # 3. Budget burn-rate threshold
    if projected_month_end > monthly_budget_usd * 1.10:
        alerts.append(("critical", f"Projected month-end spend ${projected_month_end:.2f} exceeds budget ${monthly_budget_usd:.2f} by >10%"))

    # 4. Per-entity anomaly threshold
    for entity in anomalous_entities:
        alerts.append((
            "warning",
            f"Entity `{entity['id']}` ({entity['type']}) spent ${entity['cost_today']:.2f} "
            f"today vs. its usual ${entity['baseline']:.2f} (z-score={entity['z_score']:.1f})",
        ))

    for severity, message in alerts:
        dispatch_alert(severity, message)

    return alerts


def dispatch_alert(severity: str, message: str):
    if severity == "critical":
        # Page the on-call engineer immediately
        requests.post(
            "https://events.pagerduty.com/v2/enqueue",
            json={
                "routing_key": PAGERDUTY_ROUTING_KEY,
                "event_action": "trigger",
                "payload": {
                    "summary": f"[LLM Cost] {message}",
                    "severity": "critical",
                    "source": "llm-cost-monitor",
                },
            },
            timeout=5,
        )
    # Every alert, regardless of severity, also goes to the Slack channel
    emoji = "🚨" if severity == "critical" else "⚠️"
    requests.post(
        SLACK_WEBHOOK_URL,
        json={"text": f"{emoji} *LLM Cost Alert* [{severity.upper()}]\n{message}"},
        timeout=5,
    )

Lưu ý pattern quan trọng: mọi alert đều được post lên Slack để có audit trail đầy đủ, nhưng chỉ mức critical mới trigger PagerDuty để tránh làm phiền on-call với những cảnh báo chưa cần hành động ngay. Đây gọi là "tiered alerting" (cảnh báo phân tầng) — một thực hành chuẩn trong incident response nói chung, không riêng gì cost monitoring.

Mẹo

  • Luôn có cơ chế "cooldown" (thời gian nghỉ giữa 2 lần bắn cùng loại alert) — ví dụ không bắn lại cùng alert trong 30 phút — để tránh spam khi điều kiện vẫn còn đúng qua nhiều lần chạy job.
  • Đưa đường link trực tiếp tới dashboard/query liên quan vào nội dung alert, để người nhận không phải tự tìm — mỗi giây tiết kiệm được khi debug sự cố đều có giá trị.
  • Review và điều chỉnh threshold định kỳ (hàng tháng) dựa trên traffic thực tế — threshold đặt cứng một lần rồi bỏ quên sẽ dần trở nên vô nghĩa khi hệ thống tăng trưởng.
  • Với alert "critical", luôn kèm theo hành động gợi ý (runbook link) — ví dụ "kiểm tra feature X, nếu cần hãy tắt bằng feature flag Y" — để người on-call phản ứng nhanh hơn thay vì mất thời gian điều tra từ đầu.

Công cụ Giám sát Có sẵn từ Nhà Cung cấp (Provider-Native Monitoring Tools)

Bên cạnh hệ thống tự xây, mỗi provider LLM lớn đều cung cấp sẵn công cụ giám sát usage và chi phí ở cấp độ tài khoản/project. Những công cụ này không thể thay thế instrumentation ở tầng ứng dụng (vì thiếu context nghiệp vụ), nhưng rất hữu ích để đối chiếu số liệu (cross-check) và làm lớp phòng vệ thứ hai — đặc biệt quan trọng vì nó phản ánh đúng những gì provider THỰC SỰ tính tiền, độc lập với logic tính toán (có thể có bug) trong wrapper tự viết.

OpenAI Usage Dashboard

OpenAI cung cấp trang Usage trong platform.openai.com, cho phép xem chi phí theo ngày, theo model, và quan trọng nhất là tính năng "Usage limits" — có thể đặt hard limit và soft limit theo tháng cho mỗi project/API key. Khi soft limit bị vượt, OpenAI tự gửi email cảnh báo; khi hard limit bị vượt, API key sẽ bị từ chối request mới — đây là một lớp "circuit breaker" (bộ ngắt tự động) hữu ích để chặn thiệt hại tối đa trong trường hợp xấu nhất, dù nó cũng có thể gây downtime nếu limit đặt quá thấp.

Nên tổ chức theo nhiều project riêng biệt trong OpenAI platform (mỗi project = một environment hoặc một team), vì Usage dashboard chỉ break-down được theo project/key, không có khái niệm "feature" hay "user" như hệ thống tự xây.

Anthropic Console

Anthropic Console cung cấp trang "Usage & Cost" theo Workspace, cho phép filter theo model, theo API key, và xem biểu đồ token theo ngày/giờ. Anthropic cũng hỗ trợ tạo nhiều Workspace trong một Organization — mỗi Workspace có thể có budget riêng và API key riêng, giúp cách ly (isolate) chi phí giữa các team hoặc environment mà không cần tài khoản riêng biệt. Console cũng hiển thị rõ tỷ lệ token được hưởng prompt caching discount, một chỉ số quan trọng để đánh giá hiệu quả của các kỹ thuật tối ưu context đã áp dụng ở các module trước.

Google Cloud Billing + Vertex AI

Với Gemini qua Vertex AI, chi phí được tính vào Google Cloud Billing chung, nên có thể tận dụng toàn bộ hệ sinh thái billing của GCP: Budgets & Alerts, BigQuery Billing Export, và Cloud Monitoring. Có thể truy vấn usage trực tiếp qua gcloud để lấy dữ liệu đưa vào hệ thống tự xây:

gcloud logging read \
  'resource.type="aiplatform.googleapis.com/Endpoint"
   AND protoPayload.methodName="google.cloud.aiplatform.v1.PredictionService.GenerateContent"' \
  --project=my-gcp-project \
  --freshness=1d \
  --format=json > vertex_usage_today.json

gcloud billing budgets create \
  --billing-account=012345-6789AB-CDEF01 \
  --display-name="vertex-ai-monthly-cap" \
  --budget-amount=5000 \
  --threshold-rule=percent=0.8 \
  --threshold-rule=percent=1.0 \
  --filter-labels=service=aiplatform.googleapis.com

Điểm mạnh của cách này là budget alert đi kèm sẵn với Pub/Sub notification, có thể nối trực tiếp vào Cloud Function để tự động thực hiện hành động (ví dụ tự động giảm quota, hoặc gửi tiếp vào Slack) mà không cần polling.

AWS Bedrock + Cost Explorer

Với AWS Bedrock, chi phí LLM nằm chung trong AWS Cost Explorer, filter theo service Amazon Bedrock. AWS Budgets cho phép thiết lập ngân sách và cảnh báo tương tự GCP, và có thể tích hợp với AWS Chatbot để bắn cảnh báo trực tiếp vào Slack channel không cần code trung gian:

aws budgets create-budget \
  --account-id 123456789012 \
  --budget '{
    "BudgetName": "bedrock-monthly-budget",
    "BudgetLimit": {"Amount": "3000", "Unit": "USD"},
    "TimeUnit": "MONTHLY",
    "BudgetType": "COST",
    "CostFilters": {"Service": ["Amazon Bedrock"]}
  }' \
  --notifications-with-subscribers '[
    {
      "Notification": {
        "NotificationType": "ACTUAL",
        "ComparisonOperator": "GREATER_THAN",
        "Threshold": 80
      },
      "Subscribers": [
        {"SubscriptionType": "SNS", "Address": "arn:aws:sns:us-east-1:123456789012:cost-alerts"}
      ]
    }
  ]'

Bedrock cũng có CloudWatch metrics riêng (InputTokenCount, OutputTokenCount) theo model ID, cho phép dựng alarm CloudWatch trực tiếp trên số lượng token thay vì phải chờ dữ liệu billing (thường có độ trễ 8-24 giờ) — hữu ích khi cần phát hiện bất thường nhanh hơn.

Mẹo

  • Luôn dùng công cụ provider-native như một "second opinion" — nếu số liệu từ hệ thống tự xây và dashboard provider lệch nhau quá 5-10%, đó là dấu hiệu bug trong bảng giá hoặc logic tính chi phí tự viết.
  • Đặt hard limit ở provider dashboard làm lớp bảo vệ cuối cùng (last resort), nhưng đừng phụ thuộc vào nó làm cơ chế cảnh báo chính — độ trễ billing của nhiều provider có thể lên tới nhiều giờ.
  • Với tổ chức dùng nhiều provider (multi-provider), tổng hợp số liệu từ tất cả nguồn provider-native vào một chỗ (ví dụ BigQuery hoặc data warehouse chung) để có view tổng thể, tránh phải mở 3-4 dashboard khác nhau mỗi khi cần biết tổng chi phí.
  • Đăng ký export billing data dạng file/API (BigQuery Billing Export của GCP, Cost and Usage Report của AWS) ngay từ đầu — việc backfill dữ liệu lịch sử sau này thường không khả thi hoặc rất hạn chế.

Xây dựng Dashboard Chi phí

Một dashboard chi phí tốt phải trả lời được câu hỏi "có gì bất thường không" trong vòng 10 giây nhìn lướt qua, và cho phép drill-down (đi sâu) trong vòng 2-3 lần click khi cần điều tra chi tiết. Grafana là lựa chọn phổ biến vì kết nối được trực tiếp với PostgreSQL/ClickHouse nơi lưu bảng rollup đã build ở Lớp 2.

Các Panel Chính (Key Panels)

Một dashboard chi phí đầy đủ nên có tối thiểu các panel sau, sắp xếp theo thứ tự ưu tiên xem:

  1. Big number — Chi phí hôm nay & tháng này: So sánh với cùng kỳ trước (ngày trước/tháng trước) để thấy xu hướng tức thì.
  2. Time series — Chi phí theo giờ (7 ngày gần nhất): Biểu đồ đường, có thể chồng thêm đường "baseline dự kiến" để phát hiện anomaly bằng mắt.
  3. Breakdown theo feature/model — Bar chart hoặc pie chart: Xác định ngay top 3-5 feature/model chiếm chi phí lớn nhất.
  4. Bảng Top N user/session bất thường: Liệt kê các entity có chi phí cao nhất trong 24h, kèm so sánh với baseline của chính entity đó.
  5. Gauge — Budget burn rate: Hiển thị % ngân sách tháng đã dùng so với % thời gian đã trôi qua trong tháng — nếu 60% ngân sách đã dùng nhưng mới 40% thời gian tháng đã qua, đó là dấu hiệu cảnh báo sớm.
  6. Error rate & retry rate: Vì lỗi và retry thường là nguyên nhân gốc của chi phí bất thường (một request lỗi bị retry nhiều lần vẫn tốn token mỗi lần).

Ví dụ Grafana Dashboard JSON (Các Query Mẫu)

Dưới đây là ví dụ cấu hình 2 panel tiêu biểu trong file JSON model của Grafana, dùng PostgreSQL datasource trỏ tới bảng llm_usage_hourly:

{
  "panels": [
    {
      "title": "Hourly Spend — Last 7 Days vs Baseline",
      "type": "timeseries",
      "datasource": { "type": "postgres", "uid": "cost-db" },
      "targets": [
        {
          "refId": "A",
          "rawSql": "SELECT hour_bucket AS time, SUM(total_cost_usd) AS actual_spend FROM llm_usage_hourly WHERE hour_bucket >= now() - interval '7 days' GROUP BY 1 ORDER BY 1;",
          "format": "time_series"
        },
        {
          "refId": "B",
          "rawSql": "SELECT hour_bucket AS time, AVG(total_cost_usd) OVER (PARTITION BY extract(hour from hour_bucket) ORDER BY hour_bucket ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS rolling_baseline FROM llm_usage_hourly WHERE hour_bucket >= now() - interval '7 days' ORDER BY 1;",
          "format": "time_series"
        }
      ]
    },
    {
      "title": "Top 10 Costliest Features (Last 24h)",
      "type": "bargauge",
      "datasource": { "type": "postgres", "uid": "cost-db" },
      "targets": [
        {
          "refId": "A",
          "rawSql": "SELECT feature_name, SUM(total_cost_usd) AS cost_24h FROM llm_usage_hourly WHERE hour_bucket >= now() - interval '24 hours' GROUP BY feature_name ORDER BY cost_24h DESC LIMIT 10;",
          "format": "table"
        }
      ]
    },
    {
      "title": "Monthly Budget Burn Rate",
      "type": "gauge",
      "datasource": { "type": "postgres", "uid": "cost-db" },
      "targets": [
        {
          "refId": "A",
          "rawSql": "SELECT (SUM(total_cost_usd) / 10000.0) * 100 AS pct_of_budget FROM llm_usage_hourly WHERE hour_bucket >= date_trunc('month', now());",
          "format": "table"
        }
      ],
      "fieldConfig": {
        "defaults": { "min": 0, "max": 100, "unit": "percent" }
      }
    }
  ]
}

Panel "Hourly Spend vs Baseline" dùng window function AVG() OVER (PARTITION BY ...) để tính baseline theo cùng khung giờ trong 7 ngày, cho phép nhìn thấy ngay đường "actual" tách khỏi đường "baseline" khi có bất thường — đây chính là biểu diễn trực quan của rate-of-change threshold đã nói ở Lớp 3.

Mẹo

  • Đặt dashboard cost monitoring ở vị trí dễ thấy nhất trong Grafana (ví dụ gắn vào trang chủ team hoặc TV màn hình lớn ở khu vực dev) — thói quen "nhìn qua mỗi ngày" phát hiện được nhiều bất thường hơn cả hệ thống alerting.
  • Dùng Grafana annotation để đánh dấu các mốc thời gian deploy — khi thấy chi phí tăng vọt, việc nhìn thấy ngay "à, đúng lúc deploy version mới" rút ngắn thời gian điều tra đáng kể.
  • Cho phép filter theo environment (dev/staging/production) ở template variable cấp dashboard, để tránh nhầm lẫn chi phí test với chi phí thật.
  • Thiết lập TV mode hoặc auto-refresh 1-5 phút cho các panel critical, nhưng đừng auto-refresh quá nhanh (dưới 30s) — sẽ tạo tải không cần thiết lên database.

Quy trình Phản ứng Sự cố Chi phí (Incident Response cho Cost Spikes)

Có alerting tốt vẫn chưa đủ nếu không có quy trình rõ ràng để xử lý khi cảnh báo bắn ra. Một incident response process cho cost spike nên theo các bước sau, tương tự runbook xử lý incident kỹ thuật thông thường nhưng có đặc thù riêng của chi phí LLM:

  1. Xác nhận (Triage): Khi nhận alert, việc đầu tiên là xác nhận đây là spike thật hay false positive (ví dụ do một chiến dịch marketing hợp lệ làm tăng traffic). Kiểm tra dashboard breakdown theo feature/model để khoanh vùng nguồn gốc.
  2. Cách ly (Containment): Nếu xác định là bất thường thật (bug loop, abuse, hoặc lỗi cấu hình), hành động đầu tiên KHÔNG phải là debug ngay mà là chặn thiệt hại tiếp diễn — dùng feature flag để tắt tính năng liên quan, giảm rate limit cho user/API key nghi vấn, hoặc trong trường hợp nghiêm trọng, revoke API key tạm thời.
  3. Điều tra nguyên nhân gốc (Root cause): Sau khi đã chặn được thiệt hại, mới đi sâu điều tra — xem log của agent_step_id/trace_id liên quan để hiểu chính xác chuỗi hành động dẫn tới việc đốt token bất thường (ví dụ agent bị stuck trong vòng lặp retry-tool-call vì tool trả lỗi liên tục).
  4. Khắc phục & Khôi phục (Remediation): Deploy fix (thường là thêm giới hạn số vòng lặp tối đa, sửa lỗi tool, hoặc điều chỉnh prompt), sau đó khôi phục lại tính năng đã tắt ở bước containment.
  5. Post-mortem không đổ lỗi (Blameless post-mortem): Ghi lại timeline, chi phí thiệt hại thực tế, nguyên nhân gốc, và quan trọng nhất — action item cụ thể để ngăn tái diễn (ví dụ thêm một loại threshold mới, thêm circuit breaker ở tầng code cho tool cụ thể đó).

Một chi tiết dễ bị bỏ qua: cần có sẵn "kill switch" (cơ chế tắt khẩn cấp) được chuẩn bị TRƯỚC khi sự cố xảy ra, không phải viết code tắt tính năng ngay lúc đang cháy nhà. Feature flag để bật/tắt từng agent workflow, hoặc circuit breaker tự động tắt một tool sau N lần lỗi liên tiếp, nên là một phần thiết kế chuẩn của mọi hệ thống agentic ở production, y hệt như health check hay retry logic.

Cũng nên định nghĩa rõ ràng "mức độ nghiêm trọng" (severity level) cho cost incident, tương tự như incident kỹ thuật: SEV1 khi thiệt hại dự kiến vượt một ngưỡng lớn (ví dụ $1000/giờ) cần escalate ngay tới engineering lead, SEV2/3 cho các trường hợp nhỏ hơn có thể xử lý trong giờ làm việc thông thường. Việc phân loại severity giúp team không "hoảng loạn" với mọi cảnh báo nhưng cũng không chủ quan với trường hợp nghiêm trọng.

Mẹo

  • Viết sẵn runbook cho các loại incident cost thường gặp (agent loop vô hạn, tool trả dữ liệu quá lớn, user abuse, model bị đổi ngầm sang bản đắt hơn) — thời gian phản ứng nhanh hơn rất nhiều khi có checklist sẵn thay vì phải nghĩ từ đầu lúc đang căng thẳng.
  • Luôn tính "thiệt hại thực tế" (actual cost impact) trong post-mortem bằng số tiền cụ thể, không chỉ mô tả chung — điều này giúp việc đầu tư vào cải thiện monitoring/threshold trong tương lai dễ được ưu tiên hơn khi trình bày với stakeholder.
  • Tập dượt (game day) kịch bản cost spike định kỳ, giống như tập dượt disaster recovery — team phản ứng nhanh và chính xác hơn nhiều nếu đã từng thực hành quy trình containment trước đó.
  • Đảm bảo người on-call có đủ quyền hạn (permission) để thực hiện containment ngay (tắt feature flag, revoke key) mà không cần chờ approval qua nhiều cấp — trong sự cố cost spike, mỗi phút trì hoãn đều có giá bằng tiền thật.

Tổng kết

Giám sát chi phí token cho hệ thống agentic không phải là một tính năng "nice-to-have" thêm vào sau — nó là hạ tầng bắt buộc phải có trước khi đưa agent ra production ở quy mô thực. Ba lớp của stack — instrumentation bắt từng API call, aggregation biến raw data thành metric có ý nghĩa, và alerting real-time biến metric thành hành động — hoạt động cùng nhau để biến chi phí LLM từ một "hộp đen" đáng sợ thành một chỉ số có thể quan sát và kiểm soát được, không khác gì latency hay error rate trong observability truyền thống.

Công cụ provider-native (OpenAI Usage Dashboard, Anthropic Console, GCP Billing, AWS Cost Explorer) là lớp phòng vệ bổ sung quan trọng nhưng không thể thay thế instrumentation ở tầng ứng dụng, vì chúng thiếu context nghiệp vụ (feature, user, agent step) cần thiết để debug nhanh. Dashboard tổng hợp trực quan (như Grafana) giúp team phát hiện bất thường bằng mắt mỗi ngày, còn quy trình incident response rõ ràng — với kill switch chuẩn bị sẵn từ trước — quyết định việc một sự cố dừng lại ở vài chục đô hay leo thang thành hàng nghìn đô thiệt hại.

Khoản đầu tư ban đầu để xây dựng đầy đủ 3 lớp này (thường 1-2 tuần công cho một engineer có kinh nghiệm) gần như luôn được đền đáp ngay từ sự cố cost spike đầu tiên mà nó giúp phát hiện và ngăn chặn sớm — và với agentic system, sự cố đầu tiên đó thường xảy ra sớm hơn nhiều người nghĩ.

Mẹo

  • Nếu chỉ có thời gian đầu tư một lớp trước, hãy chọn Lớp 1 (instrumentation) — thiếu dữ liệu sạch ở tầng này, hai lớp aggregation và alerting phía sau gần như vô nghĩa, dù bạn dùng công cụ đắt đến đâu.
  • Đưa việc audit lại cả 3 lớp vào lịch định kỳ mỗi quý: kiểm tra instrumentation có "lọt" call nào không (so với dashboard provider-native), threshold còn phù hợp với traffic hiện tại không, và kill switch/runbook có còn hoạt động đúng như thiết kế không.
  • Đưa chi phí xây 3 lớp giám sát này vào ngân sách engineering ngay từ quý đầu tiên đưa agent ra production, đừng đợi đến khi đã có một sự cố cost spike lớn mới xin đầu tư — lúc đó chi phí xin duyệt ngân sách còn cao hơn chi phí xây hệ thống.
  • Khi team còn nhỏ và chưa đủ người trực on-call 24/7, ưu tiên đầu tư vào absolute threshold và budget burn-rate threshold trước (dễ hiểu, dễ set up), rồi mới đầu tư per-entity anomaly detection phức tạp hơn khi hệ thống đã có traffic ổn định để tính baseline chính xác.