Tuần này trên Hacker News có một bài khá thú vị với hơn 500 điểm: một model nhỏ, khi không chắc chắn thì "hỏi" model lớn, đạt độ chính xác gần bằng model lớn nhưng chi phí thấp hơn khoảng 4 lần. Nghe giống phép màu, nhưng thật ra đây là một pattern đã có từ lâu: model cascading (hay escalation routing). Mình đã áp dụng nó cho một hệ thống phân loại ticket support và trích xuất dữ liệu hóa đơn. Kết quả: hóa đơn API giảm hơn 60%, còn accuracy gần như không đổi. Bài này chia sẻ cách làm thực tế: đo "độ tự tin" của model, chọn threshold, và những cái bẫy mình đã dính.
Ý tưởng cốt lõi: không phải request nào cũng cần model đắt nhất
Trong một workload thực tế, phần lớn request khá dễ: phân loại email rõ ràng, trích xuất field từ hóa đơn có format chuẩn, trả lời FAQ. Chỉ một phần nhỏ thật sự khó. Nếu gửi tất cả lên model lớn nhất, bạn đang trả tiền "premium" cho cả những việc mà một model 7B–8B chạy local cũng làm tốt.
Cascade hoạt động như sau:
flowchart TD
A[Request] --> B[Small model: 8B local / cheap API]
B --> C{Confidence >= threshold?}
C -- Yes --> D[Trả kết quả ngay]
C -- No --> E[Large model: frontier API]
E --> F[Trả kết quả]
E --> G[Log case khó để fine-tune / eval]
Công thức chi phí rất đơn giản:
cost = cost_small + escalation_rate × cost_large
Nếu model nhỏ rẻ hơn 20 lần và chỉ 15% request bị escalate, chi phí trung bình còn khoảng 20% so với gọi thẳng model lớn. Mấu chốt nằm ở chỗ: làm sao biết model nhỏ đang "không chắc"?
Đo confidence: logprobs là cách rẻ nhất
Hầu hết server tương thích OpenAI API (vLLM 0.6+, llama.cpp llama-server, Ollama bản mới) đều trả về logprobs. Với các task có output ngắn (label, yes/no, một con số), xác suất của token đầu tiên là tín hiệu khá tốt.
Đầu tiên, chạy thử model nhỏ bằng llama.cpp:
# llama.cpp build gần đây, model Qwen2.5-7B-Instruct quantize Q4_K_M
./llama-server -m models/qwen2.5-7b-instruct-q4_k_m.gguf --port 8080 -c 8192 -ngl 99
# Kiểm tra logprobs cho một request phân loại
curl -s http://localhost:8080/v1/chat/completions -H 'Content-Type: application/json' -d '{
"messages": [{"role": "user", "content": "Phân loại ticket: Tôi bị trừ tiền 2 lần. Trả lời 1 từ: billing, bug, account, other"}],
"max_tokens": 3, "temperature": 0, "logprobs": true, "top_logprobs": 3
}' | jq '.choices[0].logprobs.content[0].top_logprobs'
Nếu token top-1 có logprob khoảng -0.02 (xác suất ~98%) thì model chắc chắn. Nếu top-1 và top-2 sát nhau, ví dụ billing 0.48 và account 0.41, đó là lúc nên hỏi "đàn anh".
Đây là router mình dùng trong production (đã rút gọn):
import math
from openai import OpenAI # openai>=1.40
small = OpenAI(base_url='http://localhost:8080/v1', api_key='none')
large = OpenAI() # frontier model qua API, key lấy từ env
LABELS = ['billing', 'bug', 'account', 'other']
def classify(ticket: str, threshold: float = 0.85) -> dict:
prompt = f'Phân loại ticket sau. Chỉ trả lời 1 từ trong {LABELS}.\n\n{ticket}'
resp = small.chat.completions.create(
model='local',
messages=[{'role': 'user', 'content': prompt}],
max_tokens=3, temperature=0, logprobs=True, top_logprobs=5,
)
choice = resp.choices[0]
label = choice.message.content.strip().lower()
first = choice.logprobs.content[0]
prob = math.exp(first.logprob)
# Margin giữa top-1 và top-2: tín hiệu tốt hơn prob đơn thuần
tops = sorted((math.exp(t.logprob) for t in first.top_logprobs), reverse=True)
margin = tops[0] - (tops[1] if len(tops) > 1 else 0)
if label in LABELS and prob >= threshold and margin >= 0.5:
return {'label': label, 'source': 'small', 'conf': round(prob, 3)}
big = large.chat.completions.create(
model='YOUR_LARGE_MODEL',
messages=[{'role': 'user', 'content': prompt}],
max_tokens=5, temperature=0,
)
return {'label': big.choices[0].message.content.strip().lower(),
'source': 'large', 'conf': None}
Vài điểm quan trọng:
- Validate output trước khi tin confidence. Nếu model nhỏ trả về label không có trong danh sách, escalate luôn, khỏi cần xem prob.
- Dùng margin, không chỉ dùng prob. Một số model bị overconfident: prob 0.9 nhưng sai. Margin giữa top-1 và top-2 ổn định hơn.
- Ép output về dạng ngắn, có cấu trúc. Logprob của một đoạn văn 300 token gần như vô nghĩa.
Khi không có logprobs: self-consistency
Với output dài hơn (trích xuất JSON, sinh SQL), logprob trung bình không còn đáng tin. Cách thay thế: sample model nhỏ N lần với temperature > 0, nếu các lần trả lời khớp nhau thì chấp nhận, còn nếu lệch thì escalate.
sequenceDiagram
participant App
participant Small as Small model
participant Large as Large model
App->>Small: sample x3 (temperature=0.7)
Small-->>App: 3 câu trả lời
alt Đồng thuận >= 2/3
App-->>App: dùng kết quả đa số
else Bất đồng
App->>Large: gửi request + 3 bản nháp làm context
Large-->>App: kết quả cuối
end
import json
from collections import Counter
def extract_invoice(text: str, n: int = 3) -> dict:
resp = small.chat.completions.create(
model='local', n=n, temperature=0.7, max_tokens=300,
response_format={'type': 'json_object'},
messages=[{'role': 'user', 'content': f'Trích xuất JSON gồm vendor, total, date:\n{text}'}],
)
answers = []
for c in resp.choices:
try:
d = json.loads(c.message.content)
# Chuẩn hóa để so sánh: chỉ so field quan trọng
answers.append((str(d.get('vendor', '')).strip().lower(),
str(d.get('total', '')).replace(',', ''),
str(d.get('date', ''))))
except json.JSONDecodeError:
continue
if answers:
best, votes = Counter(answers).most_common(1)[0]
if votes >= 2:
return {'vendor': best[0], 'total': best[1], 'date': best[2], 'source': 'small'}
drafts = '\n'.join(c.message.content for c in resp.choices)
big = large.chat.completions.create(
model='YOUR_LARGE_MODEL', temperature=0,
response_format={'type': 'json_object'},
messages=[{'role': 'user', 'content':
f'Trích xuất JSON gồm vendor, total, date:\n{text}\n\nCác bản nháp (có thể sai):\n{drafts}'}],
)
return {**json.loads(big.choices[0].message.content), 'source': 'large'}
Mẹo nhỏ: gửi kèm các bản nháp của model nhỏ cho model lớn. Model lớn thấy ngay chỗ "tranh cãi" nên thường xử lý nhanh và chính xác hơn. Lưu ý là n=3 với llama.cpp sẽ tốn thêm compute local. Nếu GPU yếu thì giảm xuống 2 và yêu cầu khớp 100%.
Chọn threshold bằng dữ liệu, đừng đoán
Đây là bước mọi người hay bỏ qua, và cũng là bước quyết định thành bại. Quy trình của mình:
- Lấy 300–500 sample thật, có label đúng (có thể nhờ model lớn gán nhãn rồi review tay).
- Chạy model nhỏ trên toàn bộ tập, lưu lại
prob,marginvà việc nó đúng hay sai. - Sweep threshold từ 0.5 đến 0.99, với mỗi mức tính
escalation_ratevàaccuracytổng thể (giả định model lớn đúng với các case bị escalate). - Chọn điểm "khuỷu tay": tăng threshold thêm nữa thì accuracy gần như không tăng, còn chi phí lại tăng vọt.
Với bài toán ticket của mình, threshold 0.85 kèm margin 0.5 cho escalation rate khoảng 18%, accuracy chỉ kém 1.2% so với gọi toàn bộ lên model lớn. Lên 0.95 thì accuracy tăng thêm 0.4% nhưng escalation rate nhảy lên 41%, không đáng.
Những cái bẫy mình đã dính:
- Data drift: threshold calibrate tháng trước có thể sai tháng này, ví dụ khi có sản phẩm mới khiến ticket "lạ" tăng. Hãy theo dõi escalation rate trên dashboard. Nếu nó tăng đột ngột, đó là tín hiệu sớm của drift.
- Đổi quantization là phải calibrate lại. Q4 và Q8 của cùng một model cho phân bố logprob khác nhau, giống như bài trending về Gemma trả lời khác nhau trên laptop và trên T4.
- Latency cộng dồn: case bị escalate sẽ chịu latency của cả hai model. Nếu SLA chặt, đặt timeout cho model nhỏ (ví dụ 800ms), quá thời gian thì gọi thẳng model lớn.
Kết luận
Model cascading không phải trick học thuật. Đây là cách thực dụng nhất để cắt chi phí LLM mà không phải hy sinh chất lượng. Những việc nên làm ngay:
- Audit workload: xem bao nhiêu % request thực ra là task ngắn, có cấu trúc (classification, extraction). Đó là ứng viên số một cho cascade.
- Bắt đầu với logprobs + margin cho output ngắn, và self-consistency cho output có cấu trúc dài hơn.
- Luôn validate output (label hợp lệ, JSON parse được) trước khi xét confidence.
- Calibrate threshold trên 300+ sample có nhãn, chọn điểm khuỷu tay của đường cong cost/accuracy.
- Log toàn bộ case bị escalate. Đây là dataset quý để fine-tune model nhỏ, giúp escalation rate giảm dần theo thời gian.
- Monitor escalation rate như một metric production, vì nó vừa phản ánh chi phí vừa là cảnh báo sớm của data drift.
Một model nhỏ biết lúc nào mình không biết có giá trị hơn nhiều so với một model lớn được dùng cho mọi thứ.
Top comments (0)