Napisałem, że w mojej aplikacji nie ma AI. Musiałem to zmienić
· · Nie jednym promptem · odcinek 9 · Read in English
Na stronie Zakupomatu jest lista rzeczy, których w aplikacji celowo nie ma. Gazetki, szukanie najtańszego sklepu, wiele list, budżet... I jeszcze jedna pozycja, z której byłem trochę dumny: "AI w aplikacji". W czasach, kiedy każda lista zakupów ma "asystenta AI", my nie mamy. Darmowo, bez reklam, bez czatu, który kosztuje z każdym pytaniem.
W sobotę tę linijkę zmieniłem. Teraz brzmi: "czat z AI w aplikacji". Jedno słowo różnicy, a za nim cała historia 😉
Komentarz, który leżał miesiąc
Na początku października pod moim postem na LinkedIn czytelniczka napisała, że Zakupomat spełnia jej oczekiwania, ale jeśli już miałaby coś usprawnić, to pogrupowanie produktów. "Czasem łatwiej jest wybrać kategorię "nabiał" czy "chemia" i z niej wybrać konkretny produkt". I zaraz dodała, że nie czuje, żeby jej tego jakoś silnie brakowało. #simplicityrules
Zapisałem to w zgłoszeniach jako słaby sygnał. Bo kategorie to klasyczny kierunek, w którym listy zakupów puchną. Dokładnie to, co ludzi w innych aplikacjach wkurza.
Ale pomysł wracał. Kiedy baza produktów rośnie, przewijanie "Uzupełnij" w poszukiwaniu tego, czego brakuje w łazience, przestaje być zabawne.
Słownik? Nie, dziękuję
Pierwszy pomysł był "porządny" i bez AI: słownik nazwa → dział. Mleko → nabiał, płyn do naczyń → chemia. Zbudować go raz, poprawiać z czasem.
Tylko że w bazie było wtedy ponad 2000 różnych nazw produktów. W kilku językach. Z literówkami, skrótami, nazwami marek, "mleko dla Zosi", "ost" (po szwedzku ser), "danio", "plastry"... A do tego listy, które w ogóle nie są spożywcze - materiały stomatologiczne, klocki LEGO, zakupy w Castoramie. Słownik, który ma działać dobrze od pierwszego dnia, trzeba by klepać tygodniami. I nadal byłby dziurawy w ogonie.
Ten kierunek mi się po prostu nie podobał. Jakość kategorii, która raz na kilka produktów wrzuca jogurt do chemii, to jakość, której nie byłbym w stanie zaakceptować. A ręcznie poprawiany słownik to praca, która nigdy się nie kończy.
Więc wymyśliłem, jak zrobić to inaczej.
Jedna baza dla wszystkich, AI tylko do nowych nazw
Założenia, które zapisałem w zgłoszeniu:
- Stała lista kategorii w aplikacji. Wyszło ich 41 - bardziej szczegółowo niż mniej, osobno pieczywo, osobno makarony, osobno ryż i kasze. Nazwy w 26 językach, w języku aplikacji, a nie produktów. Masz produkty po polsku, a aplikację po szwedzku? Kategorie będą po szwedzku.
- Jedna wspólna baza powiązań "nazwa produktu → kategoria", bez żadnej informacji, czyja to nazwa. "Mleko" ma kategorię raz, dla wszystkich rodzin. Usunięcie rodziny tej bazy nie rusza, bo wpisy nie wskazują na nikogo.
- AI pyta się tylko o nazwy, których w bazie jeszcze nie ma. Pierwsze "mleko" kosztuje grosz, każde kolejne - zero.
- Na start cała istniejąca baza, ale mądrze i tanio - bez pytania o duplikaty.
Najpierw chciałem polski model - Basal od Remigiusza Kinasa, model decyzyjny właśnie do takich zadań. Niestety wymaga self-hostingu, a dobrze radzi sobie tylko po polsku i angielsku. A ja mam 26 języków.
Do klasyfikacji wybrałem więc Jev od TypeSafe AI. Nie czat, nie duży model do wszystkiego, tylko usługa do takich zadań: dajesz listę możliwych odpowiedzi i pytanie, dostajesz wybór i pewność. Cena? $0.042 za milion tokenów na wejściu, odpowiedzi za darmo. Tak, za milion 😮
Diabeł w szczegółach
Kod pisał Claude, ale kilka rzeczy było dla mnie ważniejszych niż sam kod.
Normalizacja nazw. Zanim nazwa trafi do bazy (i do AI), przechodzi przez sito: małe litery, bez "x2", "500g", "30%", bez nawiasów i bez końcowego "dla ...". "Mleko 2% (duże)" i "mleko" to jedna pozycja. A "mleko dla Zosi" to po prostu "mleko" - imię dziecka nie musi lecieć do żadnej usługi w USA. Wyjątek: jeśli po obcięciu zostaje jedno słowo, "dla" zostaje. "Karma dla kota" bez "dla kota" to już tylko "karma" 😂
"Mąka" to nie "maka". Na teście z prawdziwą bazą danych wyszło, że baza, porównując teksty, traktuje "ą" i "a" jak tę samą literę, a Python nie. Dwie różne nazwy zderzały się w jednym kluczu. Teraz klucz porównujemy bajt po bajcie. Drobiazg, który przy kilku językach z ogonkami robi różnicę.
Paczki po 20. Sprawdziliśmy na próbce 213 nazw dwa warianty: jedna nazwa na zapytanie albo paczka. Paczka po 20 była trafniejsza - 97% wobec 95%. Kontekst innych produktów pomaga zgadywać. 50 w paczce już się nie mieściło w limicie. Koszt całej próbki? Około 5 groszy.
Ważne - próbka była zmyślona, na wzór prawdziwych list. Prawdziwe nazwy nie wyszły nigdzie, dopóki nie zmieniłem polityki prywatności. Najpierw polityka z nowym podmiotem przetwarzającym, umową powierzenia i zakazem trenowania na naszych danych, a dopiero potem klucz na serwerze.
Próg pewności przy odczycie, nie przy zapisie. AI zwraca kategorię i to, jak bardzo jest pewne. Poniżej progu produkt ląduje w "Inne". Ale zapisujemy wszystko, a próg stosujemy dopiero przy pokazywaniu. Dzięki temu zmiana progu to jedna liczba w konfiguracji, a nie ponowne pytanie AI o całą bazę.
I dobrze, bo próg trzeba było zmienić już tego samego dnia.
Migracja: jedna pętla, jeden klucz
Nie było osobnego skryptu do "migracji". Na serwerze kręci się jedna pętla - co 15 sekund zbiera nazwy bez kategorii ze wszystkich rodzin i pyta o nie Jev, po 20. Dla nowego produktu to samo, co dla całej bazy. Po zapisie serwer wysyła zdarzenie i telefony odświeżają się same.
Migracja ruszyła więc w momencie, w którym na serwerze pojawił się klucz. Nie przy wdrożeniu kodu, tylko wtedy, kiedy ja zdecydowałem.
Wynik: 2079 nazw w 3 minuty. I to głównie przez to, że pętla czeka 15 sekund między obrotami - Jev odpowiadał na 200 nazw w około 3 sekundy. Konsola TypeSafe za cały ten dzień - próbki, migracja i nowe produkty razem: 367 zapytań, 5,4 mln tokenów, $0.18. Tak, 18 CENTÓW. Najwięcej tokenów nie zjadają same nazwy, tylko lista 41 kategorii, która idzie w każdym zapytaniu.
Próg ustawiony na próbce na 0,6 zostawił w "Inne" 21% nazw. Za dużo. Przejrzeliśmy przedział 0,5-0,6 na prawdziwych danych: koperek → świeże zioła, budyń → mąka i pieczenie. W większości dobrze. Poniżej 0,5 zaczynały się kwiatki: kola → owoce, pazłotko → sery 😂 Zszedłem na 0,5. Teraz 97,7% produktów ma kategorię, a w "Inne" zostało 299 nazw - głównie krótkich i dwuznacznych.
I nawet powyżej progu nie wszystko było tak, jak bym chciał. Mój faworyt: "soda oczyszczona" wylądowała w napojach 😜 Dla modelu, który zna angielski lepiej niż polskie wypieki, "soda" to przecież gazowany napój. Dlatego rodzina musi móc to poprawić u siebie.
AI się pomyli. Co wtedy?
Model się myli i będzie się mylił. Ważniejsze od tego, jak często, jest to, co się dzieje potem. U mnie to trzy zasady.
- Poprawka użytkownika nie zmienia wyniku dla wszystkich. W edycji produktu wybierasz inną kategorię z tej samej stałej listy 41. Nowych kategorii tworzyć się nie da. Wybór obowiązuje tylko w tej rodzinie. Nie poprawia wspólnej bazy, bo jedna rodzina trzyma ser w "Inne" z jakiegoś swojego powodu i nie będzie tego narzucać wszystkim. W panelu widzę tylko zbiorcze pary "nazwa, było → jest", bez rodzin. Gdy wiele rodzin poprawia tak samo, poprawiam bazę ręcznie.
- Wyłącznik ma użytkownik, nie tylko ja. "Automatyczne kategorie" można wyłączyć dla całej rodziny. Wyłączony - nazwy tej rodziny nie wychodzą nigdzie. Kategorie, które baza już zna, dalej działają, bo to odczyt lokalny.
- Akcja, która zmienia wszystko naraz, ma potwierdzenie i "Cofnij". Pod "Więcej" przeciągasz kategorie w kolejności swojego sklepu, a "Uzupełnij" idzie za nią. I przycisk "Ułóż produkty w sklepie wg kategorii", który przestawia całą kolejność sklepu jednym ruchem. Ręczne ułożenie w środku kategorii zostaje. Ten przycisk wymyśliłem po pierwszym teście w TestFlight i dorzuciłem warunek: "koniecznie z potwierdzeniem, żeby nikt przez przypadek nie zepsuł sobie listy". Jest potwierdzenie i jest "Cofnij" przez 10 sekund.
To próba
Na stronie i w polityce prywatności napisałem wprost - to próba. Jeśli Jev zacznie kosztować za dużo, wyłączam automat. Baza powiązań i ręczne wybory zostają, kategorie dalej działają, tylko nowe nazwy przestają dostawać je same.
Bo zasada Zakupomatu jest prosta - koszt utrzymania nigdy nie wymusi reklam ani opłat. AI, które kosztuje raz za nazwę i w sumie 18 centów na całą bazę, się w tej zasadzie mieści. Czat z AI, który kosztuje z każdym pytaniem, już nie. I dlatego na liście "czego nie ma" stoi teraz "czat z AI", a nie "AI".
Cała funkcja - od "Zrobimy kategorie z Jev?" do drugiego buildu w TestFlight z ekranem kolejności - powstała w jedną sobotę. Ale decyzja, jak ją zrobić, dojrzewała miesiąc.
Co to znaczy dla Twojego zespołu: AI w produkcie nie musi być czatem. Często najlepiej sprawdza się tam, gdzie go nie widać - jako tani klasyfikator w tle, z bazą, która uczy się raz, progiem, który zmieniasz bez kosztów, i wyłącznikiem, gdyby rachunek urósł. W ANSLAN pomagamy zespołom znaleźć takie miejsca i wdrożyć je odpowiedzialnie - z prywatnością i kosztem policzonymi przed startem.
Zakupomat to darmowa aplikacja do wspólnych zakupów rodziny, bez reklam: zakupomat.app. Kategorie są w wersji 1.10.0 🙂