Cheap Models First: Building an LLM Cascade Router That Cuts Your AI Bill Without Hurting Quality
Tuần này trên Hacker News có một thread gần 1000 điểm hỏi: "Tại sao ngành không hoảng lên vì DeepSeek 4.1 Flash?". Trên Dev.to cũng có bài kiểu "mình đã đưa agent về zero mistakes mà vẫn dùng Flash-Lite". Hai chuyện này nói cùng một điều mà nhiều team ở Việt Nam chưa để ý: model rẻ giờ đủ tốt cho phần lớn request. Model đắt chỉ nên dùng cho phần khó còn lại.
Mình đã refactor một hệ thống xử lý ticket support (khoảng 40k request/ngày) theo kiểu này: đi qua model nhỏ trước, khi cần mới escalate lên model lớn. Chi phí giảm khoảng 70%, còn chất lượng đo bằng eval set thì gần như giữ nguyên. Bài này chia sẻ cách làm cụ thể, có code chạy được.
Vì sao "một model cho tất cả" là lãng phí
Đa số app AI hiện tại hardcode một model, thường là model mạnh nhất cho chắc. Nhưng khi nhìn log thật, bạn sẽ thấy traffic phân bố kiểu này:
- ~60-70% là request đơn giản: phân loại, extract field, tóm tắt ngắn, format lại JSON.
-
- ~20-30% là request trung bình: cần suy luận vài bước.
-
- ~5-10% là request thật sự khó: reasoning dài, code phức tạp, context mơ hồ.
Dùng model flagship cho cả nhóm đầu thì giống thuê senior architect đi rename biến. Giá giữa model Flash/Lite và flagship thường chênh 10-30 lần mỗi token, nên chỉ cần chuyển nhóm đơn giản xuống model rẻ là hóa đơn đã giảm mạnh.
Có hai pattern chính:
- Routing: phân loại request trước rồi chọn model. Nhanh, nhưng bộ phân loại có thể sai.
-
- Cascade: luôn thử model rẻ trước, validate output, fail thì escalate. Chắc hơn, đổi lại latency cao hơn ở các case khó.
Trong thực tế mình kết hợp cả hai. Sơ đồ như sau:
flowchart TD
A[Request] --> B{Heuristic router}
B -->|Simple task| C[Flash-Lite model]
B -->|Hard task| E[Flagship model]
C --> D{Validator pass?}
D -->|Yes| F[Return response]
D -->|No| G[Flash model]
G --> H{Validator pass?}
H -->|Yes| F
H -->|No| E
E --> F
```
## Implement cascade router bằng Python
Đa số provider hiện nay (DeepSeek, OpenRouter, vLLM self-host, Ollama...) đều có endpoint **OpenAI-compatible**, nên mình dùng `openai` Python SDK (>= 1.40) làm client chung. Môi trường: Python 3.12.
```python
# router.py
import json
import time
from dataclasses import dataclass
from openai import OpenAI
client = OpenAI() # đọc OPENAI_BASE_URL + OPENAI_API_KEY từ env
@dataclass
class Tier:
name: str
model: str
max_tokens: int
TIERS = [
Tier('lite', 'flash-lite-model', 512),
Tier('flash', 'flash-model', 1024),
Tier('flagship', 'flagship-model', 4096),
]
def validate(output: str, required_keys: list[str]) -> bool:
try:
data = json.loads(output)
except json.JSONDecodeError:
return False
if not all(k in data for k in required_keys):
return False
# model nhỏ hay "đoán bừa" -> bắt nó tự khai confidence
return data.get('confidence', 0) >= 0.7
def cascade(prompt: str, required_keys: list[str], start: int = 0):
for tier in TIERS[start:]:
t0 = time.perf_counter()
resp = client.chat.completions.create(
model=tier.model,
messages=[{'role': 'user', 'content': prompt}],
max_tokens=tier.max_tokens,
response_format={'type': 'json_object'},
temperature=0,
)
out = resp.choices[0].message.content
ms = int((time.perf_counter() - t0) * 1000)
ok = validate(out, required_keys)
print(f'[{tier.name}] {ms}ms ok={ok} tokens={resp.usage.total_tokens}')
if ok or tier is TIERS[-1]:
return tier.name, json.loads(out)
```
Có vài điểm mình học được sau khi làm thật:
- **Validator là trái tim của cascade.** Nếu chỉ check "có trả về hay không" thì model rẻ lúc nào cũng pass. Phải check được cấu trúc (JSON schema, Pydantic), ràng buộc nghiệp vụ (enum hợp lệ, số nằm trong range) và self-reported confidence.
- **`temperature=0`** ở tier rẻ giúp output ổn định hơn và dễ debug hơn.
- **Bỏ qua tier khi cần**: tham số `start` cho phép router heuristic đẩy thẳng request khó lên tier cao, khỏi tốn thêm một round-trip vô ích.
Phần heuristic thì không cần ML gì cao siêu. Mình chỉ dùng mấy rule đơn giản:
```python
def pick_start_tier(prompt: str, task_type: str) -> int:
if task_type in {'classify', 'extract', 'translate_short'}:
return 0
if task_type == 'code_review' or len(prompt) > 12_000:
return 2 # context dài, model nhỏ hay "quên" giữa chừng
return 1
```
## Đừng tin cảm giác: dựng eval set trước khi chuyển
Đây là bước nhiều team bỏ qua rồi sau đó quay lại kêu "model rẻ ngu quá". Trước khi đổi production, bạn cần một **eval set** gồm khoảng 200-500 request thật đã gán nhãn đúng, lấy từ log, nhớ ẩn danh dữ liệu khách hàng.
```mermaid
sequenceDiagram
participant L as Production logs
participant E as Eval set (JSONL)
participant R as Cascade router
participant M as Metrics
L->>E: Sample 300 requests + label
E->>R: Replay từng case
R->>M: tier dùng, đúng/sai, latency, cost
M-->>R: Tune threshold confidence
```
Script replay đơn giản, chạy nền để khỏi block terminal:
```bash
# eval_set.jsonl: mỗi dòng {"prompt": ..., "task": ..., "expected": ...}
nohup python3 run_eval.py --data eval_set.jsonl --out results.jsonl > eval.log 2>&1 &
# Sau khi chạy xong: tỉ lệ request dừng ở mỗi tier
jq -r '.tier' results.jsonl | sort | uniq -c
# Accuracy theo từng tier
jq -s 'group_by(.tier) | map({tier: .[0].tier, acc: (map(select(.correct)) | length) / length})' results.jsonl
```
Kết quả lần đầu của mình: 64% dừng ở `lite`, 27% ở `flash`, 9% lên `flagship`. Accuracy tổng chỉ thấp hơn baseline "toàn flagship" có 1.2%. Sau đó mình tune threshold confidence từ 0.7 lên 0.8 thì gần như bằng baseline, đổi lại tỉ lệ dừng ở `lite` còn 55%. Đó là trade-off bạn phải tự chọn dựa trên số liệu, đừng chọn theo cảm giác.
## Những cái bẫy khi chạy production
**1. Latency đuôi (p99) tăng.** Request đi đủ 3 tier sẽ chậm hơn hẳn so với gọi thẳng flagship. Với flow có user ngồi chờ, nên đặt timeout ngắn cho tier rẻ (2-3s) và cho phép skip tier nếu SLA chặt.
**2. Cost ẩn của retry.** Request bị escalate tốn tiền cả tier dưới lẫn tier trên. Nếu tỉ lệ escalate vượt khoảng 40% thì cascade có thể còn đắt hơn gọi thẳng flagship. Hãy track metric `escalation_rate` trên dashboard.
**3. Model rẻ "yes-man".** Model nhỏ hay tự tin thái quá: confidence 0.95 mà vẫn sai. Đừng chỉ dựa vào self-reported confidence, nên kết hợp thêm validation nghiệp vụ, ví dụ check mã sản phẩm có tồn tại trong DB không.
**4. Provider đổi model âm thầm.** Hãy pin version model nếu provider cho phép, và chạy lại eval mỗi khi đổi version. Mình để eval chạy trong CI hằng tuần bằng một job cron đơn giản.
**5. Log đủ để debug.** Mỗi request cần log `tier`, `model`, `tokens`, `latency_ms`, `validator_reason`. Khi user báo lỗi, bạn phải biết ngay request đó dừng ở tier nào và vì sao validator cho pass.
## Kết luận
Model rẻ không còn là "đồ chơi" nữa. Câu hỏi bây giờ là kiến trúc của bạn có tận dụng được chúng hay không. Checklist để bắt đầu ngay tuần này:
1. **Phân tích log**: xem bao nhiêu % request thuộc loại classify/extract/format đơn giản. Đó chính là phần tiền đang lãng phí.
2. **Dựng eval set 200-500 case** từ dữ liệu thật trước khi đụng vào production.
3. **Bắt đầu với cascade 2 tier** (Flash + flagship), validator dùng JSON schema + confidence. Đừng over-engineer 5 tier ngay từ đầu.
4. **Track 3 metric**: escalation rate, accuracy theo tier, cost/request. Escalation vượt 40% thì xem lại validator hoặc routing.
5. **Chạy lại eval định kỳ** và mỗi khi provider ra version mới. Model rẻ cải thiện rất nhanh, có khi tháng sau tier `lite` đã gánh được 80% traffic.
Tối ưu chi phí AI rốt cuộc vẫn là bài toán engineering quen thuộc: đo, phân loại, fallback, monitor. Khác ở chỗ lần này số tiền tiết kiệm được đủ lớn để sếp chú ý.
All Rights Reserved