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

Odpowiedni model do odpowiedniego zadania.

Page content

Uruchamianie modelu o 70 miliardach parametrów w celu podsumowania 200-zdaniowego e-maila jest marnotrawstwem. Zastosowanie modelu o 3 miliardach parametrów do recenzji kodu produkcyjnego jest ryzykowne. Większość systemów znajduje się gdzieś w połowie tej skali – i właśnie tam przydaje się routing modeli.

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

Schemat strategii routingu modeli LLM

Problem routingu

Zazwyczaj zaczynamy od jednego modelu i go używamy. Działa to dobrze, dopóki nie zauważy się kosztów, opóźnień lub obu na raz. Alternatywą jest budowa routera – mechanizmu, który decyduje, który model obsłuży dane żądanie.

W praktyce działają cztery strategie:

  1. Oparte na możliwościach – routing w zależności od tego, co model potrafi zrobić
  2. Oparte na kosztach – routing w zależności od tego, ile jesteśmy w stanie wydać
  3. Oparte na opóźnieniach – routing w zależności od tego, jak szybko potrzebujemy wyniku
  4. Hybrydowe – połączenie powyższych

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

Routing oparty na możliwościach

Najprostsze podejście. Zklasifikuj zadanie, wyślij je do modelu, który je obsłuży.

Zadanie Rozmiar modelu Przykłady
Klasyfikacja, tagowanie 1-3B Qwen2.5-1.5B, Gemma-2-2B
Podsumowanie, ekstrakcja 3-7B Qwen2.5-7B, Llama-3.1-8B
Generowanie kodu 7-14B Qwen2.5-Coder-7B, DeepSeek-Coder-V2
Złożone rozumowanie 14-32B Qwen2.5-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.5B parametrów poradzi sobie dobrze z klasyfikacją sentymentu. Po prostu nie napisze spójnej eseju.

Implementacja jest prosta:

ROUTING_RULES = {
    "classify": {"model": "qwen2.5-1.5b", "max_tokens": 100},
    "summarize": {"model": "qwen2.5-7b", "max_tokens": 500},
    "code_review": {"model": "qwen2.5-coder-7b", "max_tokens": 2000},
    "reason": {"model": "qwen2.5-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 klasyfikowały recenzję kodu jako „podsumowanie” i cicho traciły na jakości.

Routing oparty na kosztach

Lokalna inferencja świetnie się tu sprawdza. Modele lokalne są praktycznie darmowe po odliczeniu amortyzacji sprzętu. Karta RTX 5080 zwraca się w około sześć miesięcy przy umiarkowanym korzystaniu z API.

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

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

Routing oparty na budżecie stosuje mechanizm cofania się w miarę wydawania pieniędzy:

class CostAwareRouter:
    def __init__(self, budget_per_session: float = 0.10):
        self.budget = budget_per_session
        self.spent = 0.0
        self.models = {
            "cheap": {"model": "qwen2.5-7b", "cost": 0.0},
            "medium": {"model": "qwen2.5-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 przechodzeniem do tańszych opcji. Zaczynasz od Claude’a, przechodzisz do Qwen-32B, a następnie do Qwen-7B. Pod koniec długiej sesji wynik jest zauważalnie gorszy. Czy to ma znaczenie, zależy od tego, co budujesz.

Routing oparty na opóźnieniach

Narzędzia interaktywne 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łkowity 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

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

class LatencyAwareRouter:
    def __init__(self):
        self.model_latencies = {
            "qwen2.5-1.5b": {"first_token": 0.05, "complete": 0.5},
            "qwen2.5-7b": {"first_token": 0.15, "complete": 2.0},
            "qwen2.5-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 "qwen2.5-1.5b"

Liczby dotyczące opóźnień są przybliżone – zależą od sprzętu, kwantyzacji i rozmiaru wsadu. Pomiary wykonuj na własnym setupie.

Strategie awaryjne

Modele zawalają się. API ogranicza limity. Występują przekroczenia limitów czasu. Działającym wzorcem 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": "qwen2.5-32b", "timeout": 120},
            {"model": "qwen2.5-7b", "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 zawali się z powodu problemu z siecią lub klucza API.

Kiedy routing pomaga

Routing ma sens, gdy obciążenie jest mieszane. Jeśli w tym samym systemie robisz klasyfikację, podsumowanie i rozumowanie, router oszczędza pieniądze i zmniejsza opóźnienia.

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

Wczesne prototypowanie to kolejny powód, aby go pominąć. Najpierw spraw, żeby zadanie zadziałało z jednym modelem, a routing dodaj dopiero wtedy, gdy koszty lub opóźnienia staną się realnym problemem.

Kompromisy

Każda strategia routingu optymalizuje coś innego i ofiara jakąś inną cechą:

  • Jeden model — najprostszy, najdrogszy, stała jakość
  • Oparty na możliwościach — lepszy koszt, wyższa jakość dla danego zadania, umiarkowana złożoność
  • Oparty na kosztach — najtańszy, zmienna jakość, umiarkowana złożoność
  • Oparty na opóźnieniach — najszybszy, może obniżać jakość, umiarkowana złożoność
  • Hybrydowy — połączenie najlepszych cech, najtrudniejszy w implementacji

Systemy produkcyjne zazwyczaj zbiegają się do rozwiązania hybrydowego. Zacznij od routingu opartego na możliwościach, dodaj świadomość kosztów, gdy przyjdzie faktura, a świadomość opóźnień, gdy użytkownicy będą narzekać na wolność.

Powiązane

Subskrybuj

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