Bỏ qua

Bài 3: Cost Optimization & Caching

Tổng quan

Module I, Bài 8 đã giới thiệu ba đòn bẩy: prompt caching, semantic caching, model routing (rule-based / embedding / classifier). Bài này mở rộng sang cấp hệ thống:

  • Hiểu token economics: tiền đi đâu, vì sao context bloat là kẻ giết ngân sách.
  • Đo trước khi tối ưu: cost per request / user / feature.
  • Cache 2 tầng hoàn chỉnh với invalidation đúng theo prompt version.
  • Model cascading (khác routing) và Batch API.
  • Cost governance: budget, alert, hard limit.

1. Token Economics

Tiền đi đâu trong một request

graph LR
    subgraph "Input tokens (rẻ hơn)"
        SYS[System prompt] --- FS[Few-shot] --- CTX[RAG context] --- HIST[Lịch sử hội thoại] --- Q[Câu hỏi]
    end
    subgraph "Output tokens (đắt 3-5x)"
        ANS[Câu trả lời] --- REASON[Reasoning / chain-of-thought]
    end
Yếu tố Đặc điểm chi phí
Input tokens Rẻ hơn; với prompt caching còn rẻ nữa (–50→90% cho phần cache hit)
Output tokens Đắt nhất - thường 3–5× giá input. Reasoning model "nghĩ" nhiều = tốn nhiều output ẩn
Context bloat RAG lấy 10 chunk khi 3 chunk là đủ; lịch sử 20 lượt khi 4 lượt là đủ; tool schema JSON lặp lại mỗi request
Retry / self-reflection Mỗi vòng lặp agent là một request đầy đủ - nhân chi phí

Công thức nhẩm

cost_request ≈ (input_tokens × giá_in) + (output_tokens × giá_out)

Ví dụ RAG (giá minh hoạ, USD / 1M tokens: in $3, out $15):
  system 400 + few-shot 300 + context 3000 + câu hỏi 100 = 3800 input
  câu trả lời 350 output
  ≈ 3800/1e6 × 3 + 350/1e6 × 15 = $0.0114 + $0.00525 ≈ $0.0167 / request

  → context (3000 token) chiếm ~70% chi phí input. Cắt còn 1500 → tiết kiệm ~35% cả request.

Trước khi tối ưu: đo phân bố token

Log input_tokens, output_tokens, và breakdown (bao nhiêu cho context, cho history, cho system). Thường phát hiện: 1 tính năng ít dùng nhưng context khổng lồ đang ngốn 40% hoá đơn.


2. Đo trước khi tối ưu

Gắn nhãn mọi request

# cost/tracker.py
import tiktoken
PRICING = {  # USD / 1M tokens (minh hoạ - luôn tra bảng giá hiện hành)
    "claude-sonnet-5": {"in": 3.0, "out": 15.0},
    "gpt-4o-mini":     {"in": 0.15, "out": 0.60},
}

def record_cost(*, feature: str, user_id: str, model: str,
                usage, cache_hit: bool = False):
    p = PRICING[model]
    cost = usage.prompt_tokens / 1e6 * p["in"] + usage.completion_tokens / 1e6 * p["out"]
    metrics.emit("llm_cost_usd", cost, tags={
        "feature": feature, "model": model,
        "cache": "hit" if cache_hit else "miss",
    })
    metrics.emit("llm_tokens_in", usage.prompt_tokens, tags={"feature": feature})
    metrics.emit("llm_tokens_out", usage.completion_tokens, tags={"feature": feature})
    # per-user: dùng cho hard limit (mục 7), không cần gửi hết lên dashboard
    redis.incrbyfloat(f"cost:user:{user_id}:{today()}", cost)

Ba lát cắt cần nhìn

Lát cắt Trả lời câu hỏi Hành động
Per feature Tính năng nào ngốn ngân sách? Tối ưu context/model cho đúng tính năng đó
Per user Ai đang lạm dụng / cost bất thường? Rate limit, hard cap
Per request (phân bố) p50 vs p99 chênh bao nhiêu? p99 cao = có request context khổng lồ / loop agent dài

Nguyên tắc: tìm 20% tính năng chiếm 80% chi phí rồi mới chọn kỹ thuật tối ưu. Đừng tối ưu đều tay.


3. Caching tầng 1: Exact / Prompt Cache

Prompt caching (nhắc lại + mở rộng)

Cơ chế đã học ở M1, Bài 8: provider cache phần prefix lặp lại. Điểm mở rộng cho production:

  • Sắp xếp prompt: tĩnh trước, động sau. System prompt + few-shot + tài liệu ít đổi đặt đầu; context RAG và câu hỏi đặt cuối → prefix cache dùng lại được nhiều nhất.
  • TTL ngắn (Anthropic mặc định ~5 phút): chỉ có lợi khi traffic đủ dày để request kế tiếp rơi vào cửa sổ TTL.
  • Invalidation khi prompt đổi version: xem dưới.

Exact-match response cache (tầng ứng dụng)

Khác prompt caching (của provider), đây là cache toàn bộ câu trả lời cho input giống hệt:

# cache/exact.py
def cache_key(prompt_name: str, prompt_version: int, model: str,
              rendered_prompt: str, params: dict) -> str:
    raw = json.dumps({
        "pn": prompt_name, "pv": prompt_version, "m": model,
        "p": rendered_prompt, "params": params,
    }, sort_keys=True, ensure_ascii=False)
    return "resp:" + hashlib.sha256(raw.encode()).hexdigest()

def get_or_call(prompt_name, prompt_version, model, rendered_prompt, params, call_fn):
    key = cache_key(prompt_name, prompt_version, model, rendered_prompt, params)
    if (hit := redis.get(key)) is not None:
        record_cost(feature=..., model=model, usage=ZERO_USAGE, cache_hit=True)
        return json.loads(hit)
    out = call_fn()
    redis.setex(key, ttl=86400, value=json.dumps(out))
    return out

Invalidation: prompt_version phải nằm trong cache key

Nếu bạn promote rag_answer v2 → v3 (Bài 1) mà cache key không chứa version, người dùng vẫn nhận câu trả lời sinh bởi v2 trong suốt TTL. Đưa prompt_version (và model) vào key → version mới tự động là "cache namespace" mới, bản cũ hết hạn tự nhiên.


4. Caching tầng 2: Semantic Cache

Trả lời cũ cho câu hỏi gần giống (không cần giống hệt). Cơ chế đã học ở M1, Bài 8. Bổ sung cho production:

Kiến trúc

graph LR
    Q[Câu hỏi mới] --> EMB[Embed]
    EMB --> SEARCH[Vector search trong cache store]
    SEARCH --> SIM{Similarity ≥ ngưỡng?}
    SIM -->|Có| HIT[Trả lời cache - $0, ~10ms]
    SIM -->|Không| LLM[Gọi LLM]
    LLM --> STORE[Lưu embedding + câu trả lời vào cache]

False hit và cách phòng

Rủi ro Ví dụ Phòng
Ngưỡng quá thấp "Điều kiện thành lập công ty TNHH" ↔ "Điều kiện giải thể công ty TNHH" similarity 0.86 → trả nhầm Đặt ngưỡng cao (0.92–0.95 với embedding tốt); tune trên tập câu hỏi thật
Câu phủ định / thời gian "... năm 2020" vs "... năm 2024" gần nhau về embedding Chuẩn hoá / tách entity quan trọng; không cache câu có tham số ngày/số cụ thể
Ngữ cảnh hội thoại "Còn trường hợp thứ hai thì sao?" phụ thuộc lượt trước Chỉ semantic-cache câu hỏi standalone (sau bước rewrite), không cache follow-up
Cache stale Tài liệu nguồn cập nhật, câu trả lời cache đã lỗi thời TTL theo tốc độ đổi của tài liệu; xoá namespace khi RAG index rebuild

Đo hiệu quả

hit_rate = hits / (hits + misses)
saving   = hit_rate × cost_trung_bình_mỗi_request

Semantic cache chỉ đáng khi hit_rate đủ cao (FAQ, support bot: 20–40%; câu hỏi đa dạng: có thể < 5% → không đáng phức tạp).


5. Model Routing & Cascading

Routing vs Cascading

graph TB
    subgraph "Routing - quyết định TRƯỚC"
        Q1[Query] --> CLF[Phân loại: dễ / khó]
        CLF -->|dễ| S1[Model rẻ]
        CLF -->|khó| B1[Model mạnh]
    end
    subgraph "Cascading - thử rồi ESCALATE"
        Q2[Query] --> S2[Model rẻ trả lời]
        S2 --> CHK{Đạt ngưỡng tin cậy /<br/>qua guardrail?}
        CHK -->|Có| DONE2[Dùng luôn]
        CHK -->|Không| B2[Escalate: Model mạnh trả lại]
    end
Routing Cascading
Quyết định Trước khi gọi model, dựa trên query Sau câu trả lời của model rẻ, dựa trên output
Chi phí thêm 1 lần phân loại (~ms) Với case khó: trả tiền cả hai model
Rủi ro Phân loại sai → model rẻ nhận câu khó Model rẻ "tự tin sai" → không escalate
Khi nào tốt Phân biệt dễ/khó rõ ràng từ query Khó đoán độ khó trước; có tín hiệu chất lượng trên output

Cascade implementation

# routing/cascade.py
def answer_cascade(question: str, context: str) -> dict:
    cheap = call("gpt-4o-mini", question, context)
    if _confident(cheap) and _passes_guardrails(cheap):
        metrics.emit("cascade_escalate", 0)
        return cheap
    metrics.emit("cascade_escalate", 1)
    strong = call("claude-sonnet-5", question, context)
    return strong

def _confident(resp) -> bool:
    # tín hiệu: model không nói "không chắc / không tìm thấy",
    # logprobs cao nếu có, hoặc self-rating >= 4
    return "không tìm thấy" not in resp["text"].lower()

Đo tỉ lệ escalate. Nếu 70% case phải escalate → cascade đang lỗ (trả 2 lần tiền cho phần lớn traffic); chuyển sang routing hoặc dùng thẳng model mạnh.


6. Kỹ thuật khác

Prompt compression & context trimming

Kỹ thuật Cách làm Lưu ý
Trim RAG context Rerank rồi chỉ giữ top-k thật sự liên quan (k=3 thay 10); cắt chunk còn phần cốt lõi Đo lại faithfulness/recall - cắt quá tay làm rớt chất lượng
Nén lịch sử hội thoại Tóm tắt các lượt cũ thành 1 đoạn ngắn, giữ nguyên 2–3 lượt gần nhất Xem M2, Bài 3 - context compression
Bỏ few-shot khi model đủ mạnh Model mới thường không cần 5 ví dụ; thử 0–2 shot + eval So điểm eval trước/sau
Prompt compression tự động (LLMLingua…) Model nhỏ nén prompt, giữ token "quan trọng" Thêm 1 bước xử lý; hợp lý khi prompt rất dài và ổn định

Batch API

Với workload không cần realtime (eval hàng loạt, tiền xử lý tài liệu, sinh embedding/nhãn):

  • Gửi tập request dạng file, provider xử lý bất đồng bộ trong cửa sổ (thường ≤ 24h).
  • Giá thường giảm ~50% so với API đồng bộ.
  • Dùng cho: chạy full golden set nightly (Bài 2), backfill phân loại, sinh dataset synthetic.
# batch/submit.py
batch_input = [
    {"custom_id": c["id"], "method": "POST", "url": "/v1/chat/completions",
     "body": {"model": "gpt-4o-mini", "messages": build_messages(c)}}
    for c in cases
]
# ghi JSONL, upload, tạo batch job, poll trạng thái, tải kết quả

7. Cost Governance

Budget & Alert

# cost/budget.py
MONTHLY_BUDGET_USD = 800
ALERT_THRESHOLDS = [0.5, 0.8, 1.0]   # 50% / 80% / 100%

def check_budget():
    spent = sum_month_to_date()          # từ metrics store
    projected = spent / days_elapsed() * days_in_month()
    for t in ALERT_THRESHOLDS:
        if spent >= MONTHLY_BUDGET_USD * t and not alerted(t):
            alert(f"Chi phí LLM đạt {t:.0%} ngân sách tháng "
                  f"(${spent:.0f}/${MONTHLY_BUDGET_USD}). Dự phóng cuối tháng: ${projected:.0f}.")
            mark_alerted(t)

Hard limit per user

def guard_user_budget(user_id: str, daily_cap_usd: float = 2.0):
    spent = float(redis.get(f"cost:user:{user_id}:{today()}") or 0)
    if spent >= daily_cap_usd:
        raise BudgetExceeded(f"Vượt hạn mức ngày (${daily_cap_usd}). Thử lại vào ngày mai.")

Bảng theo dõi

Cần theo dõi Tần suất Ngưỡng cảnh báo
Tổng chi phí ngày / tháng Realtime + báo cáo ngày > 120% dự phóng
Cost per feature Ngày Feature tăng > 50% tuần-trên-tuần
Cost per request (p50, p99) Ngày p99 > 5× p50
Cache hit rate (tầng 1 + 2) Ngày Tụt đột ngột (cache bị vô hiệu do đổi key?)
Tỉ lệ escalate của cascade Ngày > 50%

Cost spike là một loại incident

Xử lý cost spike (phát hiện → khoanh vùng feature/user → mitigate) được trình bày như một playbook ở Bài 7.


8. Hands-on: Cache 2 tầng cho Vietnamese chatbot

Dùng chatbot RAG từ Capstone Module I.

Bước 1 - Đo baseline

  • Chuẩn bị tập replay: 200 câu hỏi (trộn ~30% trùng/gần giống để mô phỏng FAQ).
  • Gắn record_cost() vào đường gọi LLM. Chạy replay không cache.
  • Ghi: tổng cost, cost/request p50/p99, token in/out trung bình, breakdown context.

Bước 2 - Tầng 1: exact response cache

  • cache_key gồm prompt_name, prompt_version, model, rendered_prompt, params.
  • Redis setex TTL 24h. Chạy lại replay → đo hit rate, cost mới.
  • Bump prompt version → xác nhận cache tự "reset" (không trả câu cũ).

Bước 3 - Tầng 2: semantic cache

  • Embed câu hỏi (sau bước rewrite → standalone), vector store nhỏ (FAISS/Redis).
  • Ngưỡng khởi đầu 0.93; chạy replay, soát 10 hit ngẫu nhiên xem có false hit.
  • Tune ngưỡng; loại câu có tham số ngày/số khỏi semantic cache.

Bước 4 - Báo cáo

Cấu hình Tổng cost Cost/req Hit rate Ghi chú
Không cache ... ... – baseline
+ Tầng 1 ... ... ...%
+ Tầng 1 & 2 ... ... ...% false hit: .../10

Bước 5 (mở rộng) - Cascade

  • Thêm answer_cascade(): gpt-4o-mini trước, escalate sang claude-sonnet-5 khi output nói "không tìm thấy" hoặc self-rating < 4.
  • Chạy replay (cache miss only), đo tỉ lệ escalate + cost so với dùng thẳng model mạnh.

Tiêu chí hoàn thành

  • Có số cost baseline với breakdown token
  • Cache key chứa prompt_version → bump version không trả câu cũ
  • Semantic cache có kiểm false hit thủ công + ngưỡng đã tune
  • Bảng so sánh cost trước/sau trên cùng tập replay
  • (Mở rộng) tỉ lệ escalate của cascade < 50% hoặc kết luận rõ cascade không đáng

Tóm tắt

graph TB
    REQ[Request] --> T1{Tầng 1<br/>exact cache?}
    T1 -->|hit| DONE[Trả lời - $0]
    T1 -->|miss| T2{Tầng 2<br/>semantic cache?}
    T2 -->|hit| DONE
    T2 -->|miss| ROUTE[Routing / Cascade]
    ROUTE --> CALL[Gọi LLM<br/>prompt tĩnh trước, động sau -> prompt cache]
    CALL --> REC[record_cost per feature/user] --> STORE[Ghi vào 2 tầng cache]
    REC --> BUD{Budget / hard limit}
Kỹ thuật Khi nào dùng
Đo token/cost per feature Luôn - trước mọi tối ưu
Prompt caching (provider) Prefix dài lặp lại; sắp xếp tĩnh→động
Exact response cache Input lặp lại y hệt; key phải chứa prompt_version + model
Semantic cache FAQ/support, hit rate > ~20%; ngưỡng cao + loại câu có tham số
Routing Phân biệt dễ/khó rõ từ query
Cascading Khó đoán độ khó trước; theo dõi tỉ lệ escalate
Batch API Workload offline (eval, backfill) - giảm ~50% giá
Budget + hard limit Alert theo % ngân sách; cap ngày per user; cost spike = incident