Rutarea modelelor în producție: OpenAI, Anthropic și modele open-weight după cost și latență

Majoritatea echipelor aleg un model, îl conectează peste tot și descoperă simultan cât costă și ce latență p95 are. Rutarea rezolvă ambele probleme doar dacă se bazează pe clase de taskuri și măsurători, nu pe preferințe. Aceasta este varianta pe care o reconstruim de fiecare dată.

Rutarea este o decizie de produs, nu o preferință de model

O aplicație în producție nu face un singur tip de apel către un model de limbaj. Clasifică un tichet de suport, extrage câmpuri dintr-un document, rescrie o interogare de căutare, redactează un răspuns și rezolvă ocazional probleme cu adevărat dificile. Cerințele sunt complet diferite, iar dacă trimiți totul către același model de top plătești prea mult pentru cele 90% de sarcini simple, fără să oferi rezultate mai bune celorlalte.

ClasificareExtragereRescriere / rezumareText pentru utilizatoriAgenți / bucle de tool-uriContext lungRouterfuncție pură, testată unitarclasă de task + limite → nivelMiccost ●○○latență ●○○Mediucost ●●○latență ●●○De vârfcost ●●●latență ●●●fallbackfallback
Fig. 1Fiecare apel declară o clasă de task; routerul îi asociază un nivel

Costurile și capabilitățile se schimbă constant, așa că articolul folosește intenționat termeni relativi — nivel inferior, mediu și superior — în locul unor modele concrete, menționate pe nume și cu prețurile lor curente. Structura de rutare supraviețuiește lansărilor de modele; un nume de model scris în cod nu supraviețuiește.

Definește întâi clasele de taskuri

Înainte să scrii un router, inventariază toate apelurile către modele din produs și încadrează fiecare apel într-o clasă. În majoritatea produselor sunt doar cinci sau șase.

  • Clasificare și rutare — input scurt, output minuscul, un set fix de etichete. Modelele mici sunt de obicei imposibil de deosebit de cele mari aici, iar acesta este adesea apelul tău cu cel mai mare volum.
  • Extragere — output structurat dintr-un document sau dintr-un mesaj. Este constrâns de o schemă, deci erorile sunt vizibile, iar apelurile pot fi reluate cu un cost mic.
  • Rescriere și rezumare — input mediu, output mediu, calitatea contează, dar nu este nevoie de un model de frontieră.
  • Redactare de text destinat utilizatorilor — calitatea este produsul. Aici un model mai bun chiar se plătește singur.
  • Raționament în mai mulți pași și fluxuri cu agenți — apeluri către instrumente într-o buclă, unde un model mai slab eșuează într-un mod costisitor: nu dă doar un răspuns mai slab, ci face și cinci apeluri către instrumente în plus.
  • Context lung — fereastra de context îți dictează alegerea, indiferent de celelalte preferințe.

Declară clasa la fiecare apel. Tot ce urmează depinde de faptul că apelantul precizează tipul de task, nu de un router care încearcă să îl ghicească din textul promptului.

Semnalele care decid efectiv ruta

Patru semnale fac aproape toată treaba, în ordinea asta.

VERIFICATE ÎN ACEASTĂ ORDINELungimea contextuluiconstrângere dură: dacă nu încape,nimic altceva nu contează1Folosirea tool-urilorbuclele în mai mulți pași urcă2Complexitatea taskuluidin clasă, niciodatădin analiza promptului3Streamingtimpul până laprimul token contează4NIVEL + BUGET DE TOKENI + TIMEOUT
Fig. 2Cele patru semnale, verificate în ordine
  • Lungimea contextului — o constrângere dură. Dacă inputul nu încape, nimic altceva nu contează, iar strategiile de trunchiere trebuie aplicate înainte de rutare, nu în interiorul routerului.
  • Folosirea instrumentelor — în cazul apelurilor către instrumente în mai mulți pași, respectarea deficitară a instrucțiunilor costă mult mai mult decât în cazul unei generări simple. Rutează aceste bucle către nivelul imediat superior, iar costul scade de obicei, pentru că bucla se termină mai devreme.
  • Complexitatea taskului — aproximeaz-o din clasă, nu printr-o analiză sofisticată a promptului. Euristicile care citesc promptul ca să estimeze dificultatea sunt o sursă de comportament imprevizibil și de buguri greu de diagnosticat.
  • Streaming — dacă un om urmărește cum apar tokenii, timpul până la primul token domină calitatea percepută. Un model cu un timp până la primul token cu 30% mai mare este perceput ca mai slab decât unul cu un scor cu 30% mai mic pe setul tău de evaluare.

Două semnale par tentante, dar sunt de obicei greșeli: folosirea unui model de clasificare pentru a ruta fiecare prompt al utilizatorului (ai adăugat un drum dus-întors prin rețea și un nou punct de eșec la fiecare cerere) și rutarea pe baza prețurilor preluate în timp real (comportamentul sistemului se schimbă din motive pe care nu le poate reproduce nimeni).

Un router mic

Păstrează routerul ca funcție pură. Primește descrierea taskului și returnează un nivel împreună cu limitele asociate. Nu face apeluri de rețea, nu are stare și poate fi testat unitar în mod exhaustiv — ceea ce contează, pentru că aceasta este funcția care îți decide factura lunară.

type Tier = 'small' | 'mid' | 'large';

type Task = {
  kind: 'classify' | 'extract' | 'summarise' | 'draft' | 'reason' | 'agent';
  inputTokens: number;
  needsTools: boolean;
  streaming: boolean;
  tenantTier: 'free' | 'paid';
};

type Route = { tier: Tier; maxOutputTokens: number; timeoutMs: number };

export function route(task: Task): Route {
  // 1. Hard constraints first — they are not negotiable.
  if (task.inputTokens > 100_000) {
    return { tier: 'large', maxOutputTokens: 2_000, timeoutMs: 60_000 };
  }
  if (task.kind === 'agent' || task.needsTools) {
    // multi-step tool calling punishes weak instruction following
    return { tier: 'mid', maxOutputTokens: 1_500, timeoutMs: 45_000 };
  }

  // 2. Cheap, high-volume, well-specified work.
  if (task.kind === 'classify' || task.kind === 'extract') {
    return { tier: 'small', maxOutputTokens: 256, timeoutMs: 8_000 };
  }

  // 3. Everything user-facing and open-ended.
  if (task.kind === 'reason') {
    return { tier: 'large', maxOutputTokens: 2_000, timeoutMs: 60_000 };
  }

  // 4. Default, with a business rule on top.
  const tier: Tier = task.tenantTier === 'paid' ? 'mid' : 'small';
  return { tier, maxOutputTokens: 800, timeoutMs: task.streaming ? 30_000 : 20_000 };
}

Asociază apoi numele nivelurilor cu modelele concrete ale furnizorilor, într-un singur fișier de configurare pentru fiecare mediu. Când un furnizor lansează un model mai bun, schimbi o singură asociere și rulezi din nou evaluările, în loc să cauți identificatori de modele prin tot codul.

Fallbackuri, reîncercări și timeouturi

Furnizorii au incidente. Dacă o singură eroare în amonte devine o eroare 500 pentru utilizatorii tăi, stratul de rutare nu este terminat. Organizează încercările în lanț: întâi nivelul preferat, apoi o alternativă, cu un timeout strict pentru fiecare încercare.

Apel modelNivelul preferattimeout strictO reîncercarebackoff · doar apeluri idempotenteFurnizor alternativaceeași clasă de task, alt vendortimeout / 5xxeșuează în continuareDegradează, evită 500răspuns din cache sau mai scurtCIRCUIT BREAKERÎNCHISDESCHISSEMI-DESCHIS
Fig. 3Lanț: nivel preferat, reîncercare, alt furnizor și degradare controlată
const CHAIN: Record<Tier, Tier[]> = {
  small: ['small', 'mid'],
  mid:   ['mid', 'small', 'large'],
  large: ['large', 'mid'],
};

export async function complete(task: Task, messages: Message[]) {
  const { tier, timeoutMs, maxOutputTokens } = route(task);
  let lastError: unknown;

  for (const candidate of CHAIN[tier]) {
    const started = Date.now();
    try {
      const res = await withTimeout(
        callProvider(candidate, messages, { maxOutputTokens }),
        timeoutMs,
      );
      record({ task: task.kind, planned: tier, used: candidate,
               ms: Date.now() - started, usage: res.usage, ok: true });
      return res;
    } catch (err) {
      lastError = err;
      record({ task: task.kind, planned: tier, used: candidate,
               ms: Date.now() - started, ok: false, err });
      if (!isRetryable(err)) throw err;   // 4xx: do not shop around
    }
  }
  throw lastError;
}

Trei reguli împiedică apariția unei probleme costisitoare. Reia numai cererile eșuate din cauze temporare — timeouturi, limitări ale frecvenței cererilor, erori 5xx — și nu relua niciodată o cerere care primește o eroare 4xx din cauza unor date incorecte trimise de aplicația ta. Limitează numărul total de încercări, pentru că un val de reîncercări către un furnizor cu probleme poate transforma un incident minor într-o factură mare. Folosește și un circuit breaker pentru fiecare furnizor, astfel încât să nu aștepți expirarea timeoutului la fiecare cerere după ce furnizorul a devenit clar indisponibil.

Configurează fallbackuri între furnizori, nu doar între niveluri, pentru orice consideri critic. Acesta este singurul argument practic pentru a menține conectați doi furnizori.

Cache pe trei straturi

  • Cache pe prefix — majoritatea furnizorilor pot reutiliza un prefix repetat de prompt la un tarif redus. Îl poți folosi aproape fără costuri suplimentare: pune tot ce este stabil la început (prompt de sistem, definiții de instrumente, context comun amplu) și tot ce este variabil la final. Echipele care le amestecă pierd acest avantaj fără să-și dea seama că l-ar fi putut folosi.
  • Cache pe răspuns — calculează hash-ul promptului normalizat împreună cu nivelul și parametrii și păstrează în cache apelurile deterministe. Apelurile repetate de clasificare și extragere sunt servite din cache mult mai des decât sugerează intuiția. Pune un TTL și include o versiune de prompt în cheie, astfel încât o schimbare a promptului să invalideze automat cache-ul.
  • Stratul superior — cel mai ieftin apel este cel pe care nu îl faci. Deduplică cererile identice aflate deja în curs, sari peste model când o regulă sau o căutare răspunde la întrebare și nu rezuma un document care nu s-a schimbat de la ultimul rezumat.
cererea continuăApelul pe care nu îl facideduplică cererile în curs · folosește întâi o regulă sau o căutareGRATUITCache pe răspunshash(prompt normalizat + nivel + parametri) · doar apeluri deterministe~0 MSCache pe prefixîntâi prefixul stabil: prompt de sistem, definiții de tool-uri, exempleTARIF REDUS
Fig. 4Trei straturi de cache: de la apelul evitat la cache-ul de prefix

Bugete de tokeni

Stabilește o lungime maximă de output pentru fiecare clasă de task și impune-o în cerere, nu într-un comentariu de code review. Outputul nelimitat este cea mai frecventă sursă de costuri neașteptate și de latență p99: o singură cerere care decide să scrie un eseu poate costa mai mult decât o mie de cereri obișnuite.

Pentru input, stabilește un buget explicit. Decide câți tokeni poate ocupa fiecare parte a promptului — sistem, context regăsit, istoric de conversație, input de la utilizator — și restrânge conținutul după reguli deterministe când bugetul este depășit, începând cu istoricul cel mai vechi. Înregistrează fiecare reducere, deoarece o limită depășită constant indică o problemă de design, nu un simplu eveniment din timpul execuției.

Măsoară costul pe cerere și latența p95

Generează câte o înregistrare structurată pentru fiecare apel către model: clasa taskului, nivelul planificat, nivelul folosit efectiv, tokenii de input și de output, tokenii serviți din cache, latența, timpul până la primul token în cazul streamingului, numărul de reîncercări și rezultatul. Poți obține apoi toți indicatorii prin agregarea acestor date.

  • Costul pe cerere, pe clasă de task — numărul care îți spune unde merită efortul de rutare. Aproape niciodată nu este distribuit uniform.
  • Latența p95 și p99 pe clasă, plus timpul până la primul token separat pentru apelurile cu streaming. Mediile ascund exact cererile ale căror întârzieri îi fac pe utilizatori să renunțe.
  • Rata de fallback — cât de des a eșuat nivelul preferat. O rată de fallback în creștere este un avertisment timpuriu care apare înainte să se plângă utilizatorii.
  • Rata de hit în cache pe clasă, separată între cache-ul pe prefix și cel pe răspuns, ca să vezi care dintre ele aduce economii reale.

Convertește numărul de tokeni în bani la momentul raportării, folosind un tabel mic de tarife, în loc să stochezi un cost deja calculat. Tarifele se schimbă; istoricul tău nu ar trebui să fie rescris când se întâmplă asta.

Dovedește că modelul mai ieftin este suficient

Nu alege niciodată un nivel inferior doar pe baza intuiției. Pentru fiecare clasă de task, păstrează 50–200 de inputuri reale cu un output acceptat sau cu o regulă de notare. Atunci decizia devine o măsurătoare, nu o dezbatere.

  • Clasificare și extragere — potrivire exactă sau F1 pe câmpuri, față de etichete. Evaluarea este obiectivă, deci automatizeaz-o și condiționează deploymentul de trecerea acestor teste.
  • Generare — un model care notează după o grilă scurtă, calibrat o dată manual pe un eșantion, ca să identifici situațiile în care modelul evaluator este prea indulgent.
  • Raportează întotdeauna latența și costul alături de calitate. Un model cu un scor cu un punct mai mic și de trei ori mai ieftin este deseori alegerea corectă; decizia îi revine persoanei cu rol de product owner, iar treaba ta este să prezinți clar datele.

Apoi livrează schimbarea mai întâi ca rulare în umbră: trimite un procent din traficul real către ambele niveluri, compară rezultatele offline și comută numai când diferența se încadrează în pragul acceptat. Păstrează evaluarea în CI, astfel încât nimeni să nu răstoarne discret decizia după trei sprinturi.

Măsuri de siguranță

  • O limită de cheltuială pentru fiecare tenant și una globală, impuse în cod. Alege dinainte ce se întâmplă când sunt atinse: treci la un nivel inferior sau pui cererile într-o coadă; nu decide în timpul incidentului.
  • Limitarea frecvenței cererilor pentru fiecare tenant, astfel încât o singură buclă de automatizare să nu poată consuma bugetul tuturor celorlalți clienți.
  • Validează outputul structurat în raport cu o schemă și reîncearcă o dată, cu eroarea de validare adăugată în prompt. Este mai ieftin și mult mai fiabil decât folosirea unui model mai mare.
  • Nu lăsa niciodată text din surse nesigure să aleagă ruta sau instrumentele. Un atac de tip prompt injection care poate trimite o cerere către nivelul tău cel mai scump este un bug de tip „golire de portofel”.
  • Înregistrează prompturile și răspunsurile după mascarea datelor cu caracter personal și stabilește o perioadă de retenție pe care chiar o poți justifica în fața unui client.

Listă de verificare

  • Fiecare apel declară o clasă de task; routerul este o funcție pură, testată unitar.
  • Numele nivelurilor sunt asociate cu modele concrete într-un singur fișier de configurare pentru fiecare mediu.
  • Întâi constrângerile stricte: lungimea contextului, apoi instrumentele, apoi complexitatea, apoi streamingul.
  • Timeout pentru fiecare încercare, reîncercare numai în cazul erorilor temporare și circuit breaker pentru fiecare furnizor.
  • Un al doilea furnizor integrat pentru toate funcțiile critice.
  • Prefix stabil de prompt la început; cache pe răspuns cu versiunea de prompt în cheie.
  • Număr maxim de tokeni de output stabilit pe clasă; conținutul de input redus după reguli deterministe, fiecare reducere fiind contorizată.
  • O înregistrare structurată pentru fiecare apel; costul calculat la raportare.
  • Un set de evaluare pe clasă în CI, cu calitate, latență și cost raportate împreună.
  • Limite de cheltuială, limitarea frecvenței cererilor pentru fiecare tenant și validarea schemei cu o singură reîncercare de corectare.

Dacă faci un singur lucru din lista asta, înregistrează datele fiecărui apel. Deciziile de rutare fără măsurători sunt presupuneri, iar presupunerile despre costul modelelor tind să ducă la facturi mai mari decât estimările.

Lucrezi la ceva asemănător?

Începe un proiect