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 nhanh và rẻ. 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 | Có | Có |
| Độ 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 |