Ten dokument opisuje architekturę i kluczowe decyzje projektowe stojące za aplikacją LLMClient. Zrozumienie tych koncepcji jest kluczowe dla deweloperów chcących rozwijać i utrzymywać projekt.
LLMClient jest zbudowany w oparciu o wzorzec Model-View-ViewModel (MVVM), który jest standardem dla aplikacji .NET MAUI. Taka struktura zapewnia czysty podział odpowiedzialności między interfejsem użytkownika (View), logiką prezentacji (ViewModel) a danymi (Model).
Architektura opiera się na trzech głównych filarach:
- Modułowy, zorientowany na serwisy backend: Logika biznesowa jest zamknięta w serwisach wstrzykiwanych przez mechanizm Dependency Injection (DI). Każdy serwis ma jasno określoną odpowiedzialność (np. komunikacja z AI, obsługa bazy danych, generowanie osadzeń).
- Reaktywny interfejs użytkownika: Widoki (XAML) są "głupie" i jedynie wiążą się z danymi i komendami z ViewModeli. Zmiany w stanie ViewModelu są automatycznie odzwierciedlane w UI.
- Natywna integracja dla krytycznych zadań: Zadania wymagające wysokiej wydajności, takie jak tokenizacja tekstu, zostały zaimplementowane w Rust i zintegrowane z aplikacją C# poprzez FFI (Foreign Function Interface), aby uniknąć wąskich gardeł wydajnościowych platformy .NET.
- Technologia: XAML
- Odpowiedzialność: Definicja struktury i wyglądu interfejsu użytkownika.
- Kluczowe cechy:
- Responsywność: Użycie
VisualStateManagerdo dynamicznego dostosowywania layoutu między platformami mobilnymi i desktopowymi. - Wiązanie danych (Data Binding): Ścisłe powiązanie z właściwościami i komendami w ViewModelach.
- Brak logiki w code-behind: Pliki
.xaml.cszawierają minimalną ilość kodu, zazwyczaj ograniczoną do obsługi zdarzeń specyficznych dla UI (np. animacje, obsługa kontrolek).
- Responsywność: Użycie
- Technologia: C#
- Odpowiedzialność: Zarządzanie stanem widoku, implementacja logiki biznesowej i obsługa interakcji użytkownika.
- Kluczowe cechy:
INotifyPropertyChanged: Standardowy interfejs do powiadamiania widoku o zmianach w danych.ICommand: Implementacja komend (np.SendMessageCommand), które są wywoływane przez UI w odpowiedzi na akcje użytkownika.- Wstrzykiwanie zależności: ViewModels otrzymują wymagane serwisy poprzez konstruktor, co ułatwia testowanie i oddzielenie logiki.
- Technologia: C# (POCO - Plain Old CLR Objects)
- Odpowiedzialność: Reprezentacja danych aplikacji (np.
Conversation,Message,AiModel). - Kluczowe cechy:
- Proste obiekty danych: Nie zawierają logiki biznesowej.
- Atrybuty SQLite: Oznaczone atrybutami do mapowania obiektowo-relacyjnego (ORM) z bazą danych.
Serwisy stanowią rdzeń aplikacji. Są rejestrowane jako singletony lub obiekty przejściowe w kontenerze DI (MauiProgram.cs).
-
AiService: Abstrakcja nad Microsoft.SemanticKernel oraz lokalnym backendem ONNX Runtime GenAI. Odpowiada za formatowanie zapytań, komunikację z API modeli językowych (OpenAI, Gemini) oraz obsługę odpowiedzi strumieniowych i standardowych. Czyta edytowalny System Prompt z bazy; pamięć użytkownika wstrzykuje wyłącznie dla modeli chmurowych (gdy włączone w ustawieniach). Dla modelu lokalnego używa szablonu czatu zgodnego z tokenizerem i bez pamięci. -
DatabaseService: Zarządza lokalną bazą danych SQLite. Kluczowe decyzje:- Szyfrowanie: Użycie SQLCipher do szyfrowania całej bazy danych. Klucz szyfrujący jest generowany i bezpiecznie przechowywany w
SecureStoragespecyficznym dla platformy. - Bezpieczeństwo kluczy API: Klucze API modeli nie są przechowywane w bazie danych. Zamiast tego
SecureApiKeyServiceprzechowuje je wSecureStorage, aDatabaseServicejedynie zarządza metadanymi modeli. - Relacje: Użycie
SQLiteNetExtensions.Asyncdo zarządzania relacjami między tabelami (np. Konwersacja -> Wiadomości).
- Szyfrowanie: Użycie SQLCipher do szyfrowania całej bazy danych. Klucz szyfrujący jest generowany i bezpiecznie przechowywany w
-
EmbeddingService: Odpowiada za generowanie wektorów osadzeń (embeddings) dla tekstu. To jeden z najbardziej krytycznych komponentów:- Model ONNX: Wykorzystuje model
multilingual-e5-largew formacie ONNX, uruchamiany przezMicrosoft.ML.OnnxRuntime. Model jest pobierany przy pierwszym uruchomieniu z Hugging Face. - Natywna Tokenizacja: Zamiast używać tokenizera w C#, co byłoby wolne,
EmbeddingServicekomunikuje się z biblioteką Rust poprzezTokenizerNative(wrapper FFI). To zapewnia niemal natywną wydajność tokenizacji. - Normalizacja i Pooling: Implementuje logikę
mean poolingi normalizacji L2, aby uzyskać końcowy wektor osadzenia, gotowy do porównania cosinusowego.
- Model ONNX: Wykorzystuje model
-
SearchService: Implementuje logikę wyszukiwania:- Wyszukiwanie Pełnotekstowe: Proste wyszukiwanie oparte na wyrażeniach regularnych.
- Wyszukiwanie Semantyczne: Wykorzystuje
EmbeddingServicedo konwersji zapytania na wektor, a następnie prosiDatabaseServiceo znalezienie najbardziej podobnych wektorów w bazie danych za pomocą podobieństwa cosinusowego.
-
TokenizerRust(Natywna Biblioteka):
LocalModelService/RobustLocalModelService: Załadunek/rozładunek lokalnego modelu (np. Phi‑4‑mini‑instruct), generacja odpowiedzi (streaming i non‑streaming), kontrola limitów i tokenów stop. Wspiera dynamicznemax_length(uwzględnia długość promptu) i dekodowanie tokenów w locie.- Pobieranie i diagnostyka:
NetworkAwareDownloadService,SmartDownloadManager,LocalModelDiagnosticServiceodpowiadają za stabilne pobieranie modelu i raportowanie stanu. - Integracja UI: Podczas pobierania i ładowania modelu widoczny jest overlay ze spinnerem; gdy model lokalny jest załadowany, selektor modeli chmurowych jest zablokowany. Brak aktywnej konwersacji powoduje automatyczne utworzenie nowej.
-
SafeLocalModelWrapper: Obejmuje dowolnyILocalModelService, śledzi kolejne niepowodzenia i tymczasowo wyłącza usługę po przekroczeniu progu, aby zapobiec awariom. Wymusza okres „cooldown” i zapewnia bezpieczne wartości domyślne. -
Przełączalne silniki lokalne: Rejestracja w DI pozwala przełączać implementacje (np.
RobustLocalModelService,LlamaSharpLocalModelService) w runtime poprzezEngineSettings. Dzięki temu UI może zmieniać backend bez restartu aplikacji. -
Przepływy zdarzeń (
MessagingCenter):MainPageViewModeliModelSettingsViewModelsubskrybują zdarzenia takie jakLocalModelLoaded,LocalModelUnloaded,ModelsChanged, synchronizując stan UI (busy/overlay, blokada wyboru chmury) z aktualnym stanem modelu lokalnego.- Powód decyzji: Wydajność. Tokenizery oparte na C# są znacznie wolniejsze niż natywne implementacje. Rust został wybrany ze względu na bezpieczeństwo pamięci, wydajność i doskonałe biblioteki ekosystemu (np.
tokenizersod Hugging Face). - Implementacja: Biblioteka Rust (
tokenizer_rust.dll/.so/.dylib) eksponuje prosty interfejs C (tokenizer_init,tokenizer_encode,tokenizer_decode), który jest wywoływany z C#. - Zarządzanie Pamięcią: Pamięć jest zarządzana po stronie Rusta, co minimalizuje ryzyko wycieków w kodzie C#.
- Powód decyzji: Wydajność. Tokenizery oparte na C# są znacznie wolniejsze niż natywne implementacje. Rust został wybrany ze względu na bezpieczeństwo pamięci, wydajność i doskonałe biblioteki ekosystemu (np.
- Użytkownik wpisuje wiadomość w
MainPage.xamli klika "Wyślij". SendMessageCommandwMainPageViewModelzostaje wywołany.- ViewModel tworzy obiekt
Message, zapisuje go wDatabaseServicei dodaje do kolekcji w UI. - ViewModel wywołuje
_aiService.GetStreamingResponseAsync(). AiServicebuduje historię czatu i wysyła zapytanie do odpowiedniego modelu (np. Gemini lub lokalny ONNX, gdy aktywny).- Gdy fragmenty odpowiedzi (chunk) napływają,
AiServicezwraca je jakoIAsyncEnumerable<string>. MainPageViewModelodbiera fragmenty i za pomocąStreamingBatchServiceaktualizuje ostatnią wiadomość w UI w czasie rzeczywistym.- Po zakończeniu strumienia, pełna wiadomość jest zapisywana w
DatabaseService.
-
Pamięć (cloud‑only injection):
MemoryExtractionServicewydobywa informacje (regex + AI) z wiadomości użytkownika i zapisuje w DB.MemoryContextServicebuduje kontekst pamięci (do 30k znaków) i — tylko dla modeli chmurowych, gdy włączone — dołącza go do system message wAiService.- Gdy aktywny jest model lokalny, ekstrakcja AI i wstrzykiwanie pamięci są pomijane, by ograniczyć szum.
-
Wielojęzyczność:
AiServicenie wymusza języka — odpowiedzi dopasowują się do języka wiadomości użytkownika.- Modele chmurowe zapewniają szeroką obsługę języków; lokalny Phi‑4‑mini‑instruct najlepiej działa w EN/PL.
- Jeśli nie istnieje aktywna konwersacja, aplikacja automatycznie tworzy nową (np. na Androidzie po „Wyślij”).
- Gdy lokalny model jest załadowany, staje się aktywnym backendem; wybór modeli chmurowych w UI jest zablokowany do czasu rozładowania lokalnego.
- Użytkownik wpisuje zapytanie w
SemanticSearchPage.xaml. SemanticSearchViewModelwywołuje_searchService.SemanticSearchAcrossConversationsAsync().SearchServiceprosi_embeddingServiceo wygenerowanie wektora dla zapytania.EmbeddingServicetokenizuje zapytanie (przez Rust FFI) i przepuszcza je przez model ONNX, aby uzyskać wektor.SearchServiceprzekazuje wektor zapytania doDatabaseService.DatabaseServicewykonuje zapytanie, które pobiera wszystkie wektory wiadomości, oblicza podobieństwo cosinusowe w pamięci i zwraca posortowane wyniki.- Wyniki są propagowane z powrotem do
SemanticSearchViewModeli wyświetlane w UI.
- .NET MAUI: Wybrane dla maksymalizacji współdzielenia kodu między platformami.
- MVVM: Dla czystej, testowalnej i skalowalnej architektury UI.
- SQLite z SQLCipher: Dla bezpiecznego, lokalnego przechowywania danych bez zależności od zewnętrznych serwerów.
- Rust dla Tokenizacji: Krytyczna optymalizacja wydajności, która odróżnia ten projekt od typowych aplikacji .NET.
- Semantic Kernel: Zapewnia elastyczną abstrakcję, która ułatwia dodawanie nowych modeli AI w przyszłości.
Ta architektura została zaprojektowana z myślą o solidności, wydajności i łatwości w utrzymaniu, co czyni ją doskonałą bazą dla dalszego rozwoju.