Crash z brakiem pamięci ukryty w etykiecie postępu
Pewnego wieczoru pod koniec września agent AI w przestrzeni roboczej jednego z użytkowników padł z błędem braku pamięci w środku tury. Oczywiste podejrzenia — zbyt wielu podagentów działających naraz albo kontekst, który urósł za bardzo — były błędne. Prawdziwą przyczyną była etykieta statusu: gdy model strumieniował duże polecenie powłoki po kilka znaków, za każdym razem wysyłaliśmy ponownie całe dotychczasowe polecenie jako etykietę postępu. Jedno polecenie o rozmiarze 124 KB zamieniło się w około 41 000 zdarzeń i mniej więcej 690 MB zakolejkowanych napisów.
Jak agent strumieniuje pracę
Gdy budujesz w Mythex, agent działa w prywatnym sandboksie chmurowym projektu. Gdy model pisze, jego wyjście strumieniuje się z powrotem do przeglądarki: tekst pojawia się słowo po słowie, a gdy agent uruchamia narzędzie — powiedzmy polecenie powłoki zapisujące plik — widzisz na żywo aktualizowaną etykietę, dzięki czemu wiesz, co się dzieje.
Model strumieniuje w małych kawałkach. Przy modelu, którego używaliśmy, te kawałki były często długie na trzy znaki.
Pierwsze hipotezy były błędne
Crash wyglądał na problem skalowania, więc tam zaczęliśmy.
- Równoległe podagenty? Tura użyła tylko dwóch, każdy trzymał około 1,5 MB stanu. To za mało.
- Zatrzymane ślady? Nasze śledzenie trzymało w pamięci drzewo uruchomień, ale było dalekie od rozmiaru sterty w chwili crashu.
Oba wyjaśnienia były wiarygodne i oba łatwo było przyjąć. Żadne nie odtworzyło crashu. Lekcja, którą wciąż na nowo odkrywamy: hipoteza nie jest przyczyną, dopóki nie odtworzy awarii.
Odtworzenie tego naprawdę
Zbudowaliśmy tymczasowy sandbox z takimi samymi limitami pamięci jak produkcja i sfałszowany model, który strumieniował duży plik w realistycznych kawałkach po trzy znaki. Spowolniliśmy też odbiorcę po drugiej stronie gniazda, jak prawdziwa sieć.
To zadziałało. Sterta rosła, a proces padł z tym samym błędem.
Mechanizm był prosty, gdy już mogliśmy go obserwować. Gdy model pisał duże polecenie — w tym przypadku heredoc zapisujący plik 124 KB — nasz kod strumieniowania aktualizował etykietę narzędzia przy każdym kawałku. Etykieta nie zawierała nowych trzech znaków; zawierała całe dotychczasowe polecenie. Zdarzenia rosły: 3 znaki, potem 6, potem 9, … aż do 124 KB, około 41 000 razy. Gniazdo nie mogło wysyłać tak szybko, jak powstawały, więc piętrzyły się w jego buforze zapisu jako napisy — około 690 MB.
Naprawa
- Ogranicz etykietę. Etykieta statusu musi tylko mówić, co się dzieje. Jest teraz ograniczona do 200 znaków, a częstotliwość ponownego skanowania częściowego polecenia jest ograniczona.
- Zlikwiduj kwadratowe sklejanie. Łączenie strumieniowanych kawałków przez powtarzane sklejanie napisów to O(n²) przy dużych wyjściach. Przeszliśmy na podejście oparte na fragmentach, które dokleja w czasie liniowym.
Potem poszukaliśmy wszystkiego innego, co mogło rosnąć bez ograniczeń przy lawinie wyjścia, i to ograniczyliśmy:
- Wyjście polecenia na żywo pokazywane w czacie jest grupowane i zachowuje tylko ostatnie 8000 znaków.
- Pojedyncze linie zdarzeń są ograniczone do 4 MB.
- Bufor powtórki, z którego przeglądarka nadrabia zaległości po ponownym połączeniu, jest ograniczony do 8 MB na projekt.
- Stare punkty kontrolne są przycinane w trakcie tury, zachowując najnowszy stan i punkty zaczepienia, których wcześniejsze tury potrzebują.
Uczciwe testowanie zalewów
Dwie rzeczy utrudniły testowanie bardziej, niż powinny:
- Prawdziwe modele nie współpracują. Poproś model o wypisanie ogromnego pliku, a zwykle odmówi, podsumuje albo napisze go sensowniej. Żeby testować zalewy, potrzebujesz skryptowanego modelu, który robi dokładnie to, co mu powiesz, działając przez prawdziwy kod agenta.
- Skrypty muszą zachowywać się jak modele. Nasz pierwszy skryptowany model używał tego samego identyfikatora odpowiedzi przy każdej odpowiedzi, więc framework agenta traktował każdą odpowiedź jako zastąpienie poprzedniej i kończył tury przedwcześnie. Wyglądało to na błąd odświeżania interfejsu. Nim nie było — to uprząż testowa kłamała. Musieliśmy też wymusić w skrypcie prawdziwy limit kontekstu modelu, bo inaczej liczby zużycia pamięci były nierealistyczne.
Co z tego wynieśliśmy
- Strumieniowe interfejsy mnożą drobne błędy. Wszystko, co wysyłasz „na kawałek”, jest wysyłane dziesiątki tysięcy razy. Wysyłaj przyrosty albo ogranicz ładunek.
- Ograniczaj każdy bufor, który przechodzi przez wolną granicę. Gniazda, logi powtórek i punkty kontrolne potrzebują sufitu.
- Odtwórz przed naprawą. Nasze dwa pierwsze wyjaśnienia brzmiały słusznie i były błędne.
Jeśli ostatnio widziałeś w czacie Mythex bardzo długi plik pojawiający się bez zacinania agenta, to właśnie częściowo dlatego.