Jak Dreamalizing uruchamia model językowy bez chmury
Sen jest najbardziej osobistą rzeczą, jaką można zapisać. Dlatego tekst snu w Dreamalizing nigdy nie trafia do zewnętrznej usługi AI. Ten artykuł opisuje, jak to wygląda od strony technicznej, jakie decyzje z tego wynikły i co po drodze poszło nie tak.
Został napisany dla programistów i dla każdego, kto chce sprawdzić, czy obietnica prywatności to coś więcej niż tylko zdanie na stronie głównej.
Obietnica w jednym zdaniu
Twój tekst snu jest przetwarzany przez nasz własny model językowy na serwerach w Unii Europejskiej. Twoje dane są przechowywane w formie zaszyfrowanej, a tekst snu nie jest przesyłany do zewnętrznych usług AI.
To zdanie znajduje się w aplikacji, w sklepach i na tej stronie. Wszystko poniżej istnieje po to, aby je spełnić, nawet jeśli ktoś popełni błąd w pliku konfiguracyjnym.
Łańcuch, od telefonu do modelu
Aplikacja (React Native, Expo) komunikuje się z API (NestJS, PostgreSQL) na wynajętym serwerze w UE. Serwer ten nie posiada modelu językowego. Poprzez tunel WireGuard łączy się z bramą (gateway) na własnym sprzęcie, a ta brama komunikuje się z modelem.
Modelem jest Gemma 4 z 26 miliardami parametrów w architekturze mixture-of-experts, z których około 4 miliardów jest aktywnych na token, w kwantyzacji 8-bitowej. Jeden model robi wszystko: odpowiada na pytania, przeprowadza analizę i wykonuje ustrukturyzowaną ekstrakcję. Okno kontekstowe jest stałe i wynosi 32 768 tokenów, ponieważ najcięższa analiza do tej pory zużyła 13 671 tokenów, a wyjście może liczyć do 8 192 tokenów.
Do sierpnia działały dwa modele: cięższy do pytań i analizy oraz lżejszy do ekstrakcji. Okazało się to droższe niż przyniosło korzyści. Dwa modele wypierały się w pamięci, co powodowało, że przy każdym śnie model był ładowany trzy razy, co zajmowało od 7 do 10 sekund. Cięższy model wykonuje ekstrakcję o 3,7 sekundy wolniej, ale oszczędza 27 sekund czasu ładowania.
Lokalność jako zabezpieczenie, a nie cel
API posiada warstwę routingu, która może kierować zapytania do wielu dostawców. Jest to przydatne podczas rozwoju, ale niebezpieczne w produkcji. Dlatego zastosowaliśmy trzy zabezpieczenia.
Po pierwsze, twarda flaga w runtime. Jeśli jest włączona, API odrzuca każde wywołanie zawierające treść snu skierowane do dostawcy spoza lokalnej infrastruktury. Istnieje zawór bezpieczeństwa, ale jest on ograniczony datą końcową maksymalnie czternastu dni i wygasa automatycznie; nieprawidłowa lub zbyt odległa data jest ignorowana, a zabezpieczenie pozostaje aktywne.
Po drugie, brama (gateway). Każde ID modelu, o które pyta API, zaczyna się od przedrostka local-. Brama odczytuje to jako tryb local-only i odrzuca takie żądanie do dostawcy chmurowego, nawet jeśli łańcuch awaryjny wskazuje na niego.
Po trzecie, testy. Jeden test wymusza, aby API nigdy nie usuwało przedrostka, a inny sprawdza, czy konfiguracja modelu w środowisku testowym i produkcyjnym jest identyczna. Pierwszy test istnieje, ponieważ do końca sierpnia API jednak usuwało ten przedrostek. W rezultacie awaryjne przekierowanie bramy mogło teoretycznie wysłać tekst snu do chmury ze statusem 200 i bez żadnego sygnału ostrzegawczego. Aplikacja nie została wtedy jeszcze uruchomiona i poza nami nie miała aktywnych użytkowników. Zamknęliśmy tę ścieżkę i dodaliśmy test, aby nie można było tego zrobić ponownie, nawet niezauważalnie.
Czas oczekiwania to wybór projektowy
Lokalny model na jednej maszynie jest wolniejszy niż API w chmurze. Odpowiedzi pojawiają się po 20 do 40 sekundach, analiza po 30 do 60. Nie ukrywamy tego; aplikacja informuje, co się dzieje i jak długo to potrwa.
Kolejność działań została pod to dostosowana. Najpierw pytania trafiają do aplikacji na podstawie surowego tekstu snu. Następnie, równolegle, drugie wywołanie buduje ustrukturyzowane dane, których analiza będzie potrzebować później. W międzyczasie śniący odpowiada na pytania i nie zauważa drugiego wywołania.
Na maszynie działa ogranicznik, który pozwala na jedno wywołanie na użytkownika jednocześnie i nigdy nie pozwala, aby wywołania opcjonalne wyprzedziły obowiązkowe. O tym ostatnim dowiedzieliśmy się podczas incydentu produkcyjnego: wywołanie opcjonalne wyparło obowiązkowe, a śniący nie musiał na nic czekać.
Przechowywanie i tryb offline
Tekst snu i odpowiedzi są przechowywane zaszyfrowane dla każdego użytkownika przy użyciu AES-256-GCM oraz własnego klucza dla każdego użytkownika, który z kolei znajduje się pod kluczem głównym. W telefonie kolejka jeszcze niewysłanych snów znajduje się w zaszyfrowanej bazie SQLCipher z kluczem w bezpiecznym magazynie urządzenia.
Każda operacja zapisu posiada stabilne ID operacji. Jeśli połączenie zostanie przerwane po wysłaniu, ale przed otrzymaniem odpowiedzi, ponowienie operacji dostarczy ten sam sen, a nie drugi. Rejestr tych ID nigdy nie zawiera tekstu snu, a jedynie rodzaj akcji, hash i wynik.
Koszty
Jedna maszyna to jeden sufit. Przy jednoczesnych użytkownikach zapytania się kumulują, a czas oczekiwania rośnie. To jest cena braku chmury i powód, dla którego zaczęliśmy w Holandii, a nie na całym świecie.
Drugim kosztem jest to, że model jest słabszy niż największe modele chmurowe. Dlatego zmiany w promptach zawsze mierzymy na modelu lokalnym, nigdy na modelu chmurowym; to, co działa na większym modelu, nie zawsze działa tutaj.
Co dalej
Pytania dotyczące architektury są mile widziane. To, co tutaj czytasz, to stan na wrzesień 2026; jeśli coś się zmieni, ten artykuł również zostanie zaktualizowany.