Skip to content
Wróć do artykułów
Poradnik9 min czytania

Postaw tani sąd przed drogim: jak typowana bramka obniża opóźnienia i koszty pipeline’u LLM

Trzy czołowe modele naradzają się piętnaście sekund. Sąd wydany w 300 ms przed nimi odsiewa śmieci i zdejmuje czekanie, którego 82% uruchomień nie potrzebowało.

TypeSafeCouncilAI EngineeringNext.jsLatency
Karta kontroli wstępnej TypeSafe AI nad pytaniem „Should I take the startup offer?”: answerable 0.94 z etykietą PASS, needs_live_data 0.08 z etykietą PLANNER SKIPPED i linia „odczytane w 0.28 s, zaoszczędzone 1.1 s czekania, 0 dodatkowych wywołań” — obok trzy odrzucone śmieciowe wejścia z wynikami 0.04, 0.11 i 0.05, a po prawej Pavel Smolin

Council zadaje to samo pytanie trzem czołowym modelom równolegle, a potem scala ich odpowiedzi w jeden werdykt. Jedno uruchomienie trwa piętnaście do dwudziestu pięciu sekund i kosztuje cztery płatne wywołania API. To jest produkt, a ta powolność jest celowa — patrzysz, jak rada się naradza.

Kłopot z potokiem o takim kształcie polega na tym, że zobowiązuje się, zanim cokolwiek spojrzy na wejście. Każde pytanie dostaje pełne piętnaście sekund — także te, które nie były warte czterech wywołań, i te, które wcale nie były pytaniami.

Wczoraj postawiłem przed nim sąd wydawany w 300 ms. Nie czwarty model w radzie, a bramkarza: jedno wywołanie, które odpowiada na dwa pytania „tak/nie” o surowe wejście i wręcza kodowi dwa prawdopodobieństwa. Poniżej sam wzorzec, liczby stojące za każdą decyzją i dwa miejsca, w których spróbowałem i się wycofałem.

Dwa marnotrawstwa na początku każdego uruchomienia

Planer wyszukiwania. Część pytań potrzebuje bieżących faktów — kursów, pogody, rozkładów, wszystkiego, co zdarzyło się po dacie odcięcia wiedzy modelu. Małe wywołanie Gemini decyduje, czy trzeba czegoś poszukać, i jeśli tak, pisze zapytania. Idzie równolegle z kontrolą limitu, a wachlarz ekspertów nie może wystartować, dopóki oba się nie zamkną: mediana 1 087 ms, maksimum 2 532 ms, przy terminie 4 s.

Wyszukiwania potrzebuje 18% produkcyjnych uruchomień — 24 na 135 w ciągu trzydziestu dni. Czyli 82% uruchomień płaci sekundą opóźnienia za odpowiedź „nie”.

Nie-pytania. Nic nie sprawdzało, czy na wejściu jest pytanie. „Привет”, „тут ?”, „Ej mam pytanie” i „عطني معلومات اول مره اسمعها” — każde z nich przepuściło trzech ekspertów i syntezę, zajęło piętnaście do dwudziestu pięciu sekund, kosztowało cztery wywołania i zostało zapisane jako publiczna strona werdyktu /s/[id] — w sitemapie, gdzie Google mógł zaindeksować staranną naradę rady nad słowem „cześć”.

To ten sam błąd. Potok nie miał taniej opinii o własnym wejściu.

Wzorzec: 300 ms sądu, potem 15 sekund rozumowania

Z przodu nie może stać kolejny model czatowy. Mały LLM zwraca tekst, czyli trzeba go parsować, czyli bierzesz na siebie problem zniekształconego wyjścia dokładnie w tym miejscu, które musi być niezawodne — i płacisz sekundą opóźnienia, żeby zaoszczędzić sekundę opóźnienia.

Działa model, którego wyjście jest już typowane. Wziąłem Jev od TypeSafe — model System One: czyta język naturalny i zwraca skalibrowane prawdopodobieństwa dla odpowiedzi, które sam definiujesz, bez tekstu i bez śladu rozumowania. Jeden POST, pięć pytań w ciele, a kształt odpowiedzi jest kontraktem, nie nadzieją.

Całe wywołanie to treść pytania, dzisiejsza data i przybliżone miasto pytającego:

const result = await askSystemOne(
  { question, today, approximate_user_location },
  { answerable, needs_live_data, stakes, taste, pick },
  { signal: AbortSignal.timeout(1_500), label: "preflight" }
);

Pomiary na 46 pytaniach: mediana 283 ms, maksimum 933 ms. Wywołanie idzie obok kontroli limitu, na którą uruchomienie i tak czekało, więc na szczęśliwej ścieżce nie kosztuje nic.

Dalej decyduje kod, nie model:

  • answerable poniżej 0.2 odrzuca uruchomienie przed pierwszym wywołaniem eksperta.
  • needs_live_data poniżej 0.5 przestaje czekać na planer i rusza bez wyszukiwania.

Wybieraj prymityw, nie prompt

Za pierwszym podejściem pomyliłem się dokładnie tutaj: sięgnąłem po prompt tam, gdzie należało wybrać typ zwracanej wartości. System One daje trzy, a wybór zależy od tego, co znaczy odpowiedź:

Prymityw Co zwraca Do czego
Noul P(tak), bez osobnej pewności Czy warunek zachodzi
Score Rozkład po uporządkowanych poziomach Stopień na opisanej skali
Choice Jedna opcja plus rozkład po wszystkich Jedna z zadanego zbioru

Oba pytania bramkarza to Noule, bo oba są naprawdę „tak/nie”. Noul na 0.5 nie znaczy „średnio” — znaczy, że „tak” i „nie” są równie prawdopodobne, a dla bramkarza to dokładnie ten odczyt, którego chcesz.

Prawdziwe pytanie od szumu oddzieliła nie instrukcja. Oddzieliły kryteria — z przykładami:

answerable: {
  type: "noul",
  instructions:
    "Is `question` an actual question or decision a panel of advisors could " +
    "give a useful verdict on, rather than a greeting, a test message, a " +
    "fragment with no ask, or noise?",
  criteria: {
    true: {
      what: "Contains a real ask, even if short or badly phrased",
      examples: ["Тонзилгон применение", "Buy or rent in Madrid?"],
    },
    false: {
      what: "Greeting, 'are you there', 'I have a question' with no question, gibberish",
      examples: ["Привет", "hello?", "I have a question"],
    },
  },
}

Te przykłady są nośne. „Тонзилгон применение” to dwa rosyjskie słowa, bez czasownika i bez znaku pytania, a jednak prawdziwe pytanie: 0.91. „Ej mam pytanie” to płynne polskie zdanie, które o nic nie pyta: 0.05. Bez przykładów model czyta interpunkcję i płynność; z nimi czyta, czy jest pytanie. To jedna poprawka w criteria, nie inżynieria promptów.

Progi się mierzy, nie zgaduje

0.2 i 0.5 wyglądają arbitralnie i — tak zapisane — są. Uzasadnionymi czyni je to, że oba siedzą w zmierzonej luce.

Bramkarz Próg Śmieci / negatywy Prawdziwe / pozytywy
answerable 0.2 0.01 – 0.16 ≥ 0.63, w większości ≥ 0.9
needs_live_data 0.5 ≤ 0.70 ≥ 0.92

Zestaw testowy planera to trzydzieści pytań, połowa wymaga wyszukiwania. Każdy pozytyw dostał 0.92 lub więcej, każdy negatyw 0.70 lub mniej, więc próg 0.5 ma 0.2 zapasu z obu stron — mogę go przesuwać daleko, zanim któraś z klas go przekroczy.

Ale produktem nie jest próg. Produktem jest skrypt pomiarowy. Przepuszcza obu bramkarzy przez zestawy i wywala build przy recall poniżej 1.0, precision poniżej 0.9, jakimkolwiek przepuszczonym śmieciu, jakimkolwiek odrzuconym krótkim prawdziwym pytaniu albo medianie powyżej 600 ms. Bieżący przebieg: precision 0.94, recall 1.00, mediana 296 ms.

To ten pomiar zamienia liczbę w decyzję produktową, a nie w zabobon. Próg bez pomiaru za sobą to domysł, którego za trzy miesiące będziesz się bał tknąć.

Politykę trzymaj w kodzie, gdzie zmiana jest darmowa

Przy skalibrowanym modelu chce się zapytać go o wniosek. Nie rób tego — pytaj o sąd, a wniosek zostaw własnemu kodowi, bo to właśnie tę drugą połowę będziesz chciał zmieniać.

Council pyta Jev także o to, jak odwracalna jest decyzja — jako Score po trzech poziomach: łatwo odwrócić, kosztownie odwrócić, drzwi w jedną stronę. Oczywisty odczyt to oczekiwany poziom, który zwraca API. I jest błędny. Pytanie podzielone 50/50 między poziomami „łatwo odwrócić” i „drzwi w jedną stronę” daje oczekiwanie 1.0, które zaokrągla się do „kosztownie odwrócić” — poziomu, który model uznał za najmniej prawdopodobny z trzech.

Dlatego polityka to jedna linia kodu: bierzemy kubełek o najwyższym prawdopodobieństwie, a przy dokładnym remisie zostawiamy poziom niższy.

// argmax po rozkładzie, nie zaokrąglone oczekiwanie
for (const l of [0, 1, 2] as const) {
  const p = stakes.probabilities[String(l)];
  if (p !== undefined && p > best) { best = p; level = l; }
}

To samo z progami wyświetlania. Czy pytanie czyta się jako „głównie osobisty gust”, czy „głównie fakty”, to TASTE_MIN = 0.6 i FACTS_MAX = 0.4 w bloku stałych. Przesunięcie któregokolwiek to deploy, nie ponowne wnioskowanie: dowody i znaczenie pytania się nie zmieniły, więc nie ma czego przeliczać. Surowe sądy zostają wielokrotnego użytku, polityka zostaje jawna i tania.

Awaria musi przepuszczać, inaczej tego nie wdrażaj

Bramkarz przed działającym potokiem to nowy sposób, żeby ten potok zepsuć. Zasada, której nie zginam: każda ścieżka awarii zwraca null, a null znaczy, że trasa zachowuje się dokładnie tak, jak przed powstaniem bramkarza.

Brak klucza, timeout 1,5 s, błąd HTTP po jednej ponownej próbie, ciało, które nie jest JSON-em — wszystko to zapisuje ostrzeżenie i nie daje werdyktu, a brak werdyktu znaczy brak odmowy i brak pominięcia planera. Funkcję wyłącza się usunięciem zmiennej środowiskowej.

Dwa szczegóły zasłużyły na swoje miejsce:

Jedna ponowna próba, potem stop. 408, 429, 5xx i 529 TypeSafe („przeciążenie”) powtarzają się raz po 300 ms. Nie dwa razy — termin wywołującego to 1,5 s, więc druga próba nigdy by się w nim nie zmieściła. Polityka ponawiania jest funkcją terminu, a nie tego, co wydaje się solidne.

Zniekształcona odpowiedź jest odrzucana, nie rzuca wyjątkiem. Jedno żądanie nosi pięć pytań, a dwa pytania bramkarza jadą w tym samym ciele co trzy pytania dla użytkownika. Jeśli ciało nie jest obiektem albo nie ma w nim żadnych odpowiedzi — to wyjątek. Ale jedna odpowiedź, która nie przechodzi walidacji, zostaje odrzucona z jednym ostrzeżeniem, a pozostałe docierają do swoich wywołujących. Inaczej jeden zły odczyt na potrzeby funkcji kosmetycznej kosztowałby uruchomienie bramkarza „czy to w ogóle pytanie” — promień rażenia ciała, którego nie kontroluję, byłby równy całemu potokowi.

I loguj decyzję. Każde uruchomienie po kontroli limitu zapisuje oba prawdopodobieństwa, to, co trasa z nimi zrobiła, oraz — gdy czekano na planer — własny werdykt Gemini o tym samym pytaniu. Zgodność Jev i Gemini jest teraz zapytaniem, a nie wiarą. Już raz wdrożyłem regułę routingu na liczbie, której nigdy nie sprawdziłem, i wolałbym nie powtarzać.

Gdy przez bramkarza idzie każde wejście, dodatkowe pytania są prawie darmowe

Nieoczekiwana premia: kosztowną częścią tego wzorca jest podróż w obie strony, a nie liczba pytań. Jeśli każde wysłane pytanie i tak idzie do Jev, to kolejne sądy o tym samym stanie kosztują prawie nic i liczą się równolegle — nie widzą odpowiedzi siebie nawzajem, więc nie ma za co płacić kolejnością.

Bramkarz Council urósł z dwóch pytań do pięciu: dwa pytania bramkarza, plus jak odwracalna jest decyzja, plus na czym się opiera — na guście czy na faktach, plus — gdy pytanie nazywa warianty — który z nich wybrałby Jev. Mediana wzrosła z 283 ms do 313 ms, p90 710 ms, maksimum 851 ms.

Te trzy dodatkowe odczyty to już funkcja dla użytkownika. Karta „Instynkt” pojawia się nad miejscami ekspertów około pół sekundy po wysłaniu: pigułki stawki i gustu, słupek na wariant, gdy rada jeszcze myśli — błyskawiczny sąd obok narady, a później linia o tym, czy rada się z nim zgodziła. Budżet opóźnienia na to był zerowy, bo żądanie i tak było w locie dla bramkarza.

To właśnie tę część warto ukraść, nawet jeśli nigdy nie zbudujesz bramkarza: jeśli już płacisz za podróż do modelu sądów, zapytaj przy okazji o wszystko, co będziesz chciał wiedzieć o tym stanie.

Dwa miejsca, w których nie wyszło

Wykrywanie brakującego kontekstu. Pomysł był taki, żeby zauważyć, że w pytaniu brakuje jednego faktu, który wszystko rozstrzyga, i dopytać użytkownika przed uruchomieniem rady. Odpowiedzi Jev wróciły z pewnością 0.2–0.6 — rozkład zbyt płaski, żeby stawiać próg. To sygnał, żeby przebudować pytanie i zebrać oznaczony zbiór, a nie wdrażać przy niższej poprzeczce. Wycięte.

Całkowite zastąpienie planera. Model sądów zwraca sądy, a nie zapytania wyszukiwania, a krok wyszukiwania potrzebuje właśnie zapytań. Dlatego „tak” na needs_live_data nadal czeka na Gemini, dokładnie jak wcześniej. Bramkarz usunął czekanie tylko na tych 82% uruchomień, które i tak miały dostać „nie” — w czym była cała wygrana, i co jest twierdzeniem znacznie skromniejszym niż „zastąpiliśmy model”.

Jedna rzecz wprost: wywołanie bramkarza wysyła treść pytania, datę i przybliżone miasto. Nigdy nie wysyła identyfikatorów użytkownika, anonimowych identyfikatorów, identyfikatorów sesji ani adresów e-mail. Ale to API nie ma dziś flagi zerowej retencji na żądanie — w odróżnieniu od wywołania wyszukiwania, które ten sam potok wykonuje w innym miejscu — więc te dane leżą pod standardową retencją dostawcy. To musi trafić do twojego tekstu o prywatności wcześniej, niż funkcja trafi na produkcję.

Co się przenosi

Jeśli prowadzisz drogi potok AI — kilka modeli, długi łańcuch, cokolwiek, gdzie jedno wejście kosztuje prawdziwe pieniądze i prawdziwe sekundy:

  1. Znajdź, do czego potok się zobowiązuje, zanim cokolwiek wie. W Council było to czekanie na planer i niesprawdzone wejście. Oba dały się zmierzyć w danych produkcyjnych, które już miałem.
  2. Bramkuj typowanym wyjściem, nie tekstem. To, co stoi z przodu, musi być bardziej niezawodne niż to, co chroni.
  3. Wybieraj prymityw po znaczeniu odpowiedzi. Warunek, stopień albo jedna ze zbioru.
  4. Przykłady wkładaj do kryteriów. Stamtąd wzięła się dokładność.
  5. Zmierz lukę, postaw próg w jej środku — i zachowaj pomiar.
  6. Awaria przepuszcza na każdej ścieżce, decyzja idzie do logu.

Uczciwe zastrzeżenie: za problemem stoją liczby z pomiarów i trzydzieści dni historii produkcyjnej, ale sam bramkarz żyje na produkcji jeden dzień. Wiem, że odrzuca śmieci i zdejmuje czekanie na planer. Czy sąd wydany w 300 ms zgadza się z trzema czołowymi modelami dość często, by rozszerzać jego rolę — to zapytanie, którego jeszcze nie mogę wykonać, i zapisałem, czego wymaga odpowiedź, zanim zrobię z nim cokolwiek więcej.

Council działa na trycouncil.app, w App Store oraz w Google Play. Jeśli masz potok AI, który płaci pełną cenę za wejścia tego nie warte, i chcesz drugiej pary oczu na to, gdzie postawić taniego bramkarza — napisz do mnie.

Pavel Smolin · 28 października 2026

Powiązane artykuły

Poradnik4 min czytania

Jak dodać serwer MCP Supabase do Claude Code

Jedna komenda łączy Supabase z Claude Code przez hostowany serwer MCP: OAuth albo personal access token, project_ref, tryb tylko do odczytu i które narzędzia dodać do allowlisty.

Poradnik3 min czytania

Jak dodać serwer MCP RevenueCat do Claude Code

RevenueCat łączy się z Claude Code dwiema komendami: oficjalny plugin albo hostowany serwer MCP. OAuth czy klucz API, jak sprawdzić połączenie i co dodać do allowlisty.