Tworzenie lokalnego agenta AI: modele, pamięć, przechowywanie i interfejs
Pięć warstw stosu lokalnego agenta AI, z porównaniem Ollamy, OpenClawa, Mem0 oraz Qdrant lub Chromy, plus poprawki dotyczące JSON w trybie myślenia i niewidocznych niezgodności wymiarów.
Źródła: Building Local AI Agents: A Practical Guide to Models, Memory, and Orchestration
Agent AI działający lokalnie przechowuje model na twoim sprzęcie, działa w twoim imieniu i przenosi kontekst z jednej sesji do drugiej, bez wysyłania danych do zewnętrznego API. Zwykły chatbot odpowiada i zapomina. Agent może rozumować krok po kroku, korzystać z narzędzi i gromadzić wiedzę z upływem czasu. Dzięki uruchomieniu go lokalnie masz asystenta bez kompromisów dotyczących prywatności czy opłat za każdą rozmowę, jakie występują w usługach hostowanych.
Aby stworzyć takiego agenta, muszą współpracować pięć warstw: warstwa LLM, która przeprowadza obliczenia na twoim komputerze, framework agenta, który kieruje działaniami, warstwa pamięci, która poprawia funkcjonowanie agenta z czasem, warstwa przechowywania, która zachowuje zebrane informacje, oraz warstwa interfejsu, dostosowana do twojego sposobu pracy. Poniżej dla każdej warstwy podano opcje, kompromisy oraz rekomendacje, abyś mógł skomponować rozwiązanie odpowiednie dla swojej sytuacji.
Warstwa 1: model
To jest silnik. Model wykrywa intencję, generuje kod oraz wydobywa strukturyzowane dane, od których zależy reszta systemu, a wszystko inne to elementy integracyjne opracowane wokół jego niezawodnych możliwości. Trzy narzędzia dominują w lokalnej inferyencji.
Ollama to najprostszy sposób na rozpoczęcie pracy. Jednym poleceniem go instaluje się, drugim ładuje model, a od razu ma się dostęp do API kompatybilnego z OpenAI na lokalnym serwerze. Zarządza modelami, wybiera metodę kwantyzacji oraz ustawia okna kontekstowe bez konieczności konfiguracji. Nie został stworzony do obsługi ruchu produkcyjnego i lepiej nadaje się jako narzędzie asystenta osobistego lub dewelopera.
ollama pull qwen3:8b
ollama pull nomic-embed-text
llama.cpp znajduje się na niższym poziomie. Jest to silnik napisany w C++, który uruchamia skwantyzowane modele GGUF bezpośrednio, bez żadnego demona ani warstwy HTTP. Jego niewielki zużycie pamięci sprawia, że nadaje się doskonale do urządzeń brzegowych takich jak Raspberry Pi lub systemy Linux wbudowane, gdzie Ollama byłby zbyt obciążający. Wadą jest brak wbudowanego zarządzania modelami oraz konieczność ręcznej konfiguracji.
vLLM to opcja przeznaczona do produkcji. Wykorzystuje techniki PagedAttention i ciągłego grupowania zadań, stosowane w skali przemysłowej do wykonywania obliczeń w chmurze. Jeśli obsługujesz wielu użytkowników lub masz duże wymagania co do przepustowości, to właśnie ta opcja jest odpowiednia, choć wymaga odpowiedniej infrastruktury GPU i skaluje się lepiej niż pozostałe dwa rozwiązania.
Dla lokalnego asystenta pojedynczego programisty Ollama jest rozsądnym standardem, ponieważ prostota ma większe znaczenie niż prędkość przetwarzania. Warto wspomnieć o modelu qwen3:8b: zapewnia niezawodne dane w formacie JSON do wykrywania intencji, generuje kod w czytelny sposób i mieści się w 8 GB pamięci VRAM. Należy znać jedną osobliwość – model otacza swoją odpowiedź blokami <think>…</think> przed rzeczywistą treścią, co utrudnia pracy dowolnego parsera JSON przetwarzającego surową odpowiedź. Mały wrapper, który wszędzie ustawia think: False, rozwiązuje ten problem:
_orig_ollama_chat = ollama.chat
def _no_think_chat(*args, **kwargs):
opts = kwargs.get("options") or {}
if isinstance(opts, dict):
opts.setdefault("think", False)
kwargs["options"] = opts
return _orig_ollama_chat(*args, **kwargs)
ollama.chat = _no_think_chat
Ponieważ wykorzystuje setdefault, poszczególne użytkownicy mogą nadal włączyć tryb myślenia, gdy tego chcą. Przy wydobywaniu danych w formacie JSON oraz generowaniu kodu w ramach pipeline narzędziowy, tryb myślenia powoduje opóźnienia bez poprawy jakości.
Szczegół 2: framework agenta
Model generuje tekst. Framework decyduje, co zrobić z tym tekstem i jakie kroki podjąć dalej, obejmując klasyfikację intencji, kierowanie i wykonywanie zadań.
OpenClaw skupia się przede wszystkim na narzędziach i działa na poziomie systemu plików. Każda akcja to polecenie wysyłane do określonej umiejętności, takiej jak tworzenie pliku, pisanie kodu, otwieranie folderu lub uruchamianie polecenia w shellu. Tworzy się plik SKILL.md, w którym określa się nazwę umiejętności oraz sposób jej uruchamiania, a framework zajmuje się kierowaniem zadaniami:
- -
name: local-ai-assistant
description: Local AI coding assistant with persistent memory
command-dispatch: tool
command-tool: exec
command-arg-mode: raw
-
Gdy większość operacji polega na odczytywaniu i zapisywaniu plików, jak w przypadku asystenta programistycznego, ten model pasuje idealnie. Linie command-dispatch: tool oraz command-tool: exec instruują OpenClaw do uruchamiania skryptów w Pythonie po wywołaniu danej umiejętności, dzięki czemu praca z plikami odbywa się poprzez ten framework. Jego ograniczeniem jest to, że nie został zaprojektowany do długich łańcuchów rozumowania składających się z wielu kroków ani do koordynacji kilku agentów.
LangGraph przyjmuje przeciwny podejście. Definiuje się w nim wyraźny graf stanów, w którym węzłami są wywołania modeli lub działania, a krawędziami przejścia pomiędzy nimi; framework przenosi stan między poszczególnymi krokami. Cena takiego rozwiązania to konieczność wcześniejszego zaprojektowania: trzeba ustalić schemat stanów i strukturę grafu przed wprowadzeniem jakiejkolwiek logiki biznesowej. W przypadku prostego używania narzędzi jest to nadmierna skomplikowaność, natomiast przy złożonych procesach wieloetapowych umożliwia tworzenie sprawdzalnych zapisów tego, co wydarzyło się na każdym kroku.
CrewAI znajduje się na jeszcze wyższym poziomie. Opisujesz agenty poprzez role i zadania wraz z ich opisami, a framework sam określa kolejność wykonywania działań oraz komunikację między agentami. To najszybsza droga do stworzenia działającego prototypu wieloagentowego, chociaż abstrakcja utrudnia debugowanie błędów.
Niezależnie od wyboru, klasyfikacja intencji jest pierwszą czynnością przeprowadzaną przy każdej otrzymanej wiadomości. Połączenie klasyfikacji opartej na modelach z metodą awaryjną opartą na regex radzi sobie lepiej z błędami niż każda z metod osobno:
def detect_intent(message: str) -> dict:
try:
resp = ollama.chat(
model=OLLAMA_CHAT_MODEL,
messages=[{"role": "user", "content": INTENT_PROMPT.format(message=message)}],
options={"temperature": 0, "num_predict": 1024},
)
return extract_json(resp["message"]["content"])
except Exception:
return keyword_intent_fallback(message)
Tryb awaryjny radzi sobie z typowymi problemami, gdy model zwraca nieprawidłowy wynik. Przy wyłączonym myśleniu i temperaturze ustawionej na 0 większość zapytań jest obsługiwana przez standardową ścieżkę modelu, jednak to właśnie tryb awaryjny utrzymuje agenta przy życiu.
Szczegół 3: pamięć
Pamięć odróżnia agenta, który dostosowuje się do Ciebie, od tego, który traktuje każdą rozmowę jak pierwszą. Uczy się Twoich konwencji, preferencji oraz kontekstu projektu i stosuje je bez dodatkowych instrukcji.
Zwykłe rozwiązania znajdują się wewnątrz okna kontekstowego: plik PREFERENCES.md odczytywany przy uruchamianiu, długi komunikat systemowy lub plik CONTEXT.md. Wszystkie one mają jeden wspólny problem – wewnątrz okna są ograniczone przez kompresję, limity tokenów oraz ponowne uruchamianie sesji. To, czego naprawdę potrzebuje agent, to pamięć poza oknem, przechowywana trwale, dostępna w razie potrzeby na podstawie znaczenia i aktualizowana wraz ze zmianami faktów. Istnieją trzy powszechne podejścia, które służą do zaspokojenia różnych potrzeb.
Podstawowy RAG jest najbardziej rozpowszechniony i najłatwiejszy w użyciu dla początkujących. Dokumenty lub historię rozmów dzieli się na fragmenty, te fragmenty są wstawiane do systemu, a po otrzymaniu zapytania wybierane są najbardziej podobne elementy. Wymaga niewielkiej konfiguracji. Jego wadą jest to, że pamięć zamienia się w zbiór dokumentów, bez rozróżnienia między tym, co warto przechować na dłużej, a zwykłym pytaniem tymczasowym.
Pamięć skończona na sesję, np. ConversationBufferMemory i ConversationSummaryMemory z LangChain, zapewnia spójność w ramach danej sesji. Agent nie zapomni tego, co powiedziałeś pięć wiadomości temu, a streszczenia zapobiegają wypełnianiu się okna informacji. Nie przetrwa jednak restartów, co stanowi główną wadę dla asystenta programistycznego.
Strukturalna pamięć trwała, tak jak w Mem0, traktuje pamięć jako dane pierwszej klasy, a nie surowy tekst. Zamiast wiernych kopii wiadomości model wyodrębnia dyskretne fakty, które są umieszczane i przechowywane w bazie danych wektorowej. Podczas wyszukiwania zapytanie jest przekształcane, a powracają fakty semantycznie podobne, nawet jeśli nowa wiadomość nie używa oryginalnych sformułowań. Przechowywane fakty pozostają aktywne między sesjami, mogą być łączone w przypadku duplikacji oraz przepisywane, gdy zmienia się rzeczywistość. Mem0 obsługuje lokalne modele, więc łatwo jest go połączyć z stackiem opartym na Ollama.
Weźmy programistę, który wspomina, że lubi pytest. Zwykły system RAG przechowuje surowe zdanie. System strukturalny wyodrębnia i przechowuje samą preferencję. Następnym razem, gdy poprosisz o funkcję z testami, pamięć zwróci informację „użytkownik woli pytest”, nawet jeśli w wiadomości nigdy nie został wymieniony żaden framework do testowania.
Podłączenie Mem0 do lokalnej instalacji Ollamy wymaga krótkiej konfiguracji wskazującej na lokalny model, lokalny model embeddingów oraz magazyn wektorów:
from mem0 import Memory
config = {
"llm": {
"provider": "ollama",
"config": {"model": "qwen3:8b", "ollama_base_url": "http://localhost:11434"},
},
"embedder": {
"provider": "ollama",
"config": {"model": "nomic-embed-text", "ollama_base_url": "http://localhost:11434"},
},
"vector_store": {
"provider": "qdrant",
"config": {"host": "localhost", "port": 6333, "embedding_model_dims": 768},
},
}
memory = Memory.from_config(config)
memory.add("I always use type hints and pytest", user_id="dev")
results = memory.search("write a utility function", user_id="dev")
Blok llm kieruje Mem0 do modelu qwen3:8b w celu wydobywania faktów, przekształcając surową wiadomość w oddzielne, zapisane preferencje zamiast na wersję dosłowną tekstu. Blok embedder wykorzystuje funkcję nomic-embed-text, aby wektorować te fakty podczas zapisu oraz wgrzewać zapytania podczas wyszukiwania, a następnie memory.search() odnajduje informacje na podstawie znaczenia, a nie słów kluczowych.
Niedocenionym ulepszeniem w każdym podejściu opartym na pamięci jest filtrowanie tego, co trafia do przechowywania przed jego zapisaniem. Lekki klasyfikator działający przed każdym zapisem utrzymuje pamięć w czystości i zapewnia wysoką jakość wyszukiwania:
def _is_worth_storing(self, user_message: str) -> bool:
response = ollama.chat(
model=OLLAMA_CHAT_MODEL,
messages=[{"role": "user", "content": SMART_MEMORY_PROMPT.format(
user_message=user_message
)}],
options={"temperature": 0, "num_predict": 512},
)
data = self._extract_json_robust(response["message"]["content"])
return bool(data.get("worth_storing", False))
Jego instrukcja dostarcza modelowi jasnych przykładów tego rozróżnienia:
HIGH-VALUE (worth storing):
- "I prefer TypeScript over JavaScript"
- "I use pytest, never unittest"
- "Always use Google-style docstrings"
LOW-VALUE (discard):
- "What does enumerate() do?"
- "Write a retry decorator"
- "Thanks"
Jeśli pominiesz filtr, baza wektorowa powoli wypełnia się zapytaniami jednorazowymi, które pogarszają jakość wyszukiwań.
Szczegół 4: przechowywanie
Szczegół pamięci decyduje, co należy zapamiętać, a przechowywanie to miejsce, w którym dane są zapisywane na dysku.
FAISS to biblioteka działająca wewnątrz procesu: szybka, dobrze przetestowana i nie wymagająca żadnych zależności infrastrukturalnych. Może być włączona do procesu Pythona i działać w pamięci lub zostać zapisana na dysku. Jej ograniczenia dotyczą funkcjonowania – utrwalenie danych wymaga wyraźnych operacji serializacji, a brakuje API HTTP czy wbudowanej funkcji replikacji. To nadaje się do pipeline’ów badawczych i zadań batchowych, ale jest niewystarczające dla długotrwałego asystenta, który często się restartuje.
Chroma to najprostsza baza danych wektorowych, jaką może wybrać programista. Działa jako lokalny proces w Pythonie lub jest wbudowywana w własny kod, a dane są automatycznie zapisywane do lokalnej katalogu. Zainstaluj ją za pomocą:
pip install chromadb
Tysiące przechowywanych faktów w osobistym asyście doskonale mieści się w strefie komfortu Chromy. Całkowicie eliminuje ona zależność od infrastruktury, ale kosztem filtrowania, które jest bardziej ograniczone niż w Qdrant.
Qdrant działa jako pełnoprawna usługa z kompletnymi interfejsami REST i gRPC, filtrowaniem treści oraz wektorami o nazwach. Jest przeznaczony do zastosowań z milionami wektorów i jednoczesnymi zapytaniami. Natywnie zapisuje dane na dysku i przetrwa restarty Dockera bez konieczności specjalnego działania:
# Qdrant via Docker
docker run -d - name qdrant-local -p 6333:6333 \
-v $(pwd)/qdrant_storage:/qdrant/storage qdrant/qdrant
Jeden szczegół konfiguracji potrafi cicho zepsuć wszystko: wymiar embeddingu w konfiguracji magazynu wektorów musi dokładnie odpowiadać wymiarowi wyjściowemu Twojego modelu embeddingu. Na przykład model nomic-embed-text generuje wektory o wymiarze 768. Jeśli zmienisz model embeddingu, nie aktualizując tej wartości, operacje wstawiania zawiodą bez widocznego błędu.
"vector_store": {
"provider": "qdrant",
"config": {
"collection_name": "coding_assistant",
"host": "localhost",
"port": 6333,
"embedding_model_dims": 768, # must match your embedding model exactly
},
}
Chroma to prosta alternatywa, jeśli chcesz zrezygnować z Dockera: wystarczy zmienić dostawcę vector_store w konfiguracji, a reszta stacku pozostaje bez zmian.
Warstwa 5: interfejs
Najzewnętrzniejsza warstwa określa sposób komunikacji z agentem, a właściwy wybór zależy od jego pozycji w Twoim procesie pracy.
- Web UI. Najbardziej elastyczna opcja. Działa w przeglądarce, renderuje markdown, kod z podświetleniem oraz sekcje, które można składać, i funkcjonuje na każdym systemie operacyjnym bez konieczności instalacji. Asystent programistyczny może pokazać aktualny plik, wygenerowany kod oraz stan pamięci agenta na jednym ekranie.
Jak te opcje się łączą
Każda warstwa ogranicza swoje sąsiednie warstwy. Model wybiera format API, który jest używany przez framework, podejście do pamięci decyduje o tym, jakie rozwiązania przechowywania są potrzebne, a interfejs określa, jak ważne jest formatowanie wyników.
Dla lokalnego asystenta programistycznego jednego dewelopera ta struktura minimalizuje trudności, zachowując jednocześnie możliwość zastąpienia każdej warstwy:
Interface: Web UI on localhost
│
Framework: OpenClaw - tool-dispatch, SKILL.md routing
│
LLM: Ollama for qwen3:8b
(think mode disabled for JSON reliability)
│
Memory: Structured persistent memory with value filtering
Embeddings via nomic-embed-text
│
Storage: Chroma (zero infrastructure) or Qdrant (more robust)
Ponieważ każdą warstwę można wymieniać niezależnie, zacznij od najprostszej opcji na każdym poziomie i ulepszaj ją w miarę rosnących potrzeb. Przejście z Chromy na Qdrant lub z Ollamy na vLLM nie wymaga przebudowy niczego powyżej lub poniżej niej. Jeśli jedna warstwa zasługuje na wcześniejszą inwestycję, to powinna to być pamięć. Skromny model lokalny z dobrze zaprojektowaną warstwą pamięci trwałej niezawodnie przewyższa większy model, który przy każdej sesji musi wszystko budować od zera.