Routing modeli: przestań używać jednego modelu do wszystkiego

Odpowiedni model dla odpowiedniego zadania.

Page content

Uruchamianie modelu o 70 miliardach parametrów w celu podsumowania 200-znakowego e-maila jest marnotrawstwem. Zastosowanie modelu o 3 miliardach parametrów do recenzji kodu produkcyjnego jest bezmyślną ryzykownością. Większość systemów funkcjonuje gdzieś w tym spektrum – i tutaj z pomocą przychodzi routing modeli.

Dopasowuje on złożoność zadania do możliwości modelu. Kompromisy są realne, ale oszczędności również.

LLM model routing strategies diagram

Problem routingu

Zazwyczaj zaczynamy od jednego modelu i trzymamy się go. Działa to dobrze, dopóki nie zauważymy kosztów, opóźnień lub obu jednocześnie. Alternatywą jest zbudowanie routera – czegoś, co decyduje, który model obsłuży dane żądanie.

W praktyce sprawdzają się cztery strategie:

  1. Oparta na możliwościach – routing w zależności od tego, co model potrafi
  2. Z uwzględnieniem kosztów – routing w zależności od tego, ile jesteś skłonny wydać
  3. Z uwzględnieniem opóźnień (latencji) – routing w zależności od tego, jak szybko potrzebujesz wyniku
  4. Hybrydowa – połączenie powyższych

Każda z nich optymalizuje coś innego. Wybór jednej to zwykle decyzja o tym, co boli najbardziej.

Routing oparty na możliwościach

Najprostsze podejście. Sklasyfikuj zadanie, prześlij je do modelu, który je obsługuje.

Zadanie Rozmiar modelu Przykłady
Klasyfikacja, tagowanie 1-3B Qwen3-1.7B, Gemma-2-2B
Podsumowanie, ekstrakcja 3-7B Qwen3-8B, Llama-3.1-8B
Generowanie kodu 7-14B Qwen2.5-Coder-7B, DeepSeek-Coder-V2
Złożone rozumowanie 14-32B Qwen3-32B, Llama-3.1-70B
Twórcze pisanie, analiza 32B+ Qwen2.5-72B, Claude, GPT-4

Jeśli zadanie nie wymaga większego modelu, nie używaj go. Model o 1.5 miliarda parametrów świetnie radzi sobie z klasyfikacją sentymentu. Po prostu nie napisze spójnej eseju.

Implementacja jest prosta:

ROUTING_RULES = {
    "classify": {"model": "qwen3-1.7B", "max_tokens": 100},
    "summarize": {"model": "qwen3-8B", "max_tokens": 500},
    "code_review": {"model": "qwen2.5-coder-7b", "max_tokens": 2000},
    "reason": {"model": "qwen3-32b", "max_tokens": 4000},
    "creative": {"model": "claude-sonnet-4", "max_tokens": 8000},
}

def route_request(task_type: str) -> dict:
    return ROUTING_RULES.get(task_type, ROUTING_RULES["reason"])

Haczykiem jest sama klasyfikacja. Jeśli źle określisz typ zadania, wyślesz je do niewłaściwego modelu. Widziałem systemy, które sklasyfikowały recenzję kodu jako „podsumowanie” i cicho straciły na jakości.

Routing z uwzględnieniem kosztów

Tu błyszczy się wnioskowanie lokalne. Modele lokalne są w praktyce darmowe po amorystyzacji sprzętu. RTX 5080 zwrotuje się w około sześciu miesięcy przy umiarkowanym korzystaniu z API.

Model Wejście ($/M tokenów) Wyjście ($/M tokenów) Koszt lokalny/godzinę
GPT-4o $2.50 $10.00
Claude Sonnet 4 $3.00 $15.00
Qwen2.5-72B (API) $0.50 $2.00
Qwen3-32B (lokalnie) $0.00 $0.00 ~$0.10
Qwen3-8B (lokalnie) $0.00 $0.00 ~$0.05

Jeśli przetwarzasz tysiące żądań na sesję, nawet $0.05 za prąd bije $15 za milion tokenów.

Routing oparty na budżecie cofa się w miarę wydawania:

class CostAwareRouter:
    def __init__(self, budget_per_session: float = 0.10):
        self.budget = budget_per_session
        self.spent = 0.0
        self.models = {
            "cheap": {"model": "qwen3-8B", "cost": 0.0},
            "medium": {"model": "qwen3-32b", "cost": 0.0},
            "expensive": {"model": "claude-sonnet-4", "cost": 0.000015},
        }

    def route(self, task: str) -> str:
        ratio = self.spent / self.budget
        if ratio < 0.5:
            return self.models["expensive"]["model"]
        elif ratio < 0.8:
            return self.models["medium"]["model"]
        return self.models["cheap"]["model"]

Jakość spada wraz z cofaniem się. Zaczynasz od Claude’a, przechodzisz do Qwen3-32B, a następnie do Qwen3-8B. Pod koniec długiej sesji wynik jest wyraźnie gorszy. Czy to ma znaczenie, zależy od tego, co budujesz.

Routing z uwzględnieniem opóźnień (latencji)

Interaktywne narzędzia potrzebują szybkich pierwszych tokenów. Zadania wsadowe mogą czekać. Różnica to zwykle czynnik pięciokrotny w rozmiarze modelu.

Przypadek użycia Pierwszy token Całość Maks. rozmiar modelu
Czat w czasie rzeczywistym < 200ms < 2s < 7B
Narzędzia interaktywne < 500ms < 5s < 14B
Przetwarzanie wsadowe < 1s < 30s Dowolny
Badania/analiza < 2s < 60s Dowolny

Gdy strumieniujesz tokeny do użytkownika, to opóźnienie pierwszego tokenu jest tym, co odczuwa. Model o 32 miliardach parametrów, który potrzebuje pół sekundy na start, wydaje się ociężały w porównaniu do modelu o 1.5 miliarda parametrów, który odpala natychmiastowo.

class LatencyAwareRouter:
    def __init__(self):
        self.model_latencies = {
            "qwen3-1.7b": {"first_token": 0.05, "complete": 0.5},
            "qwen3-8B": {"first_token": 0.15, "complete": 2.0},
            "qwen3-32b": {"first_token": 0.5, "complete": 10.0},
            "claude-sonnet-4": {"first_token": 0.3, "complete": 5.0},
        }

    def route(self, target_latency: float) -> str:
        for model, latencies in sorted(
            self.model_latencies.items(),
            key=lambda x: x[1]["complete"]
        ):
            if latencies["complete"] <= target_latency:
                return model
        return "qwen3-1.7b"

Te liczby opóźnień są przybliżone – zależą od sprzętu, kwantyzacji i rozmiaru wsadu. Zmierz je na własnym setupie.

Strategie awaryjne (fallback)

Modele się psują. API nakłada limity. Występują timeouty. Wzorcem, który działa, jest łańcuch awaryjny, uporządkowany od najlepszego do najbardziej niezawodnego:

class FallbackRouter:
    def __init__(self):
        self.fallback_chain = [
            {"model": "claude-sonnet-4", "timeout": 30},
            {"model": "qwen2.5-72b", "timeout": 60},
            {"model": "qwen3-32b", "timeout": 120},
            {"model": "qwen3-8b", "timeout": 300},
        ]

    def route_with_fallback(self, prompt: str) -> str:
        for config in self.fallback_chain:
            try:
                return self.call_model(
                    config["model"], prompt,
                    timeout=config["timeout"]
                )
            except (TimeoutError, APIError) as e:
                log.warning(f"Model {config['model']} failed: {e}")
                continue
        raise RuntimeError("All fallback models failed")

Ostatni model w łańcuchu powinien być lokalny. Jest wolniejszy, ale nie zawiedzie z powodu problemu z siecią lub klucza API.

Kiedy routing pomaga

Routing ma sens, gdy Twoje obciążenie jest mieszane. Jeśli w tym samym systemie wykonujesz klasyfikację, podsumowanie i rozumowanie, router oszczędza pieniądze i czas.

Nie ma sensu, gdy wszystko, co robisz, ma tę samą złożoność. Po prostu użyj modelu, który dobrze radzi sobie z tym zadaniem. Router dodaje złożoność, której nie potrzebujesz.

Wczesne prototypowanie to kolejny powód, by go ominąć. Zabezpiecz zadanie z jednym modelem, a następnie dodaj routing, gdy koszty lub opóźnienia faktycznie staną się problemem.

Kompromisy

Każda strategia routingu optymalizuje coś i ofiarowuje coś innego:

  • Pojedynczy model – najprostszy, najdrogszy, stała jakość
  • Oparty na możliwościach – lepszy koszt, wyższa jakość per zadanie, umiarkowana złożoność
  • Z uwzględnieniem kosztów – najtańszy, zmienna jakość, umiarkowana złożoność
  • Z uwzględnieniem opóźnień – najszybszy, może poświęcać jakość, umiarkowana złożoność
  • Hybrydowa – najlepsze z całego zestawu, najbardziej złożona do implementacji

Systemy produkcyjne zwykle zbiegają się do hybrydowej. Zacznij od routingu opartego na możliwościach, dodaj świadomość kosztów, gdy przyjdzie rachunek, dodaj świadomość opóźnień, gdy użytkownicy skąplą się na powolność.

Powiązane

Subskrybuj

Otrzymuj nowe wpisy o systemach, infrastrukturze i inżynierii AI.