Agent Router & Model Cascading 2026 - Kiến trúc Routing Thông minh cho Multi-Agent với Semantic Classifier, Bandit Feedback, Redis và ClickHouse

Posted on: 4/15/2026 12:11:58 AM

Một trong những nghịch lý lớn nhất của kiến trúc multi-agent năm 2026 là ở chỗ: chi phí LLM không tăng vì người dùng hỏi nhiều hơn, mà vì mọi câu hỏi đều được gửi tới mô hình to nhất. Một câu "chào buổi sáng" tốn cùng số token tới Claude Opus 4.6 như một yêu cầu phân tích log phức tạp. Một truy vấn FAQ lặp đi lặp lại vẫn đi qua cả pipeline reasoning. Đây là lý do một lớp kiến trúc mới — Agent Router — đã trở thành thành phần gần như bắt buộc trong mọi hệ thống LLM sản xuất: nó là "bộ não đứng trước bộ não", chọn đúng mô hình, đúng độ sâu suy luận, đúng ngân sách token cho từng request. Bài viết đi sâu vào kiến trúc router hiện đại năm 2026 — từ semantic classifier, multi-armed bandit, cascade routing, cho tới cách đặt Redis làm state store, ClickHouse làm backbone telemetry và đóng gói toàn bộ thành một layer có thể đặt trước bất kỳ multi-agent framework nào.

47-80%Chi phí LLM giảm khi router được triển khai đúng cách
<5msĐộ trễ overhead điển hình của semantic router trên Redis
14%Cascade routing vượt routing thuần hoặc cascading thuần
10xChênh lệch giá token giữa Haiku và Opus trong cùng một family

1. Vì sao mọi multi-agent đều cần một router ở tầng đầu

Khi hệ thống chỉ chạy một mô hình duy nhất, câu chuyện rất đơn giản: mọi request đều đi thẳng xuống mô hình đó. Nhưng kiến trúc sản xuất năm 2026 gần như không còn dự án nào chỉ dùng một LLM. Một team điển hình kết hợp Claude Opus 4.6 cho reasoning phức tạp, Claude Sonnet 4.6 cho tác vụ thông thường, Claude Haiku 4.5 cho latency cực thấp, GPT-4o cho đa phương tiện, một mô hình open-source (Llama 3.3, Mistral) cho các luồng nhạy cảm về dữ liệu, và vài mô hình embedding chuyên biệt. Không có router, toàn bộ traffic đổ về một mô hình — hoặc tệ hơn: nhân viên tự chọn thủ công.

Điểm đau thực sự lộ ra khi nhìn vào phân phối request. Phân tích log LLM của các đội engineering chuyên nghiệp cho thấy một pattern rất ổn định: khoảng 60-70% request đơn giản tới mức mô hình rẻ nhất cũng trả lời hoàn hảo, 20-25% cần reasoning trung bình, và chỉ 5-15% thực sự cần mô hình đầu bảng. Nếu bạn đang gửi tất cả tới Opus, bạn đang đốt 80% ngân sách cho 15% lợi ích.

graph LR
    Users["User requests
100%"] --> Router["Agent Router"] Router -->|"~65%
greeting, FAQ, format"| Cheap["Haiku 4.5
$0.25/1M in"] Router -->|"~25%
general chat, code edit"| Mid["Sonnet 4.6
$3/1M in"] Router -->|"~10%
deep reasoning, research"| Big["Opus 4.6
$15/1M in"] Cheap --> Metrics["Telemetry
Redis + ClickHouse"] Mid --> Metrics Big --> Metrics Metrics -.->|"reward signal"| Router style Router fill:#e94560,stroke:#fff,color:#fff style Metrics fill:#f39c12,stroke:#fff,color:#fff style Cheap fill:#4CAF50,stroke:#fff,color:#fff style Mid fill:#0f3460,stroke:#fff,color:#fff style Big fill:#6610f2,stroke:#fff,color:#fff

Router không chỉ cắt chi phí. Nó cắt latency cho các truy vấn dễ (Haiku trả lời trong vài trăm mili-giây trong khi Opus mất vài giây), cắt rủi ro rate-limit trên mô hình đầu bảng (traffic đơn giản không bao giờ chạm tới đó), và trở thành điểm kiểm soát chính sách: một chỗ duy nhất để quyết định ai được dùng mô hình nào, theo budget nào, qua chặn nào.

2. Phân loại router — từ rule đơn giản tới bandit học online

Các router không sinh ra bình đẳng. Có ít nhất năm lớp tiếp cận đang song song tồn tại trong sản xuất năm 2026, và chúng hợp lý trong những ngữ cảnh khác nhau:

Lớp routerCơ chế quyết địnhTraining dataĐộ trễPhù hợp
Rule-basedRegex, keyword, length thresholdKhông cần<0.5 msMVP, luồng có format biết trước
Embedding k-NNTìm prompt gần nhất, mượn nhãn modelVài trăm cặp (prompt → model thắng)1-3 msBắt đầu nhanh, không có dataset preference
BERT/ModernBERT classifierFine-tune classifier phân loại 2-N lớpVài nghìn đến vài chục nghìn label5-15 ms (GPU)Sản xuất ổn định, domain tương đối cố định
Preference-trained (RouteLLM)Học từ preference data, điều khiển bằng cost thresholdChatbot Arena + augmentation2-10 msTeam có preference log hoặc chấp nhận public data
Bandit / Neural UCBCập nhật online dựa trên reward thực tếKhông cần cố định; học liên tục3-8 msTraffic lớn, nhu cầu thích nghi nhanh với model mới

Trên thực tế, hầu hết team sản xuất chín chắn dùng kiến trúc pha trộn: rule đi đầu để bắt các case rõ ràng (từ khóa nhạy cảm, độ dài thấp, API call cho analytics), embedding classifier làm xương sống cho phần lớn traffic, và một lớp bandit học online ngồi phía sau để tinh chỉnh lựa chọn khi môi trường thay đổi (model mới được release, giá thay đổi, phân phối traffic dịch chuyển).

Vì sao không có một router duy nhất chiến thắng mọi kịch bản

Rule-based quá cứng nhưng cực nhanh và dễ audit. BERT classifier cân bằng tốt nhưng phải retrain mỗi khi domain thay đổi. Bandit tự học nhưng cần đủ traffic để hội tụ. Pha trộn cho phép mỗi lớp làm tốt thứ nó mạnh, và router trở thành một pipeline nhiều tầng chứ không phải một mô hình duy nhất.

3. Semantic classifier — trái tim của router hiện đại

Trong hầu hết kiến trúc router sản xuất năm 2026, thành phần chịu trách nhiệm chính là một semantic classifier nhẹ, thường là ModernBERT hoặc một encoder-only fine-tune được train để gán nhãn "class" cho prompt — với mỗi class tương ứng một mô hình đích. Quy trình lấy cảm hứng từ các hệ thống routing của vLLM Semantic Router: prompt đi qua embedding model, vector được đưa vào một classifier head, classifier trả về phân phối xác suất trên các model, router chọn model có kỳ vọng lợi ích cao nhất sau khi tính đến cost.

import torch
from transformers import AutoTokenizer, AutoModel

class ModernBertRouter:
    LABELS = ["haiku", "sonnet", "opus", "open-source"]
    PRICES = {"haiku": 0.25, "sonnet": 3.0, "opus": 15.0, "open-source": 0.1}

    def __init__(self, model_path: str):
        self.tok = AutoTokenizer.from_pretrained(model_path)
        self.encoder = AutoModel.from_pretrained(model_path).eval()
        self.head = torch.load(f"{model_path}/router_head.pt").eval()

    @torch.no_grad()
    def classify(self, prompt: str) -> dict[str, float]:
        x = self.tok(prompt, truncation=True, max_length=256, return_tensors="pt")
        h = self.encoder(**x).last_hidden_state[:, 0]
        logits = self.head(h).squeeze(0)
        probs = torch.softmax(logits, dim=-1).tolist()
        return dict(zip(self.LABELS, probs))

    def choose(self, prompt: str, cost_weight: float = 1.0) -> str:
        probs = self.classify(prompt)
        # Expected utility: prob * quality - cost_weight * price
        # Quality prior: Opus 1.0, Sonnet 0.9, Haiku 0.75, open-source 0.7
        quality = {"haiku": 0.75, "sonnet": 0.9, "opus": 1.0, "open-source": 0.7}
        scores = {
            m: probs[m] * quality[m] - cost_weight * self.PRICES[m] / 15.0
            for m in self.LABELS
        }
        return max(scores, key=scores.get)

Điểm tinh tế của đoạn code trên không phải classifier, mà là cost_weight. Nó là núm vặn duy nhất cho phép runtime điều chỉnh vị trí trên đường cong cost-quality: cost_weight = 0 biến router thành "luôn chọn mô hình tốt nhất theo xác suất"; cost_weight cao biến router thành "chọn rẻ bất cứ khi nào không chắc chắn". Trong sản xuất, cost_weight thường được đặt theo loại request: free tier nhận cost_weight cao, enterprise tier nhận thấp hơn, internal debugging nhận gần 0.

4. Cascade routing — khi routing đơn thuần chưa đủ

Routing giải quyết câu hỏi "model nào cho request này", nhưng vẫn có bug cơ bản: classifier có thể đoán sai, và khi nó đoán sai, một prompt khó bị gửi tới Haiku và nhận một câu trả lời tệ. Cascade routing khắc phục bằng cách biến quá trình routing thành một chuỗi thử nhiều bước — bắt đầu ở model rẻ nhất, chỉ leo thang nếu kết quả không đạt một ngưỡng chất lượng nào đó.

graph TB
    Req["Request"] --> Stage1["Stage 1: Haiku 4.5
try first"] Stage1 --> Check1{"Chất lượng
đủ?"} Check1 -->|"đủ
~65%"| Done1["Trả về user"] Check1 -->|"không đủ"| Stage2["Stage 2: Sonnet 4.6"] Stage2 --> Check2{"Chất lượng
đủ?"} Check2 -->|"đủ
~25%"| Done2["Trả về user"] Check2 -->|"không đủ"| Stage3["Stage 3: Opus 4.6
~10% cuối"] Stage3 --> Done3["Trả về user"] style Stage1 fill:#4CAF50,stroke:#fff,color:#fff style Stage2 fill:#0f3460,stroke:#fff,color:#fff style Stage3 fill:#6610f2,stroke:#fff,color:#fff style Check1 fill:#f39c12,stroke:#fff,color:#fff style Check2 fill:#f39c12,stroke:#fff,color:#fff

Câu hỏi then chốt của cascade là: làm sao biết kết quả ở một stage là "đủ tốt"? Có bốn phương pháp chính được dùng trong sản xuất:

  • Log-probability threshold: nếu mô hình trả lời với độ tự tin thấp (entropy cao hoặc logprob thấp), leo thang. Đơn giản, không cần thêm call, hoạt động tốt với các task có câu trả lời rõ ràng (JSON, phân loại, extraction).
  • Self-verification: cho mô hình hỏi ngược "bạn có chắc chắn không" hoặc "liệt kê các giả định". Tốn thêm một lượt nhưng không phải gọi mô hình đắt hơn.
  • Small verifier model: một mô hình nhỏ (DeBERTa, ModernBERT) kiểm tra câu trả lời có hợp lý với prompt không. Rẻ, nhanh, và có thể fine-tune riêng.
  • LLM-as-judge ở tier thấp hơn: dùng Haiku để judge câu trả lời của Haiku. Trái với trực giác, điều này hoạt động tốt hơn mong đợi vì việc đánh giá thường dễ hơn sinh.

Nghiên cứu gần đây (OpenReview 2024, "A Unified Approach to Routing and Cascading for LLMs") chỉ ra rằng kết hợp routing với cascading đạt cost-quality tradeoff tốt hơn 14% so với dùng một trong hai. Ý tưởng: router chọn starting stage dựa trên độ khó dự đoán của prompt, cascade leo thang nếu cần. Pattern này gần như đã trở thành tiêu chuẩn de facto trong sản xuất cuối 2025 - đầu 2026.

Pattern tiết kiệm nhất

Gộp routing và cascade thành một pipeline hai bước: (1) semantic classifier chọn starting stage — phần lớn request bắt đầu ở Haiku, phần khó bắt đầu ở Sonnet, chỉ phần cực khó hoặc đã từng thất bại mới bắt đầu ở Opus; (2) một log-probability check nhẹ quyết định leo thang hay không. Trong đo lường của Mavik Labs và PremAI, pattern này đạt 47-65% giảm chi phí trên traffic thật mà chất lượng đầu ra bằng hoặc cao hơn so với "tất cả vào Opus".

5. Bandit feedback — router tự học từ kết quả thật

Rule và classifier đều là phương pháp "đóng băng": một khi deploy, chúng không học thêm. Nhưng môi trường LLM năm 2026 không đứng yên — giá thay đổi hàng quý, model mới xuất hiện hàng tháng, phân phối prompt của user dịch chuyển liên tục. Đây là sân chơi của multi-armed bandit: một lớp router học online, cập nhật chính sách mỗi khi có feedback thật.

Mô hình chuẩn: mỗi request tạo thành một "arm pull". Router chọn một model (arm), quan sát reward (chất lượng câu trả lời), cập nhật niềm tin về arm đó. Công thức cân bằng khai thác và khám phá quen thuộc — UCB, Thompson sampling, epsilon-greedy — đều hoạt động, với hai điều chỉnh quan trọng cho ngữ cảnh LLM:

import math
from dataclasses import dataclass

@dataclass
class ArmState:
    pulls: int = 0
    reward_sum: float = 0.0
    cost_sum: float = 0.0

class ContextualUcbRouter:
    def __init__(self, models: list[str], prices: dict[str, float],
                 cost_weight: float = 0.3):
        self.models = models
        self.prices = prices
        self.cost_weight = cost_weight
        self.arms: dict[tuple, dict[str, ArmState]] = {}

    def _context_key(self, prompt_emb: list[float]) -> tuple:
        # Quantize embedding to bucket similar prompts into same context
        return tuple(round(x, 1) for x in prompt_emb[:16])

    def choose(self, prompt_emb: list[float]) -> str:
        ctx = self._context_key(prompt_emb)
        if ctx not in self.arms:
            self.arms[ctx] = {m: ArmState() for m in self.models}
        arms = self.arms[ctx]
        total = sum(a.pulls for a in arms.values()) + 1
        best, best_score = None, -math.inf
        for m, a in arms.items():
            if a.pulls == 0:
                return m  # cold start — explore
            avg_reward = a.reward_sum / a.pulls
            ucb = avg_reward + math.sqrt(2 * math.log(total) / a.pulls)
            score = ucb - self.cost_weight * self.prices[m]
            if score > best_score:
                best, best_score = m, score
        return best

    def update(self, prompt_emb: list[float], model: str,
               reward: float, cost: float):
        ctx = self._context_key(prompt_emb)
        a = self.arms[ctx][model]
        a.pulls += 1
        a.reward_sum += reward
        a.cost_sum += cost

Hai nét đáng chú ý. Thứ nhất, context key biến một bandit phẳng thành bandit ngữ cảnh: router học riêng cho từng "vùng prompt" thay vì một chính sách toàn cục. Thứ hai, reward = quality - cost_weight * price — bandit không tối ưu chỉ chất lượng, nó tối ưu chất lượng có trừ chi phí. Điều này khớp đúng với cái team sản xuất thực sự quan tâm: ROI, không phải accuracy tuyệt đối.

Cạm bẫy cold-start

Bandit chỉ hoạt động tốt khi đã có đủ feedback. Với traffic thấp hoặc sau khi thêm một model mới, các arm rỗng có thể dẫn tới quyết định tệ. Pattern sản xuất hay dùng: khởi tạo prior bằng kết quả từ classifier đã train sẵn, rồi để bandit cập nhật dần. Đây là ý tưởng của NeuralUCB và các biến thể "warm-started bandit" gần đây.

6. Redis làm state store cho router — tại sao đây là lựa chọn đúng

Một router có ba loại state cần giữ cực nhanh: (1) bảng arm của bandit — cần đọc/ghi sub-ms vì mỗi request đều chạm, (2) semantic cache — lưu các prompt đã được route trước đó để trả lời lại ngay mà không cần chạy classifier, (3) token budget counter — đếm chi phí theo tenant/user/API key cho rate limiting.

Không có nhiều lựa chọn có thể làm cả ba với cùng một data store. Redis là ứng cử viên tự nhiên. Với Redis 8, ba mẫu hình sau khớp sát vào ba thành phần của router:

graph TB
    Router["Agent Router"] --> RedisHash["Redis HASH
bandit:arms:{ctx}"] Router --> RedisVS["Redis 8 Vector Set
router:cache"] Router --> RedisStream["Redis Stream
router:events"] Router --> RedisCounter["Redis INCRBYFLOAT
budget:{tenant}:{day}"] RedisHash -->|"HINCRBYFLOAT pulls,reward"| BandStore["Per-arm statistics
contextual UCB"] RedisVS -->|"VSIM embedding → model"| SemCache["Semantic cache hit
skip classifier"] RedisStream -->|"XADD request"| Ingest["Batch → ClickHouse
via consumer group"] RedisCounter -->|"check vs cap"| Budget["Tenant budget
throttle / downgrade"] style Router fill:#e94560,stroke:#fff,color:#fff style RedisHash fill:#dc3545,stroke:#fff,color:#fff style RedisVS fill:#dc3545,stroke:#fff,color:#fff style RedisStream fill:#dc3545,stroke:#fff,color:#fff style RedisCounter fill:#dc3545,stroke:#fff,color:#fff

Chi tiết các pattern:

  • HASH cho bandit arms: HINCRBYFLOAT bandit:arms:{ctx}:{model} pulls 1HINCRBYFLOAT ... reward 0.87 cho phép cập nhật atomic dưới 0.5 ms. Không cần lock, không cần transaction.
  • Vector Set cho semantic cache của router: VADD router:cache VALUES 1536 ... req:123 SETATTR '{"model":"sonnet","reward":0.9}' — lần sau khi prompt tương tự xuất hiện, VSIM router:cache ... COUNT 1 có thể trả về ngay "model=sonnet" mà không chạy classifier BERT. Thường hit rate 30-50% trên traffic có tính lặp lại cao.
  • Stream cho event log: XADD router:events * req_id 123 model sonnet latency 420 reward 0.9. Consumer group batch pull và đẩy vào ClickHouse mỗi 500 ms.
  • Counter cho budget: INCRBYFLOAT budget:tenant42:2026-04-14 0.0034 (chi phí USD ước lượng của request). Một GET nhanh để so với cap; vượt cap thì downgrade xuống model rẻ hoặc trả 429.

Lý do Redis thắng ở đây không phải "nhanh nhất" — nhiều KV store cũng sub-ms. Lý do là bốn pattern này nằm cùng một cluster. Không cần join, không cần sync, không cần hai lớp cache. Với router cần chạm 5-6 key/hash/set khác nhau cho mỗi request, việc tất cả nằm cùng cluster (và thường cùng một pipeline TCP) là khác biệt giữa overhead 2 ms và overhead 20 ms.

7. ClickHouse — backbone analytics cho router

Redis là nơi lưu state "sống"; ClickHouse là nơi lưu lịch sử và trả lời các câu hỏi phân tích. Mỗi event router sinh ra được stream từ Redis Stream vào ClickHouse qua Kafka Engine hoặc qua một consumer Python nhỏ. Schema điển hình đủ để trả lời mọi câu hỏi kinh doanh về hiệu quả router:

CREATE TABLE router_events (
    ts              DateTime64(3),
    request_id      String,
    tenant_id       LowCardinality(String),
    user_id         String,
    api_key_hash    FixedString(16),
    prompt_tokens   UInt32,
    response_tokens UInt32,
    prompt_hash     UInt64,
    prompt_class    LowCardinality(String),    -- 'chat', 'code', 'research', 'sql'
    classifier_score Float32,                  -- độ tự tin của classifier
    chosen_model    LowCardinality(String),    -- 'haiku', 'sonnet', 'opus', 'llama-70b'
    chosen_reason   LowCardinality(String),    -- 'classifier', 'cache_hit', 'cascade', 'bandit'
    cascade_depth   UInt8,                     -- 1, 2, 3 (số model đã thử)
    cost_usd        Float64,
    latency_ms      UInt32,
    ttft_ms         UInt32,                    -- time to first token
    reward          Float32,                   -- quality signal (thumbs up/down, judge score)
    was_downgraded  UInt8,                     -- đã bị force xuống model rẻ vì budget
    cost_weight     Float32                    -- giá trị cost_weight tại thời điểm route
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(ts)
ORDER BY (tenant_id, chosen_model, ts)
TTL ts + INTERVAL 90 DAY;

Với bảng này, một dashboard duy nhất trả lời được các câu hỏi quyết định hàng ngày: tenant nào đang chi nhiều nhất? Classifier nhầm model nào thường xuyên nhất (cascade_depth > 1)? Tỷ lệ cache_hit ở giờ cao điểm có đang tụt không? Cost per request trung bình theo phân lớp prompt? Cost_weight bao nhiêu thì đạt điểm tối ưu reward/cost?

Vẻ đẹp của pattern này là nó khép vòng feedback. ClickHouse không chỉ là nơi xem dashboard — nó là nơi router đọc reward để cập nhật bandit offline, nơi team đo cost_weight tối ưu, nơi phát hiện drift phân phối prompt. Router và ClickHouse tạo thành một hệ thống học liên tục: ClickHouse lưu tín hiệu, Redis lưu trạng thái, router vận hành inference, event pipeline nối chúng lại.

8. Pipeline hoàn chỉnh — ghép các mảng vào một request thực tế

Đến đây, hãy xem pipeline đầy đủ của một request đi qua router sản xuất. Từ khi user nhấn enter tới khi nhận được token đầu tiên, có bảy mốc cần đo và kiểm soát:

sequenceDiagram
    participant U as User
    participant GW as API Gateway
    participant R as Agent Router
    participant RS as Redis
    participant CLS as Classifier
    participant M as Model (Haiku/Sonnet/Opus)
    participant CH as ClickHouse
    U->>GW: prompt + api_key
    GW->>R: forward + tenant_id
    R->>RS: GET budget:{tenant}:{day}
    RS-->>R: 0.85 (85% used)
    R->>RS: VSIM router:cache  (semantic cache)
    RS-->>R: nearest={model:sonnet,score:0.94}
    alt cache hit (score > 0.93)
        R->>M: call sonnet
    else cache miss
        R->>CLS: classify(prompt)
        CLS-->>R: probs={haiku:0.7, sonnet:0.25, opus:0.05}
        R->>RS: HGETALL bandit:arms:{ctx}
        RS-->>R: arm stats
        R->>R: compute UCB - cost_weight * price
        R->>M: call haiku (cheapest with acceptable UCB)
    end
    M-->>R: response + logprobs
    R->>R: check confidence threshold
    alt confidence low
        R->>M: call sonnet (cascade up)
        M-->>R: response
    end
    R->>RS: XADD router:events ...
    R->>RS: HINCRBYFLOAT bandit:arms ...
    R->>RS: INCRBYFLOAT budget:{tenant}:{day} ...
    R->>RS: VADD router:cache ...
    R-->>GW: response
    GW-->>U: stream tokens
    RS-->>CH: consumer batch (async)

Quan sát ba điều quan trọng về pipeline này. Một, mọi tương tác với Redis đều được gộp vào một pipeline TCP — GET budget + VSIM cache + HGETALL arms được gửi đi cùng lúc, chỉ chờ một round-trip. Hai, classifier được skip hoàn toàn khi cache hit, và cache hit chiếm 30-50% traffic trong đa số ứng dụng có user-facing. Ba, cập nhật bandit và cache xảy ra sau khi phản hồi tới user, nên không cản trở latency end-to-end.

Kết quả điển hình: p50 overhead của router ~5 ms, p99 ~15 ms. Trên latency tổng 2-4 giây của một LLM call, đây là lớp "miễn phí" về mặt UX.

9. Token budget & cost governance — chặng cuối cùng

Một router hoàn chỉnh không chỉ chọn model; nó còn từ chối chọn nếu ngân sách đã hết. Trong các tổ chức lớn, nơi hàng chục team cùng chia sẻ một cluster LLM, đây là tính năng sống còn. Pattern chuẩn gồm ba lớp:

  • Lớp tenant: mỗi tenant có daily cap (ví dụ 50 USD/ngày). INCRBYFLOAT budget:{tenant}:2026-04-14 cost sau mỗi call; EXPIRE về lúc nửa đêm. Vượt cap → downgrade về model rẻ nhất hoặc trả 429.
  • Lớp API key: trong tenant, mỗi API key có quota riêng — quan trọng khi một service misbehave làm cạn ngân sách.
  • Lớp global: kill-switch để nếu tổng chi phí giờ vừa qua vượt ngưỡng bất thường, router chuyển toàn bộ traffic không-critical về model rẻ.

Kỹ thuật: dùng sorted set Redis để lưu quota theo slot thời gian (ZADD budget:tenant42 {unix_ts} {cost}), sau đó ZRANGEBYSCORE để tính tổng chi phí trong 1 giờ/24 giờ qua. Chi phí kiểm tra: một lệnh Redis, dưới 1 ms.

Graceful downgrade thay vì hard cap

Hard cap (vượt cap là trả 429) rất tệ về UX. Pattern tốt hơn: khi ngân sách đạt 80%, cost_weight tự động nhân 2x; đạt 95%, nhân 5x. Router sẽ chọn model rẻ hơn cho tất cả request "không bắt buộc phải dùng model lớn". User không bao giờ thấy lỗi, chỉ thấy câu trả lời hơi khác. Đây là cách các platform như Cursor, Perplexity và Phind áp dụng trong sản xuất.

10. Router cho Claude Code và multi-agent framework khác

Câu hỏi thực tế: router có áp dụng cho Claude Code không? Câu trả lời là có — và đây là một chỗ thú vị vì Claude Code chạy nhiều sub-agent, mỗi sub-agent có thể chọn model riêng qua cấu hình. Một router sản xuất đặt trước Claude Code (hoặc đặt trong qua custom sub-agent dispatcher) cho phép:

  • Sub-agent Explore dùng Haiku vì phần lớn thời gian chỉ duyệt file và grep — reasoning nhẹ.
  • Sub-agent Plan dùng Sonnet vì cần reasoning trung bình về thiết kế.
  • Agent chính dùng Opus khi cần tổng hợp nhiều nguồn thông tin hoặc khi cascade đã leo lên.
  • Các tool call nội bộ (git status, test runner) không cần LLM nào cả — router phát hiện pattern lặp lại, trả từ cache.

Pattern tương tự áp dụng cho LangGraph, OpenAI Swarm, CrewAI, AutoGen. Thay vì để framework trực tiếp cài model hardcoded, team đặt một layer gateway ở giữa — thường là LLM Gateway (xem bài về LLM Gateway trên anhtu.dev) — và router là một middleware trong gateway đó. Kết quả: bạn có thể thay model, thay giá, thay chính sách routing mà không phải chỉnh code agent.

11. Drift, retraining và lifecycle của router

Router không phải thứ "deploy xong là xong". Ba nguồn drift chính cần theo dõi và xử lý:

  1. Prompt distribution drift: user hỏi kiểu khác sau khi launch feature mới. Classifier bị "ngoài phân phối" và đoán sai nhiều hơn. Theo dõi: classifier_score trung bình theo ngày — rơi xuống là dấu hiệu.
  2. Model quality drift: provider update model, đôi khi tốt lên, đôi khi tệ đi. Theo dõi: reward trung bình theo model theo tuần.
  3. Price drift: giá token thay đổi hàng quý. Reload bảng giá vào router, cost_weight tự dịch theo.

Lịch trình thường thấy: retrain classifier 4-8 tuần/lần, refresh bandit online liên tục, cập nhật bảng giá ngay khi provider thông báo, review cost_weight hàng tháng theo KPI tài chính. Team nên có sẵn một pipeline ClickHouse → Parquet → training notebook, không phải dựng từ đầu mỗi lần.

Shadow routing — cách retrain an toàn

Khi deploy classifier mới, đừng switch traffic ngay. Chạy shadow: router cũ quyết định model, router mới cũng quyết định nhưng không được thực thi, cả hai quyết định được log vào ClickHouse. So sánh dự đoán, đo hiệu quả giả lập bằng reward mà router cũ thu được. Nếu router mới khớp hoặc vượt trên tập control, switch. Pattern này tránh mọi "big bang" và cho phép rollback tức thì.

12. So sánh các framework router mã nguồn mở 2026

Năm 2026 có vài framework router trưởng thành đủ để team có thể chọn thay vì tự viết từ đầu:

FrameworkCơ chế chínhĐiểm mạnhHạn chế
vLLM Semantic RouterModernBERT classifier + HNSW semantic cache (Redis/Milvus)Envoy ExtProc, hỗ trợ jailbreak detect, PII check, phân loại 14 MMLU categoriesKhớp sâu với vLLM stack, cần hạ tầng Envoy
RouteLLM (LMSYS)4 router types: SW ranking, BERT, causal LLM, matrix factorizationPreference-data driven, benchmark rất kỹ, có pretrained modelThiết kế cho pair 2 model, mở rộng N model cần retrain
NVIDIA AI Blueprints llm-routerRule + fine-tuned classifier + cascadeGắn sẵn với NeMo Guardrails, có blueprint sản xuấtNặng nghiêng về stack NVIDIA
Aurelio Labs semantic-routerEmbedding similarity + ruleĐơn giản, dễ hiểu, rất nhẹKhông có cascade, không bandit
Martian RouterProprietary; routing theo cost-quality learned từ trafficNgắm sản xuất enterprise, API OpenAI-compatClosed source, phụ thuộc vendor

Lựa chọn thực dụng: bắt đầu với Aurelio semantic-router hoặc tự viết một rule + embedding k-NN trong vài trăm dòng Python để có baseline chạy được. Khi traffic vượt vài trăm nghìn request/ngày và chi phí LLM vượt 5000 USD/tháng, di cư sang vLLM Semantic Router hoặc tự build với ModernBERT + RouteLLM preference dataset. Bandit là bước cuối — chỉ cần khi traffic đủ lớn để việc khám phá online có ý nghĩa thống kê (thường >100k request/ngày cho bandit hội tụ nhanh).

13. Checklist đưa Agent Router vào production

  • Đã phân tích log 4 tuần gần nhất để biết phân phối thực tế của prompt theo độ khó — không đoán, phải đo.
  • Đã chọn cơ chế router phù hợp với quy mô (rule → embedding → BERT → bandit) và không chọn lớn quá tầm.
  • Đã có ground truth nhỏ (50-200 mẫu) để đo classifier accuracy trước khi deploy lần đầu.
  • Cost_weight được cấu hình qua flag runtime, không hardcode — để tinh chỉnh mà không deploy.
  • Redis cluster có đủ RAM cho bandit state, semantic cache, token budget; và có replica cho VSIM.
  • ClickHouse đã có schema router_events, partition theo tháng, TTL 60-90 ngày.
  • Dashboard Grafana theo dõi: p95 latency router overhead, cost per request trung bình, cascade depth, classifier confidence, hit rate semantic cache, budget theo tenant.
  • Có shadow routing pipeline để test classifier mới mà không ảnh hưởng sản xuất.
  • Có kill-switch để trong sự cố có thể tạm dừng router và đổ toàn bộ traffic về một model duy nhất.
  • Tài liệu rõ policy downgrade khi budget sắp cạn — user tier nào sẽ bị ảnh hưởng thế nào.

14. Tổng kết

Agent Router là thành phần mà ít ai nhắc tới khi nói về "kiến trúc AI" nhưng lại quyết định kinh tế của mọi hệ thống multi-agent sản xuất năm 2026. Nó trả lời đúng một câu hỏi duy nhất — model nào, stage nào, cho request nào, dưới budget gì — nhưng trả lời đúng câu đó cắt giảm 47-80% chi phí LLM và giảm latency trung bình đáng kể. Trái tim của router hiện đại là semantic classifier nhẹ, đệm bằng semantic cache trên Redis Vector Set, leo thang qua cascade khi cần, học online qua bandit, và đo đạc mọi quyết định trong ClickHouse để khép vòng feedback.

Điểm đáng lưu ý nhất là router không thay thế các thành phần khác của stack AI — LLM Gateway, Agent Memory, Semantic Cache, Guardrails, Observability vẫn còn đó. Router là lớp điều phối đặt giữa chúng, biến một tập mô hình phân tán thành một "tổ hợp mô hình" hoạt động như một hệ thống duy nhất. Với team engineering đang xây agent năm 2026, câu hỏi không còn là "có nên thêm router không" mà là "router của tôi đang vận hành ở lớp nào — rule, embedding, hay bandit — và có còn là bottleneck của cost không". Nếu trả lời của bạn cho câu thứ hai vẫn là "chưa biết", đây là nơi ROI cao nhất trong stack AI để bắt đầu đào tiếp.

Nguồn tham khảo