Bỏ qua

Bài 8: Production Optimization - Latency & Cost

Tổng quan

Ở môi trường production, một LLM app không chỉ cần trả lời đúng mà còn phải nhanhrẻ. Ba kỹ thuật chủ lực để tối ưu latency và cost là Prompt Caching, Semantic Caching, và Model Routing. Bài này đi sâu vào cả ba, đặc biệt là các phương pháp routing nhẹ và nhanh.


1. Bức tranh chi phí & độ trễ

Mỗi request tới LLM tiêu tốn hai thứ: tiền (theo token) và thời gian (latency). Ba đòn bẩy chính:

graph LR
    REQ[Request] --> C1{Đã thấy<br/>prefix này?}
    C1 -->|Có| PC[Prompt Cache<br/>giảm cost input]
    C1 -->|Chưa| C2{Câu hỏi<br/>tương tự đã hỏi?}
    C2 -->|Có| SC[Semantic Cache<br/>bỏ qua LLM hoàn toàn]
    C2 -->|Chưa| C3{Task khó hay dễ?}
    C3 -->|Dễ| SMALL[Model nhỏ/rẻ]
    C3 -->|Khó| BIG[Model lớn/mạnh]
Kỹ thuật Giảm gì Cơ chế
Prompt Caching Cost input + TTFT Cache phần prefix lặp lại của prompt
Semantic Caching Cost + latency toàn bộ Trả lại câu trả lời cũ cho câu hỏi tương tự
Model Routing Cost + latency Gửi task dễ tới model rẻ, task khó tới model mạnh

2. Prompt Caching

Khái niệm

Khi nhiều request chia sẻ chung một prefix dài (system prompt, few-shot examples, tài liệu context), provider có thể cache phần đã xử lý này và tái sử dụng - không tính lại từ đầu.

Request 1: [System prompt 2000 tokens] + [Câu hỏi A]
Request 2: [System prompt 2000 tokens] + [Câu hỏi B]
        Phần này được cache -> request 2 chỉ trả tiền cho [Câu hỏi B]

Lợi ích

  • Giảm cost: token cache thường rẻ hơn 50–90% so với token thường.
  • Giảm TTFT (Time To First Token): không phải xử lý lại prefix.

Cách dùng

import anthropic

client = anthropic.Anthropic()

response = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=1024,
    system=[
        {
            "type": "text",
            "text": "Bạn là chuyên gia pháp lý...",  # prefix dài
        },
        {
            "type": "text",
            "text": LARGE_LEGAL_DOCUMENT,  # tài liệu context lớn
            "cache_control": {"type": "ephemeral"},  # đánh dấu cache
        },
    ],
    messages=[{"role": "user", "content": "Điều 5 nói gì?"}],
)
# Các request sau với cùng prefix -> đọc từ cache
# OpenAI tự động cache prompt > 1024 tokens, không cần config.
# Đặt phần tĩnh (system, examples) LÊN ĐẦU, phần động xuống cuối
# để tối đa hoá cache hit.
response = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[
        {"role": "system", "content": LONG_STATIC_INSTRUCTIONS},  # tĩnh
        {"role": "user", "content": dynamic_question},            # động
    ],
)
# response.usage.prompt_tokens_details.cached_tokens cho biết số token hit cache

Nguyên tắc vàng: tĩnh trước, động sau

Prompt caching chỉ hit khi prefix khớp chính xác. Luôn đặt nội dung cố định (system prompt, examples, tài liệu) ở đầu và phần thay đổi (câu hỏi người dùng) ở cuối.


3. Semantic Caching

Khái niệm

Prompt caching yêu cầu prefix khớp y hệt. Semantic caching đi xa hơn: nếu một câu hỏi gần giống về ngữ nghĩa với câu đã hỏi trước, ta trả lại câu trả lời cũ - bỏ qua LLM hoàn toàn.

Đã cache: "Thủ đô Việt Nam là gì?" -> "Hà Nội"

Câu mới:  "Việt Nam có thủ đô ở đâu?"
          -> embedding similarity = 0.94 > threshold
          -> trả lại "Hà Nội" ngay, KHÔNG gọi LLM

Kiến trúc

graph LR
    Q[Câu hỏi mới] --> E[Embed câu hỏi]
    E --> S[Tìm nearest neighbor<br/>trong cache vector store]
    S --> T{Similarity ><br/>threshold?}
    T -->|Có| HIT[Cache HIT<br/>trả answer cũ]
    T -->|Không| MISS[Cache MISS<br/>gọi LLM + lưu lại]
    MISS --> STORE[(Vector Store)]

Triển khai cơ bản

from sentence_transformers import SentenceTransformer
import numpy as np

embedder = SentenceTransformer("all-MiniLM-L6-v2")

class SemanticCache:
    def __init__(self, threshold=0.92):
        self.threshold = threshold
        self.questions = []   # list[str]
        self.embeddings = []  # list[np.ndarray]
        self.answers = []     # list[str]

    def get(self, question):
        if not self.embeddings:
            return None
        q_emb = embedder.encode(question, normalize_embeddings=True)
        sims = np.dot(np.array(self.embeddings), q_emb)
        best = int(np.argmax(sims))
        if sims[best] >= self.threshold:
            return self.answers[best]  # cache HIT
        return None

    def set(self, question, answer):
        self.questions.append(question)
        self.embeddings.append(embedder.encode(question, normalize_embeddings=True))
        self.answers.append(answer)

cache = SemanticCache()

def ask(question):
    cached = cache.get(question)
    if cached is not None:
        return cached
    answer = call_llm(question)   # chỉ gọi khi MISS
    cache.set(question, answer)
    return answer

Cẩn trọng với threshold và tính tươi của dữ liệu

  • Threshold quá thấp -> trả nhầm câu trả lời cho câu hỏi khác nghĩa (false positive nguy hiểm).
  • Threshold quá cao -> ít cache hit, mất tác dụng.
  • Không hợp cho câu trả lời phụ thuộc thời gian/ngữ cảnh ("Hôm nay thế nào?", giá cổ phiếu...). Cần đặt TTL để invalidate cache cũ.

Production thường dùng vector store chuyên dụng (Redis với RediSearch, hoặc thư viện GPTCache) thay cho list in-memory.


4. Model Routing

Tại sao cần Routing?

Không phải request nào cũng cần model mạnh nhất. "1 + 1 bằng mấy?" không cần tới model flagship đắt tiền. Model routing phân loại request và gửi tới model phù hợp - cân bằng giữa chất lượng và chi phí.

Task đơn giản (FAQ, phân loại, format)  -> Model nhỏ, rẻ, nhanh
Task phức tạp (reasoning, code, phân tích) -> Model lớn, mạnh, đắt

Router phải nhẹ hơn cái nó định tuyến

Một router lý tưởng phải nhanh và rẻ hơn nhiều so với chính các model nó định tuyến tới. Nếu router tốn bằng việc gọi model lớn thì vô nghĩa. Dưới đây là ba phương pháp fast & lightweight xếp theo độ phức tạp tăng dần.

4.1. Rule-based Routing

Đơn giản nhất: dùng heuristic/luật cứng dựa trên đặc điểm bề mặt của request. Latency gần như bằng 0, cost = 0.

def rule_based_router(query: str) -> str:
    n_tokens = len(query.split())

    # Luật 1: query rất ngắn -> thường là task đơn giản
    if n_tokens < 10:
        return "gpt-4o-mini"

    # Luật 2: keyword chỉ dấu task khó
    hard_keywords = ["chứng minh", "phân tích", "viết code",
                     "debug", "so sánh chi tiết", "step by step"]
    if any(kw in query.lower() for kw in hard_keywords):
        return "gpt-4o"

    # Luật 3: có code block -> cần model mạnh
    if "```" in query:
        return "gpt-4o"

    return "gpt-4o-mini"  # mặc định
Ưu điểm Nhược điểm
Cực nhanh, không tốn cost, dễ debug Cứng nhắc, không hiểu ngữ nghĩa sâu, phải maintain luật thủ công

Dùng khi: Domain hẹp, đã biết rõ pattern của query; cần baseline nhanh.

4.2. Embedding Similarity Routing

Nhúng query vào vector rồi so với các embedding mẫu đại diện cho từng nhóm độ khó (hoặc từng model). Chọn nhóm có similarity cao nhất. Chỉ tốn một lần embed (rất rẻ so với gọi LLM).

from sentence_transformers import SentenceTransformer
import numpy as np

embedder = SentenceTransformer("all-MiniLM-L6-v2")

# Câu mẫu đại diện cho từng route
ROUTE_EXAMPLES = {
    "gpt-4o-mini": [   # task đơn giản
        "Thủ đô của Pháp là gì?",
        "Dịch câu này sang tiếng Anh",
        "Tóm tắt đoạn văn sau",
    ],
    "gpt-4o": [        # task phức tạp
        "Chứng minh định lý Pythagoras",
        "Viết và tối ưu thuật toán Dijkstra",
        "Phân tích ưu nhược điểm của kiến trúc microservices",
    ],
}

# Tính trước centroid embedding cho mỗi route
route_centroids = {
    model: np.mean(
        embedder.encode(examples, normalize_embeddings=True), axis=0
    )
    for model, examples in ROUTE_EXAMPLES.items()
}

def embedding_router(query: str) -> str:
    q_emb = embedder.encode(query, normalize_embeddings=True)
    scores = {
        model: float(np.dot(centroid, q_emb))
        for model, centroid in route_centroids.items()
    }
    return max(scores, key=scores.get)
Ưu điểm Nhược điểm
Hiểu ngữ nghĩa, dễ mở rộng route bằng cách thêm câu mẫu, không cần train Cần chọn câu mẫu tốt; embedding model phải load sẵn; kém chính xác hơn classifier train riêng

Dùng khi: Cần phân biệt theo ý nghĩa nhưng chưa có/không muốn train data.

4.3. Fine-tuned Small Classifier

Train một classifier nhỏ (ví dụ DistilBERT, hoặc thậm chí logistic regression trên embedding) để dự đoán nhãn route. Chính xác nhất trong ba cách, vẫn cực nhanh ở inference (vài mili-giây trên CPU).

# --- Giai đoạn train (offline, một lần) ---
from sentence_transformers import SentenceTransformer
from sklearn.linear_model import LogisticRegression
import joblib

embedder = SentenceTransformer("all-MiniLM-L6-v2")

# Data gán nhãn: (query, label) với label = "easy" / "hard"
X_text, y = load_routing_dataset()
X = embedder.encode(X_text, normalize_embeddings=True)

clf = LogisticRegression(max_iter=1000)
clf.fit(X, y)
joblib.dump(clf, "router.joblib")

# --- Giai đoạn inference (online, mỗi request) ---
clf = joblib.load("router.joblib")

def classifier_router(query: str) -> str:
    emb = embedder.encode([query], normalize_embeddings=True)
    label = clf.predict(emb)[0]
    # Có thể dùng predict_proba để lấy confidence + ngưỡng escalate
    return "gpt-4o-mini" if label == "easy" else "gpt-4o"

Lấy training data từ đâu?

Một cách thực dụng: chạy cả model nhỏ và model lớn trên một tập query, dùng LLM-as-judge (xem Bài 7) chấm xem model nhỏ có đủ tốt không. Nếu model nhỏ đủ tốt -> nhãn "easy", ngược lại -> "hard". Đây chính là tín hiệu vàng để train router.

Ưu điểm Nhược điểm
Chính xác nhất, học từ data thực, cho ra confidence score Cần training data có nhãn + pipeline train/retrain; rủi ro drift khi phân phối query thay đổi

Dùng khi: Đã có volume traffic đủ lớn, chênh lệch cost giữa các model đáng kể, cần tối ưu nghiêm túc.

So sánh ba phương pháp routing

Tiêu chí Rule-based Embedding Similarity Fine-tuned Classifier
Latency router ~0 ms vài ms (1 lần embed) vài ms (embed + clf)
Cost router 0 rất thấp rất thấp
Cần training data Không Không (chỉ câu mẫu) Có (gán nhãn)
Hiểu ngữ nghĩa Không
Độ chính xác Thấp Trung bình Cao
Công sức maintain Cao (sửa luật tay) Thấp Trung bình (retrain)

Kết hợp nhiều tầng (cascade)

Production thực tế thường xếp tầng: thử model rẻ trước, nếu confidence thấp hoặc output không đạt guardrail thì mới escalate lên model mạnh. Routing + cascade + caching kết hợp lại cho hiệu quả cost tốt nhất.


Tóm tắt

graph TD
    REQ[Incoming Request] --> PC[1. Prompt Caching<br/>tĩnh trước, động sau]
    PC --> SC[2. Semantic Cache<br/>check câu tương tự]
    SC -->|HIT| DONE[Trả lời ngay]
    SC -->|MISS| ROUTE[3. Model Routing]
    ROUTE -->|Dễ| SMALL[Model nhỏ/rẻ]
    ROUTE -->|Khó| BIG[Model lớn/mạnh]
    SMALL --> DONE
    BIG --> DONE
Kỹ thuật Khi nào dùng
Prompt Caching Prompt có prefix dài lặp lại (system prompt, RAG context)
Semantic Caching Nhiều câu hỏi trùng/gần giống nhau (FAQ, support bot)
Rule-based Routing Domain hẹp, pattern rõ ràng, cần baseline nhanh
Embedding Routing Cần hiểu ngữ nghĩa, chưa có training data
Classifier Routing Volume lớn, chênh lệch cost cao, tối ưu nghiêm túc