Startseite / Artikel

Aufbau eines lokalen KI-Agenten: Modelle, Speicher, Lagerung und Schnittstelle

Die fünf Schichten eines lokalen AI-Agent-Stacks, wobei Ollama, OpenClaw, Mem0 sowie Qdrant oder Chroma verglichen werden, zusammen mit Korrekturen für JSON im Denkmodus und stille Dimensionenunterschiede.

Verlag: REACTAPP.TOP10 Min. Lesezeit2248 Wörter

Quellen: Building Local AI Agents: A Practical Guide to Models, Memory, and Orchestration

Ein lokaler KI-Agent hält das Modell auf Ihrer eigenen Hardware, handelt im Auftrag des Benutzers und überträgt den Kontext von einer Sitzung zur nächsten, ohne Daten an eine externe API zu senden. Ein einfacher Chatbot antwortet und vergisst anschließend. Ein Agent kann mehrere Schritte durchdenken, Tools nutzen und im Laufe der Zeit Wissen ansammeln. Die Lokalverarbeitung bietet Ihnen einen Assistenten ohne die Privatsphäreeinbußen oder Gebühren pro Anruf von gehosteten Diensten.

Fünf Schichten müssen zusammenarbeiten, um einen solchen Agenten zu erstellen: eine LLM-Schicht, die auf Ihrem Gerät Inferenzen durchführt, ein Agent-Framework, das Aktionen leitet und ausführt, eine Speicherschicht, die den Agenten im Laufe der Zeit verbessert, eine Speicherschicht, die gesammelte Informationen aufbewahrt, sowie eine Schnittstellschicht, die sich an Ihre bestehende Arbeitsweise anpasst. Unten finden Sie für jede Schicht ihre Optionen, Vor- und Nachteile sowie eine Empfehlung, damit Sie eine Lösung zusammenstellen können, die zu Ihrer Situation passt.

Schicht 1: das Modell

Das ist der Motor. Das Modell erkennt die Absicht, generiert Code und extrahiert strukturierte Daten, auf die der Rest des Systems angewiesen ist; alles Weitere ist lediglich die Infrastruktur, die um die zuverlässigen Fähigkeiten dieses Motors aufgebaut wird. Drei Tools dominieren die lokale Inferenz.

Ollama ist der einfachste Einstieg. Mit einem Befehl wird es installiert, mit einem weiteren Befehl wird ein Modell heruntergeladen, und sofort steht eine OpenAI-kompatible API auf dem lokalen Rechner zur Verfügung. Es verwaltet Modelle, wählt die Quantisierungsmethode aus und legt die Kontextfenster fest – ohne weitere Konfiguration. Es ist nicht für Produktivumgebungen konzipiert und eignet sich eher für persönliche Assistenten oder Entwicklerwerkzeuge.

ollama pull qwen3:8b
ollama pull nomic-embed-text

llama.cpp steht auf einer niedrigeren Ebene. Es handelt sich um einen C++-Motor, der quantisierte GGUF-Modelle direkt ausführt, ohne Daemon und ohne HTTP-Schicht. Sein geringer Speicherverbrauch macht ihn besonders geeignet für Edge-Geräte wie Raspberry Pi oder eingebettetes Linux, wo Ollama zu viel Ressourcen verbrauchen würde. Der Nachteil ist jedoch das Fehlen einer integrierten Modellverwaltung sowie eine aufwändigere manuelle Einrichtung.

vLLM ist die Produktionslösung. Es nutzt PagedAttention sowie kontinuierliches Batching – Techniken, die hinter der großskaligen Cloud-Inferenz stehen. Wenn Sie viele Benutzer bedienen oder hohe Durchsatzanforderungen haben, ist dies die richtige Wahl, allerdings erfordert es eine geeignete GPU-Infrastruktur und skaliert besser als die anderen beiden Lösungen.

Für den lokalen Assistenten eines einzelnen Entwicklers ist Ollama die sinnvolle Standardwahl, denn Einfachheit ist wichtiger als Durchsatz. Das Modell qwen3:8b verdient eine Erwähnung: Es liefert zuverlässige JSON-Daten für die Absichtserkennung, schreibt sauberen Code und passt in 8 GB VRAM. Eine Besonderheit, die man kennen sollte, ist, dass es seine Antworten vor dem eigentlichen Inhalt in <think>…</think>-Blöcke einbettet, was jeden nachfolgenden JSON-Parser, der mit der Rohantwort arbeitet, stört. Ein kleiner Wrapper, der überall think: False setzt, behebt dieses 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

Weil es setdefault verwendet, können einzelne Aufrufer den Denkmodus jederzeit wieder aktivieren, wenn sie es wünschen. Für die Extraktion von JSON-Daten und die Codegenerierung in einem Tool-Pipeline fügt der Denkmodus Verzögerungen hinzu, ohne die Qualität zu verbessern.

Ebene 2: Das Agentenframework

Das Modell generiert Text. Das Framework entscheidet, was mit diesem Text gemacht werden soll und welcher nächste Schritt unternommen wird – dabei umfasst es die Intent-Klassifizierung, das Routing sowie die Ausführung.

OpenClaw ist auf Tools ausgerichtet und arbeitet auf Dateisystemebene. Jede Aktion ist ein Befehl, der an eine deklarierte Fähigkeit gesendet wird – beispielsweise das Erstellen einer Datei, das Schreiben von Code, das Öffnen eines Verzeichnisses oder das Ausführen eines Shell-Befehls. Man schreibt ein SKILL.md, in dem der Name der Fähigkeit sowie die Art ihrer Ausführung deklariert werden, und das Framework übernimmt das Routing:

- -
name: local-ai-assistant
description: Local AI coding assistant with persistent memory
command-dispatch: tool
command-tool: exec
command-arg-mode: raw
-

Da bei den meisten Operationen Datei-Eingabe/Ausgabe vorliegt, wie es beispielsweise bei einem Programmierassistenten der Fall ist, passt dieses Modell ideal dazu. Die Zeilen command-dispatch: tool und command-tool: exec weisen OpenClaw an, bei Aufruf einer Fähigkeit auf Python-Skripte zurückzugreifen, sodass die Dateiarbeit über das Framework abgewickelt wird. Seine Grenze besteht darin, dass es nicht für lange, mehrstufige Schlussfolgerungsketten oder für die Koordination mehrerer Agenten konzipiert ist.

LangGraph verfolgt den gegenteiligen Ansatz. Man definiert ein explizites Zustandsdiagramm, bei dem die Knoten Modellaufrufe oder Aktionen darstellen, die Kanten Übergänge sind und das Framework den Zustand zwischen den Schritten überträgt. Der Preis dafür ist eine vorab durchzuführende Konzeption: Man muss das Zustandsschema sowie die Form des Diagramms bereits vor der Implementierung der Geschäftslogik festlegen. Für einfache Tool-Nutzungen ist das überdimensioniert, doch bei komplexen, mehrstufigen Workflows liefert es eine nachvollziehbare Aufzeichnung dessen, was in jedem Schritt geschehen ist.

CrewAI geht noch einen Schritt weiter. Sie beschreiben Agenten mit Rollen und Aufgaben sowie dazugehörigen Beschreibungen, wobei das Framework die Ausführungsreihenfolge sowie die Kommunikation zwischen den Agenten festlegt. Es ist der schnellste Weg zu einem funktionsfähigen Mehr-Agenten-Prototyp, doch die Abstraktion erschwert das Debuggen von Fehlern.

Egal, welche Methode Sie wählen – bei jeder eingehenden Nachricht wird zunächst eine Absichtsklassifizierung durchgeführt. Die Kombination aus modellbasiertem Klassifizieren und einer Regex-Notlösung bewältigt Fehler besser als jede Methode für sich allein:

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)

Der Fallback fängt gängige Muster ab, wenn das Modell fehlerhaftes Ausgabeformat zurückgibt. Bei deaktiviertem Denken und einer Temperatur von 0 werden die meisten Anfragen über den Modellpfad bearbeitet, doch es ist der Fallback, der den Agenten am Laufen hält.

Schicht 3: Speicher

Der Speicher unterscheidet einen Agenten, der sich an Sie anpasst, von einem, der jede Konversation als die erste betrachtet. Er lernt Ihre Gewohnheiten, Präferenzen und den Projektkontext und wendet sie ohne Aufforderung an.

Die üblichen Workarounds befinden sich innerhalb des Kontextfensters: ein PREFERENCES.md, das beim Start gelesen wird, eine ausführliche Systemanfrage oder eine CONTEXT.md-Datei. Sie weisen alle denselben Nachteil auf – innerhalb des Fensters werden sie durch Komprimierung, Tokenbegrenzungen und Neustarts der Sitzung eingeengt. Was ein Agent tatsächlich benötigt, ist Speicher außerhalb des Fensters, der dauerhaft gespeichert wird, bei Bedarf nach Bedeutung abgerufen werden kann und sich mit ändernden Fakten aktualisiert. Drei gängige Ansätze decken unterschiedliche Anforderungen ab.

Basis-RAG ist am weitesten verbreitet und am einfachsten für Anfänger. Dokumente oder Konversationshistorien werden in Blöcke aufgeteilt, diese eingebettet und bei einer Anfrage die k-ähnlichsten Blöcke abgerufen – es ist kaum eine Einrichtung nötig. Seine Schwäche liegt darin, dass der Speicher zu einem Haufen von Dokumenten wird, ohne Unterscheidung zwischen Inhalten, die langfristig aufbewahrt werden sollten, und vorübergehenden Anfragen.

Sitzungsspezifisches Speicher, wie zum Beispiel LangChains ConversationBufferMemory und ConversationSummaryMemory, sorgt für Kohärenz innerhalb einer Sitzung. Der Agent vergisst nicht, was Sie vor fünf Nachrichten gesagt haben, und Zusammenfassungen verhindern, dass das Speicherfenster überläuft. Es überdauert jedoch keine Neustarts, was der wesentliche Mangel bei einem Programmierassistenten darstellt.

Gestützte persistente Speicher, wie bei Mem0, behandeln den Speicher als erstklassige Daten und nicht als rohen Text. Anstelle von wörtlichen Nachrichten extrahiert das Modell diskrete Fakten, die eingebettet und in einer Vektordatenbank gespeichert werden. Bei der Abfrage wird die Anfrage selbst eingebettet, sodass semantisch ähnliche Fakten zurückgegeben werden – auch wenn die neue Nachricht nicht denselben Wortlaut verwendet. Gespeicherte Fakten bleiben über Sessions hinweg bestehen, können bei Duplikaten zusammengeführt werden und werden neu geschrieben, wenn sich die Realität ändert. Mem0 unterstützt lokale Modelle, wodurch eine Verbindung zu einer auf Ollama basierenden Architektur einfach möglich ist.

Nehmen wir einen Entwickler, der erwähnt, dass er pytest mag. Ein einfaches RAG-System behält den rohen Satz bei. Ein gestütztes System extrahiert und speichert hingegen die Präferenz selbst. Beim nächsten Mal, wenn Sie nach einer Funktion mit Tests fragen, gibt der Speicher „Der Benutzer bevorzugt pytest“ zurück – auch wenn in der Nachricht nie ein Testframework genannt wird.

Die Einbindung von Mem0 in eine lokale Ollama-Installation erfordert eine kurze Konfiguration, die auf das lokale Modell, das lokale Embedding-Modell sowie einen Vektorlagertyp verweist:

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")

Der llm-Block leitet Mem0 an qwen3:8b zur Faktenerfassung weiter, wodurch eine Rohnachricht in diskrete, gespeicherte Präferenzen umgewandelt wird anstelle von wörtlichem Text. Der embedder-Block verwendet nomic-embed-text, um diese Fakten bei der Speicherung zu vektorisieren und Abfragen bei der Suche einzubetten, wobei memory.search() anschließend nach Bedeutung und nicht nach Schlüsselwörtern sucht.

Eine unterschätzte Verbesserung für jeden Speichermethodenansatz ist das Filtern dessen, was vor der Speicherung aufgenommen wird. Ein leichtgewichtiger Klassifikator, der vor jeder Speicherung ausgeführt wird, hält den Speicher sauber und gewährleistet eine hohe Suchqualität:

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))

Ihre Anweisung liefert dem Modell klare Beispiele für den Unterschied:

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"

Wenn man den Filter überspringt, füllt sich die Vektordatenbank allmählich mit einmaligen Anfragen, wodurch die Suchleistung nachlässt.

Schicht 4: Speicherung

Die Speicherschicht entscheidet, was gespeichert werden soll, und der Speicher ist der Ort, an dem die Daten auf der Festplatte abgelegt werden.

FAISS ist eine Bibliothek, die im selben Prozess läuft: schnell, gut getestet und unabhängig von Infrastruktur. Sie kann in Ihren Python-Prozess eingebettet und im Speicher ausgeführt werden oder serialisiert auf die Festplatte gespeichert werden. Ihre Einschränkungen liegen im Betriebsbereich: Die Persistenz erfordert explizite Serialisierungsaufrufe, es gibt keine HTTP-API und keine eingebauten Replikationsfunktionen. Das eignet sich für Forschungspipelines und Batch-Arbeitslasten, ist aber nicht geeignet für einen langlaufenden Assistenten, der häufig neu gestartet wird.

Chroma ist die einfachste Vektordatenbank, die ein Entwickler wählen kann. Sie startet als lokaler Python-Prozess oder wird in Ihrem eigenen Code eingebettet ausgeführt und speichert die Daten automatisch in einem lokalen Verzeichnis. Installieren Sie sie mit:

pip install chromadb

Tausende gespeicherter Fakten in einem persönlichen Assistenten liegen voll und ganz im Komfortbereich von Chroma. Es beseitigt die Abhängigkeit von Infrastruktur völlig – auf Kosten einer begrenzteren Filterfunktion im Vergleich zu Qdrant.

Qdrant wird als echter Dienst mit vollständigen REST- und gRPC-APIs, Payload-Filterung sowie benannten Vektoren bereitgestellt. Es ist für Umgebungen mit Millionen von Vektoren und gleichzeitigen Abfragen konzipiert. Es speichert Daten nativ auf der Festplatte und überlebt Docker-Neustarts ohne besondere Handhabung:

# Qdrant via Docker
docker run -d - name qdrant-local -p 6333:6333 \
-v $(pwd)/qdrant_storage:/qdrant/storage qdrant/qdrant

Ein Konfigurationsdetail führt stillschweigend zu Problemen: Die Eingabedimension in der Konfiguration des Vektor-Speichers muss genau mit der Ausgabedimension Ihres Embedding-Modells übereinstimmen. Das nomic-embed-text-Modell erzeugt beispielsweise 768-dimensionalen Vektoren. Ändern Sie das Embedding-Modell, ohne diesen Wert zu aktualisieren, führt dies dazu, dass die Einfügeoperationen ohne sichtbaren Fehler fehlschlagen.

"vector_store": {
    "provider": "qdrant",
    "config": {
        "collection_name": "coding_assistant",
        "host": "localhost",
        "port": 6333,
        "embedding_model_dims": 768,  # must match your embedding model exactly
    },
}

Chroma ist ein direkter Ersatz, wenn Sie Docker vermeiden möchten: Ändern Sie einfach den vector_store-Anbieter in der Konfiguration, der Rest des Stack bleibt unverändert.

Schicht 5: die Schnittstelle

Die äußerste Schicht bestimmt, wie Sie mit dem Agenten kommunizieren, und die richtige Wahl hängt davon ab, wo er in Ihrem Workflow platziert ist.

  • Weboberfläche. Die flexibelste Option. Sie läuft im Browser, zeigt Markdown an, färbt Code hervor und bietet zusammenfaltbare Abschnitte; außerdem funktioniert sie auf jedem Betriebssystem ohne Installation. Ein Programmierassistent kann den aktuellen Dateiinhalt, den generierten Code sowie den Speichinzustand des Agenten auf einem Bildschirm anzeigen.
  • CLI. Ein Terminal-Assistent integriert sich in Shell-Arbeitsabläufe, leitet Ausgaben an andere Tools weiter und verursacht keine visuellen Überlastungen. Umfangreiche Ausgaben erfordern besondere Sorgfalt, und mehrstufige Gespräche sind unhandlicher als in einer Chat-Window. Er eignet sich am besten für schnelle Abfragen, Dateioperationen und skriptbasierte Aufgaben.
  • IDE-Plugin. Ein Assistent innerhalb von VS Code oder JetBrains befindet sich neben der bearbeiteten Datei, weiß, welche Datei geöffnet ist, und schlägt Änderungen direkt ein. Man muss die APIs der Erweiterung lernen und mit den Updates des IDEs Schritt halten, was für ein täglich genutztes Team-Tool lohnenswert sein kann.
  • Chat-Apps wie Slack, Discord oder Telegram. Diese sind sinnvoll, wenn der Assistent von vielen Geräten aus erreichbar sein soll oder innerhalb eines Teams geteilt werden muss. Sie eignen sich nicht für Code-Reviews oder Dateibearbeitung, sondern eher für Fragen und Antworten, Statusüberprüfungen sowie leichte Anfragen.
  • Wie die Optionen zusammenpassen

    Jede Schicht beschränkt ihre Nachbarn. Das Modell wählt die von dem Framework aufgerufene API-Struktur, der Speichermanagement-Ansatz bestimmt, welche Speicherlösungen erforderlich sind, und die Schnittstelle legt fest, wie wichtig die Ausgabeformatierung ist.

    Für einen lokalen Programmierassistenten eines einzelnen Entwicklers minimiert dieser Stack Reibungsverluste, während jede Schicht austauschbar bleibt:

    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)
    

    Da jede Schicht unabhängig ausgetauscht werden kann, sollte man auf jeder Ebene mit der einfachsten Option beginnen und diese bei wachsenden Anforderungen aufrüsten. Der Wechsel von Chroma zu Qdrant oder von Ollama zu vLLM erfordert nicht, dass etwas darüber oder darunter neu aufgebaut wird. Wenn eine Schicht eine frühzeitige Investition rechtfertigt, dann ist es die Speicherschicht. Ein bescheidener lokaler Modell mit einer gut konzipierten persistenten Speicherschicht übertrifft zuverlässig ein größeres Modell, das bei jedem neuen Sitzung start von vorne beginnt.