Skip to content
← Wszystkie wpisy
6 min czytania Dawid Skłodowski

Postawienie funkcji LLM przed prawdziwymi użytkownikami: opowieść z frontu

Prototyp był magią. Potem wysłaliśmy go do prawdziwych użytkowników i wszystko, co demo ukryło, stało się wymagalne naraz — koszt, opóźnienie, nadużycie, padające API i unikalny problem wspierania funkcji, której wyjścia nie możesz w pełni przewidzieć. Opowieść z frontu o przepaści między demem LLM a produktem LLM.

Pisałem kilkukrotnie o przepaści między demem LLM a produktem LLM, zwykle w abstrakcji. To wersja konkretna: opowieść z frontu o wzięciu funkcji, która działała pięknie w prototypie, i postawieniu jej przed prawdziwymi użytkownikami przy prawdziwym wolumenie. Prototyp był łatwą, magiczną częścią — zbudowany w dni, zademonstrowany przy aplauzie. Wysłanie go do faktycznych użytkowników to miejsce, gdzie wszystko, co demo po cichu ukryło, stało się wymagalne, naraz: koszt, opóźnienie, nadużycie, padające API i naprawdę nowy problem wspierania funkcji, której wyjścia nie możesz w pełni przewidzieć. Żadne z tych nie były zaskoczeniami w sensie, że o nich nie czytałem; były zaskoczeniami w sensie, że przeżywanie ich różni się od wiedzy o nich. Oto, czego nauczyła produkcja.

Zaskoczenie kosztem

Pierwszą rzeczą, którą robią prawdziwi użytkownicy, jest kosztowanie cię pieniędzy, w sposób, którego prototyp nigdy nie zasugerował. W developmencie robisz garść wywołań API dziennie; rachunek to błąd zaokrąglenia. Postaw funkcję przed prawdziwymi użytkownikami, a wywołania skalują się z użyciem — a ponieważ wywołania LLM są opomiarowane per token, rachunek skaluje się obok, w sposób, który może zrobić się alarmujący szybko. Parę konkretnych lekcji:

  • Niektórzy użytkownicy używają tego znacznie bardziej, niż zamodelowałeś. Użycie nie jest jednolite; mała liczba ciężkich użytkowników może napędzać dużą część kosztu. Nie wprowadziliśmy limitów per użytkownik i nie zdaliśmy sobie sprawy, że ich potrzebujemy, dopóki rachunek nam nie powiedział.
  • Długie wejścia kosztują więcej, niż myślisz. Cennik oparty na tokenach znaczy, że funkcja przetwarzająca duże dokumenty albo długie historie kosztuje proporcjonalnie więcej per wywołanie, a te wywołania się sumują. „Przeciętne” wywołanie ukrywało długi ogon drogich.
  • Potrzebujesz widoczności kosztu przed kontrolą kosztu. Pierwszą poprawką nie były limity — to było mierzenie wydatku tokenów per użytkownik i per funkcja, byśmy mogli zobaczyć, gdzie poszły pieniądze. Nie możesz kontrolować tego, czego nie widzisz, a prototyp nie miał żadnej instrumentacji.

Lekcją jest traktować koszt jako pierwszoklasową troskę produkcyjną od początku: instrumentuj wydatek tokenów, ustaw limity per użytkownik i znaj swoją ekonomię jednostki zanim prawdziwy ruch nauczy cię jej kosztownie.

Rzeczywistość opóźnienia

Prototyp czuł się szybki, bo byłem jedynym użytkownikiem i byłem cierpliwy. Prawdziwi użytkownicy nie są żadnym. Wywołania LLM są wolne — sekundy, czasem wiele — a na demie to czytało się jako „przemyślane”; na produkcji czytało się jako „zepsute”. Co produkcja wymusiła:

  • Strumieniowanie nie było opcjonalne. Kazanie użytkownikom gapić się na spinner przez pełną wielosekundową generację było nieakceptowalne, gdy prawdziwi ludzie w to uderzyli; strumieniowanie wyjścia, by widzieli, jak się buduje, było różnicą między „zepsute” a „żywe”, dokładnie jak argumentowałem w abstrakcji.
  • Wolne wywołanie nie mogło żyć w żądaniu. Trzymanie workera webowego na czas trwania wywołania modelu padało pod współbieżnym obciążeniem szybko. Wywołanie musiało przejść do zadania w tle, z wynikiem dostarczonym asynchronicznie — ta sama separacja, której wymaga każde wolne wywołanie zewnętrzne.
  • Timeouty i wolne odpowiedzi są normalne, nie wyjątkowe. Czasem model jest po prostu wolny. Zaprojektowanie UI, by obsłużyło wywołanie trwające znacznie dłużej niż zwykle — zamiast zakładać ciasne opóźnienie — było konieczne, bo ogon jest realny i użytkownicy w niego uderzają.

Nadużycie, którego nie wyobrażałeś

Oto to, czego prototyp nie może ci pokazać, bo jedynym użytkownikiem prototypu jesteś ty, a używasz go zgodnie z zamierzeniem. Prawdziwi użytkownicy — i ludzie, którzy nie są naprawdę „użytkownikami” w ogóle — robią rzeczy, których nigdy nie zamodelowałeś:

  • Ludzie próbują to złamać. Próby prompt-injection, próby skłonienia funkcji, by powiedziała coś żenującego albo zrobiła coś, czego nie powinna, używanie jej do celów, których nigdy nie zamierzałeś. W chwili, gdy funkcja dotyka publiczności, jakiś ułamek ruchu jest przeciwnikowy.
  • Potrzebujesz moderacji i barier, których nie prototypowałeś. Filtrowanie wejść, ograniczanie tego, z czym funkcja się zaangażuje, obsługa prób nadużycia — żadne z nich nie istniało w demie, wszystkie produkcja wymagała natychmiast. Funkcja LLM przed publicznością bez barier to incydent reputacyjny czekający, by się zdarzyć.
  • Wyjście może cię zawstydzić. Nawet z dobrze-intencjonowanym wejściem niedeterministyczny model może wyprodukować coś nie-w-tonie, błędnego albo nieodpowiedniego. Musisz pomyśleć, co się dzieje, gdy to robi, bo przy dość prawdziwym ruchu, zrobi.

To była najbardziej stroma krzywa uczenia, właśnie dlatego, że to część, której nie możesz nauczyć się z prototypu — pokazuje się tylko, gdy funkcja spotyka ludzi niedzielących twoich założeń.

Gdy API pada

Prototyp zakłada, że API modelu zawsze odpowiada. Produkcja nie dostaje tego luksusu. Zewnętrzne API będzie miało wolny okres, odrzucenie limitu, awarię — a gdy to robi, twoja funkcja jest wyłączona, chyba że to zaplanowałeś. Co musieliśmy zbudować:

  • Łagodna degradacja. Gdy API jest niedostępne, funkcja musi paść w sposób uczciwy i nie-zepsuty — jasna wiadomość, fallback, „spróbuj wkrótce ponownie” — nie ślad stosu ani zawieszenie. Zdecyduj, czym funkcja jest, gdy model jest niedostępny.
  • Ponowienia z backoffem i obsługa limitu. Przejściowe awarie i odpowiedzi limitu są rutynowe przy wolumenie; obsługa ich sensownymi ponowieniami (bez młócenia męczącego się API) to podstawowa higiena produkcyjna, której prototyp nigdy nie potrzebował.
  • Model to niezawodna zewnętrzna zależność. To ujęcie, które wiąże to razem, i to, do którego cała ta seria wciąż wraca: API LLM to wolna, opomiarowana, czasem-niedostępna usługa zewnętrzna, a każda dyscyplina, którą zastosowałbyś do dowolnej krytycznej zależności od strony trzeciej — timeouty, ponowienia, fallbacki, monitoring — stosuje się do niej. Prototyp pozwolił mi o tym zapomnieć. Produkcja nie.

Wspieranie nieprzewidywalnego

Naprawdę nowym problemem produkcyjnym — tym bez odpowiednika w normalnym oprogramowaniu — jest wspieranie funkcji, której wyjścia nie możesz w pełni przewidzieć ani odtworzyć. Użytkownik zgłasza „dało mi błędną odpowiedź”, idziesz zbadać i… model jest niedeterministyczny, więc możesz nie móc odtworzyć dokładnego wyjścia. To zmienia historię wsparcia i debugowania:

  • Loguj wszystko. Wejścia, prompt, odpowiedź, wersję modelu — logowane dla każdej interakcji, bo gdy coś pójdzie źle, log jest jedynym zapisem tego, co faktycznie się stało. Bez niego zgłoszenie błędu o nieodtwarzalnym wyjściu jest niezbadalne.
  • Evale łapią regresje, które inaczej znaleźliby użytkownicy. Zbiór eval, który promowałem, zarobił tu na swoje utrzymanie: gdy zmienialiśmy prompt, eval mówił nam, czy jakość się ruszyła, zanim użytkownicy. Produkcja to miejsce, gdzie „zmieniliśmy prompt, a jakość po cichu spadła” staje się pożarem wsparcia — chyba że eval złapał to pierwszy.
  • Ustawiaj oczekiwania użytkownika uczciwie. Częścią wspierania nieprzewidywalnej funkcji jest bycie szczerym z użytkownikami, że jest generowana przez AI i może być błędna — zarządzanie oczekiwaniem zamiast sugerowania precyzji, której funkcja nie ma.

Werdykt

Prototyp to magiczna, łatwa część funkcji LLM; postawienie jej przed prawdziwymi użytkownikami przy prawdziwym wolumenie to miejsce, gdzie wszystko, co demo ukryło, staje się wymagalne naraz. Koszt skaluje się per token z użyciem i ogonem ciężkich użytkowników, którego nie zamodelowałeś, więc instrumentuj wydatek tokenów i ustaw limity per użytkownik, zanim rachunek cię nauczy. Opóźnienie, które czytało się jako przemyślane w demie, czyta się jako zepsute na produkcji, wymuszając strumieniowanie, zadania w tle i UI oczekujące wolnego ogona. Nadużycia, którego nie możesz prototypować — injection, nadużycie, żenujące wyjście — przybywa w chwili, gdy funkcja spotyka publiczność, wymagając barier i moderacji, których nie było w demie. API padnie, więc budujesz łagodną degradację, ponowienia i fallbacki, traktując model jako niezawodną zależność zewnętrzną, którą jest. A naprawdę nowym problemem jest wspieranie nieprzewidywalnego — logowanie każdej interakcji, bo nie możesz odtworzyć niedeterministycznego wyjścia, opieranie się na evalach, by łapać regresje, zanim użytkownicy, i ustawianie uczciwych oczekiwań. Żadnego z tego nie ma w prototypie, a wszystko to faktyczna praca wysyłania funkcji LLM. Demo zarabia aplauz; produkcja zarabia lekcje — a przepaść między nimi to cały zawód.