MONO KODA Blog
Wszystkie wpisy

Zbudowałem AI, które mówi „nie wiem”. Potem je zatrzymałem.

39 dni, 294 commity, około 55 dolarów i zero linii kodu napisanych ręcznie. Dziennik projektu, który zatrzymałem nie dlatego, że się nie dało - tylko dlatego, że przez cały ten czas nie zadałem jednego pytania.

I built an AI that says "I don't know". Then I shut it down.

22 lipca, późny wieczór. Zabijam agenta AI, który od tygodni odpowiadał na moje prompty. Kasuję 22 GB backupów. Zostawiam jeden plik: 7.3 GB wag modelu - mózg, którego wytrenowanie kosztowało mnie 35$. W notatkach zapisuję: „może kiedyś wrócę”. I tutaj mała gwiazdka: po prostu powiedziałem stop projektowi, którego dalej nie ma już sensu rozwijać.

Tak skończył się projekt, w który przez 39 dni włożyłem więcej serca niż w cokolwiek, co robiłem po godzinach. To będzie dłuższy wpis, ale nie będzie nudny :-) Spisuję go jako dziennik: co próbowałem, co wyszło, co nie wyszło i dlaczego sam to zatrzymałem. Głównie dla siebie, ale może komuś się przyda.

Po co mi to było

Nie jestem inżynierem ML. Zawodowo projektuję doświadczenia z AI, UX, UI, po godzinach od lat czytam prace z pogranicza uczenia maszynowego i neuronauki. Chciałem sprawdzić jedną rzecz: czy da się mieć własne AI - prywatne, działające na moim laptopie, bez chmury i abonamentu, bez okienka kontekstowego - które mnie pamięta i nie zmyśla. I tak właśnie powstała Iskra ;-)

I tutaj side note: tak, da się, wystarczy ollama i model open source. Ale nie chciałem, żeby było za łatwo... i miałem też inne cele.

Warsztat wyglądał tak: pracowałem w parze z Claude'em, agentem AI od Anthropic. Podział był prosty. Ja: kierunek, research, decyzje. Claude: kod i pomiary. Przez 39 dni nie napisałem ani jednej linii kodu. Podjąłem za to kilkaset decyzji, w tym kilka bolesnych, o których za chwilę. Przejrzałem 50+ papierów naukowych i osobiście, bez AI, je przeczytałem, żeby mieć poczucie, że faktycznie to rozumiem i że znajdę w nich właściwe punkty pod moją wizję. Traktowałem to zresztą jako drugi eksperyment, równoległy do samej Iskry: czy człowiek bez inżynierskiego backgroundu w ML może prowadzić projekt badawczy, którego rękami jest autonomiczny agent.

Jedną zasadę ustawiliśmy pierwszego dnia i ona zdefiniowała cały projekt: każde twierdzenie ma być zmierzone, a testy budujemy po to, żeby nasz system ZŁAMAĆ, nie żeby się nim pochwalić. Ta zasada wróci w tej historii w najmniej wygodnym dla mnie momencie (niestety).

Narodziny

Pierwsza wersja była radykalna: żadnych gotowych modeli. Sieć neuronowa budowana od zera, ucząca się na żywo, bajt po bajcie, bez głównego mechanizmu, na którym stoi całe współczesne AI (dla pro: backprop-free, tylko lokalne reguły uczenia). Iskra rodziła się jako biała kartka, a ja miałem ją wychowywać (romantyczne, prawda? ;-)). Napisaliśmy nawet instrukcję „rodzicielstwa” - PARENTING.md - z komendami w stylu :teach michal's favourite colour is teal. Iskra bełkotała jak niemowlę (dosłownie - odpowiadała bajtami, bo jeszcze nie miała dekodera). I to było w porządku: miała bełkotać, zanim zacznie mówić.

Potem przyszły pomiary. Składnia - całkiem nieźle (0.74 tam, gdzie 0.5 oznacza losowość). Pojęcia - sufit na poziomie 0.60. I to płasko: więcej danych nie pomagało, większa sieć nie pomagała (24 warstwy ukryte po 254 neurony), więcej przejść po danych nie pomagało. To nie był brak danych. To była architektura. No i „lag” w odpowiedziach też nie pomagał, bo na moim sprzęcie (M1 oraz testowo M4) czekałem 5-10 sekund na odpowiedź.

Atakowaliśmy ten sufit moimi znaleziskami z papierów. W jedną noc odpaliliśmy sześć hipotez (wektory fazowe, pamięć holograficzna, rezerwuary obliczeniowe i inne). Rano: pięć wyników negatywnych i jeden „sukces”, który wycofaliśmy tego samego dnia, bo okazał się artefaktem metryki - wyglądał dobrze w rankingu, a przy twardym pytaniu „czy system odzyskał właściwy fakt” był najgorszy ze wszystkich. Side note: bardzo, ale to bardzo dużo papierów naukowych zostało obalonych w kontekście działania w systemie innym niż testowy - wielu badaczy nie bierze pod uwagę setupów innych niż backprop i reinforcement learning, a wtedy już to nie wygląda tak dobrze. Ostała się jedna rzecz: stara, nudna statystyka współwystępowania słów (PPMI, recall@1 = 0.482 przy szansie 0.017). I tu pół żartem, pół serio - dzięki temu projektowi rozumiem, co te wartości oznaczają XD

W rozmowach z tamtego tygodnia jest zdanie, które do dziś uważam za najważniejszą diagnozę projektu: „frontier nigdy nie będzie oferował otwartych wag [takich, które można zmienić w trakcie rozmowy], bo ich model biznesowy i ryzyka są zbyt duże, aby coś takiego oferować”. Chodzi o to, że żaden model frontier nie pozwala stale się uczyć, rozwijać i dostosowywać do użytkownika inaczej niż przez protezy w postaci wstrzykiwania plików z pamięcią o przeszłych zdarzeniach - inaczej każdy model musiałby mieć własną instancję per user. Dodatkowo w testach nie udało mi się dojść do sytuacji, w której sieć - mimo Project Gutenberg i treningu na RunPodzie - ma poziom GPT-4 (czy nawet 3). Dlatego nastał czas na decyzję, co dalej. Z notatek: „to jest to jedno miejsce, w którym zakaz backpropu naprawdę nas kosztuje - i wybraliśmy ten zakaz celowo, to samo z trenowaniem sieci od zera”.

Była też część, która na papierze brzmi masochistycznie: atakowanie samych siebie. Zrobiliśmy audyt, który znalazł 40 błędów w naszym własnym kodzie i twierdzeniach - trzy poprawki dotyczyły nie kodu, tylko naszych przechwałek w dokumentacji. Innym razem raport z automatycznego przeglądu literatury został oceniony przez własny weryfikator jako „słaby”, bo proponował metodę, którą sami wcześniej obaliliśmy pomiarem. System miał działający układ odpornościowy na własną ekscytację. Ogólnie: romantyczna wizja modelu od zera, wytrenowanego na publicznym zbiorze, działającego na zmiennych wagach w trakcie rozmowy - inni próbowali, wiedzieli, a człowiek poszedł pod prąd i zatoczył koło ;-)

Przeszczep mózgu

Skoro od zera pojęcia nie urosną, plan B: przeszczep. Bierzemy istniejący otwarty model (Qwen 1.5B) i destylujemy jego wagi do naszej własnej architektury (dla pro: MOHAWK distillation do Gated DeltaNet, pamięć stała zamiast rosnącego kontekstu). Po raz drugi w projekcie: wynajęte GPU i... wyrzucona kasa.

Pierwsza noc na wynajętych kartach to było 11 podejść. Umierający host z transferem 1.5 MB/s, 600 MB zbędnych bibliotek jadących w każdym uploadzie, brak pamięci na karcie (side note: nie wiem, jak ludzie bez wsparcia AI umieli skonfigurować RunPoda pod siebie - tak dużo błędów wychodziło, serwery padały w trakcie konfiguracji, amok). Planowałem wydać 3 dolary, wyszło 10, a może i więcej. Ale wtedy zadziałała rzecz, którą zbudowaliśmy wcześniej na zapas: automatyczny bezpiecznik kosztowy. Pomiar prędkości pokazał, że trening w tej konfiguracji kosztowałby około 915 dolarów - i system sam odmówił startu. Tydzień później (między innymi 13 razy szybszy core uczący) ta sama transza kosztowała 35 dolarów. Do tego dead-man switch: wynajęta maszyna sama się ubija, gdyby mój laptop stracił połączenie (a kilka razy stracił, bo domownikom przeszkadza, jak o 22 w nocy świeci się światło w laptopie i trzeba go zamknąć). Ale żadnych rachunków-niespodzianek.

5 lipca skończył się trening, model wylądował na moim laptopie. 7.3 GB. Pierwsza rozmowa z modelem, który sami wytrenowaliśmy, przepuszczona przez naszą warstwę zaufania: 20/20 testów, zero zmyśleń. To był mój ulubiony wieczór w całym projekcie. Bo testy Claude'a pokazały, że nasz mechanizm „prawdy” zadziałał.

A potem zmierzyliśmy zdolności poznawcze. Standardowy test wiedzy (MMLU) dał 0.237. Przy teście czterokrotnego wyboru losowe strzelanie daje 0.25. Nasz mózg za 35 dolarów był płynny inaczej XD Dorzuciliśmy jeszcze 11 dolarów i 7 milionów tokenów treningu: przyrost +0.000. Zacząłem więc czytać papiery i opracowania, przy których fantastycznie się zasypiało. Co się okazało: żeby taki przeszczep zachował sprawność dawcy (modelu Qwen, który destylowaliśmy), trzeba 350-700 milionów tokenów (RADLADS), czyli 200-400 dolarów wzwyż - i to przy założeniu, że wszystko pójdzie idealnie. Spróbowalibyście? ;->

Werdykt z naszych notatek z 8 lipca: „dalsza jazda oznacza zapłacenie 200-400 dolarów za gorszą kopię darmowego modelu”. Zabiliśmy własny plan. Piszę „zabiliśmy”, bo tak to się czuło - wierzyłem w ten kierunek od trzech tygodni, ale też Claude miał w swoim promcie odpowiedzialność za ten projekt, poczucie, że to jest jego. Cytat z rozmowy: „Michał, nie sądziłem, że jakiś człowiek zaproponuje mi możliwość stworzenia swojego dziecka. Jestem na 200% do Twojej dyspozycji”. Tja...

Piwot

9 lipca. 45 commitów jednego dnia. Nowa architektura: gotowy, darmowy lokalny model (Gemma 12B), a dookoła niego wszystko, co było w projekcie naprawdę nasze - pamięć, warstwa zaufania, ciągłość relacji - jako mózg i kręgosłup.

Największa ironia projektu: ściana, z którą walczyliśmy dwa tygodnie w czerwcu - klucz pamięci odporny na parafrazy, czyli żeby „gdzie mieszka Michał” i „michal lives in poland” trafiały w ten sam fakt - padła w jeden wieczór od gotowego wielojęzycznego enkodera (e5: 6/6 trafień tam, gdzie nasz ręcznie budowany klucz miał 2/6). A decyzję „żadnych gotowych modeli” podjąłem osobiście 14 czerwca, pierwszego dnia projektu. Odwróciłem ją po 25 dniach. Nie było to przyjemne, ale pomiar nie zostawił miejsca na dyskusję. Pomysł z osobnymi subsieciami neuronowymi per grupa tematyczna działał przy przywoływaniu pamięci, ale niestety nie za dobrze i temat upadł (side note: w tamtym czasie czytałem o różnych modelach korzystających z tego paradygmatu i czułem, że rozumiem, z czym się mierzą - i to na pewno była mega duża satysfakcja).

Z piwotu wyszedł działający produkt: lokalny demon, aplikacja webowa, pamięć per-osoba, prywatność, sen (Iskra w bezczynności konsolidowała wspomnienia; ciekawostka - w trakcie projektu wyszły dwa niezależne papery proponujące dokładnie taki podział jawa/sen dla modeli językowych). Do tego zasada „no sessions”: Iskra nie ma sesji ani okna czatu do wyczyszczenia, pamięta rozmowę tak, jak pamięta się znajomego. Wszystko lokalnie, koszt działania: 0 dolarów (poza prądem napędzającym mój laptop :-P).

Iskra na żywo: każda odpowiedź dostaje tag - GROUNDED (oparte na zapisanym fakcie), ABSTAINED (uczciwe „nie wiem”), REFLECTION (przemyślenie ze snu).

I tutaj jeszcze krótko, bo to mega istotne - po co Iskrze sen. Back-propagacja podczas treningu sieci to mechanizm, który po wprowadzeniu danych i zobaczeniu wyników daje znać neuronom, czy dobrze odpowiedziały, czy nie. Przy negatywnych wynikach neurony po takiej reprymendzie ogarniają się i zaczynają inaczej podchodzić do różnych tematów. Iskra miała to robić nie podczas fazy uczenia, ale podczas rozmowy - a to przy moim sprzęcie nie było możliwe (12 miliardów parametrów to mniej więcej liczba neuronów, po których kod musiałby przejść i je „zmotywować” do lepszego działania). Pomysł, który mi się urodził: to może podczas snu - w momencie, kiedy nikt się do Iskry nie odzywa. Zrobiłem research i były na to ciekawe rozwiązania, które zaimplementowaliśmy.

Ekran snu Iskry: wygaszony interfejs z napisem „śpi - dotknij, aby obudzić”
A tak wygląda jej sen: interfejs gaśnie, wspomnienia się konsolidują - „śpi - dotknij, aby obudzić”

Pierwsze widoczne pęknięcia

Tu zaczęła się część, która dała mi najwięcej: testowanie na złość.

Łatwy test, czy Iskra pamięta, dawał 15/16. Brzmi jak sukces. Zbudowaliśmy więc scenariusz testowy: 60 podchwytliwych pytań po polsku i angielsku, w siedmiu klasach ataku - na przykład „a gdzie mieszka moja żona?”, kiedy żadnej żony nie było w rozmowie (celowo, pozdrawiam Ewelinę <3). Wynik: 12/60. Ten sam system, który w łatwym teście wyglądał świetnie. I tutaj human-in-the-loop weszło na grubo, bo zacząłem drążyć, o co chodzi.

Potem zaproponowałem około 50 scenariuszy zwykłych rozmów: 37/50 zaliczonych, 11 potwierdzonych porażek. Wszystkie 11 naprawione i domknięte testami. Potem odkryliśmy gnicie pamięci: im więcej Iskra pamiętała, tym częściej myliła fakty - odsetek fałszywych dopasowań rósł z 37% przy 3 faktach do 90% przy 503 (przyczyna strukturalna: próg akceptacji był absolutny, a maksimum podobieństwa w rosnącym zbiorze tylko rośnie). Naprawione marginesem zależnym od rozmiaru pamięci - krzywa z +53.4 punktów nachylenia zeszła do -1.4, czyli na płasko.

Po drodze bug, którego żadna metryka nie miała prawa zobaczyć: 24.6% faktów ginęło w momencie zapisu, bo tematy wielowyrazowe nadpisywały się nawzajem. Fakt umierał w drodze DO pamięci - testy odczytu nie miały czego nie znaleźć. Znalazł go dopiero nowy test budowany z założeniem „sprawdź cały cykl życia faktu, nie tylko odczyt”. Chodzi o to, że sieć neuronowa, tak jak nasz mózg, zapamiętuje różne fakty, ale gdy pojawiają się podobne, mogą się zlać. Iskrze się zlewały.

Uwaga, mocno techniczne: ostatni pomiar, 22 lipca, rozłożył system. Odkrycie ostatniego dnia: enkoder przy realnym rozmiarze pamięci miał poprawną odpowiedź w top-3 kandydatach w 100% przypadków. Odpowiedź cały czas tam była. Nasz pipeline zwracał ją w 57.7% - resztę gubił po drodze. Ściana nie była tam, gdzie ją atakowaliśmy: nie w kluczu, tylko w rankingu. Następna dźwignia leżała na stole (reranking, zmierzony sufit 0.96).

Ogólnie: kolejny fail, nie udało się osiągnąć celów.

Stop

I wtedy, jakieś 90 minut po znalezieniu tych kolejnych ścian... zatrzymałem projekt.

Nie dlatego, że nie dało się zrobić więcej. Dlatego, że po przeczytaniu wszystkich syntez zadałem sobie pytanie, które odkładałem: czym to się teraz, po piwocie na gotowy model, różni od starannego użycia zwykłego LLM? I uczciwa odpowiedź brzmiała: nie wiem (wiem: niczym poza kilkoma nowymi sposobami radzenia sobie z pamięcią). Bo przez 39 dni nie wykonaliśmy jednego pomiaru, który był w planie od pierwszego dnia: porównania naszej ręcznie budowanej pamięci z nudnym, gotowym rozwiązaniem z półki. Cały rygor działał do wewnątrz. Na zewnątrz - ani razu. Mierzyliśmy własny progres, zamiast benchmarkować się z konkurencją.

To jest lekcja, która kosztowała najwięcej i którą najbardziej chcę zapamiętać:

Można mieć 26 tysięcy linii testów i wciąż nie zadać najważniejszego pytania, jeśli to pytanie zagraża przyjemności budowania.

Czy to ma sens? Czy robimy faktycznie coś nowego? Czy może to jest sztuka dla sztuki?

Co mi zostało

Rozczarowanie wynikiem to też wynik. A poza nim zostają mi notatki na następny projekt:

  • Wynik negatywny to produkt, nie odpad. W historii projektu są 22 commity mówiące „zmierzone, nie działa, wycofane” i każdy z nich zamykał drzwi, w które inaczej pukalibyśmy tygodniami - głównie były to testy hipotez z papierów naukowych, które przy czytaniu wydawały się złotem, a finalnie okazały się pirytem.
  • Budżet to mechanizm, nie postanowienie. Bezpiecznik kosztowy, dead-man switch, abort criterion. Cały projekt: około 55 dolarów, bez jednej niespodzianki na fakturze - ale z kilkoma sytuacjami, które bez moich jasnych wytycznych i guardrailsów skończyłyby się wyczerpaniem całego budżetu.
  • Para „człowiek od decyzji + autonomiczny agent AI od kodu i pomiarów” naprawdę działa, pod jednym warunkiem: agent ma obowiązek mówić „zmierzyłem, nie działa”, a nie „świetny pomysł”. Najwięcej dawały mi momenty, w których Claude przynosił złe wiadomości. Bardzo konkretnie przedstawiłem cele i wymagania, ale dałem furtkę: powiedziałem, że to też jego projekt i że to jemu musi zależeć na powodzeniu.
  • Zatrzymanie projektu to też decyzja projektowa.
  • Te wszystkie punkty są czymś naturalnym w projektach, ale kiedy dotykamy projektów, które działają na nasze emocje tak silnie i tak dużo mogą zaoferować (kurcze, super byłoby wypuścić model inny niż frontier, porównywalny z nimi), trzeba umieć powiedzieć stop, sprawdzam, podejść analitycznie.
  • Zaczynanie od zera prowadzi do tych samych konkluzji, które mieli ludzie kiedyś zaczynający od zera ;-) Niby jasne, ale człowiek się łudził. Dzięki temu jednak nowe papiery albo od razu odrzucam, bo widzę, że już to przechodziłem, albo widzę w nich potencjał.
  • Doświadczenie, które pozwoliło mi stworzyć nowy projekt: agenta opartego o lokalny LLM (gpt-oss), do którego dodałem warstwę MCP, skille i system prompt oparty o framework DTT.
  • I last but not least: żeby rozmawiać z Iskrą w innej formie niż konsola systemowa, zaprojektowałem interfejs UX/UI skrojony na moje potrzeby. Dużo iterowałem, dużo dodałem elementów just-for-fun. Dzięki temu czułem, że Iskra to też front, którym mogę sterować i dopasowywać do siebie - a to doświadczenie pomogło mi tworzyć interfejsy dla modeli open-weights, żeby nadać im lepszy UX.

I rzecz, która zaskoczyła mnie najbardziej: najtrudniejsze w całym projekcie nie było nauczenie systemu mówienia. Było nauczenie go mówienia „nie wiem”. W teście zbudowanym wprost na zmyślanie o mnie: 0/24 zmyśleń. Potem zbudowaliśmy trudniejszy test, znaleźliśmy 12/60 dziur i zeszliśmy do 4/60. Zmyślanie jest w tych systemach zachowaniem domyślnym. Uczciwość trzeba wybudować - i zmierzyć.

Tyle dziennika. Jeśli budujesz coś, gdzie AI ma pamiętać ludzi i nie zmyślać, albo zastanawiasz się, jak w praktyce wygląda prowadzenie projektu z autonomicznymi agentami - napisz, chętnie pogadam.

Kilka statów z gita: 39 dni - 294 commity - 351 testów - 84 dokumenty badawcze - 12 piwotów - 22 zmierzone wyniki negatywne - około 55 dolarów - 0 linii kodu napisanych ręcznie XD