Memory
Das OpenClaw-Gedaechtnis besteht aus purem Markdown im Agent-Workspace. Die Dateien sind die Wahrheitsquelle; das Modell “erinnert” sich nur an das, was auf die Festplatte geschrieben wird.
Memory-Such-Tools werden vom aktiven Memory-Plugin bereitgestellt (Standard:
memory-core). Deaktiviere Memory-Plugins mit plugins.slots.memory = "none".
Memory-Dateien (Markdown)
Das Standard-Workspace-Layout nutzt zwei Gedaechtnis-Schichten:
memory/YYYY-MM-DD.md- Tagesprotokoll (nur anfuegen).
- Lies heute und gestern beim Sitzungsstart.
MEMORY.md(optional)- Kuratiertes Langzeitgedaechtnis.
- Nur in der privaten Hauptsitzung laden (nie in Gruppenkontexten).
Diese Dateien liegen im Workspace (agents.defaults.workspace, Standard
~/.openclaw/workspace). Siehe Agent-Workspace fuer den vollstaendigen Aufbau.
Memory-Tools
OpenClaw stellt dem Agenten zwei Tools fuer diese Markdown-Dateien bereit:
memory_search— semantische Suche ueber indizierte Snippets.memory_get— gezieltes Lesen einer bestimmten Markdown-Datei/Zeilenbereich.
memory_get reagiert jetzt graceful, wenn eine Datei nicht existiert (zum Beispiel
das heutige Tagesprotokoll vor dem ersten Schreiben). Sowohl der eingebaute Manager als auch das QMD-Backend
geben { text: "", path } zurueck, statt ENOENT zu werfen, damit Agenten
“noch nichts aufgezeichnet” handhaben und ihren Workflow fortsetzen koennen, ohne den
Tool-Call in try/catch-Logik zu verpacken.
Wann Memory schreiben
- Entscheidungen, Praeferenzen und dauerhafte Fakten kommen in
MEMORY.md. - Tagesnotizen und laufender Kontext kommen in
memory/YYYY-MM-DD.md. - Wenn jemand sagt “merk dir das”, schreibe es auf (behalte es nicht im RAM).
- Dieser Bereich entwickelt sich noch weiter. Es hilft, das Modell daran zu erinnern, Erinnerungen zu speichern; es weiss dann, was zu tun ist.
- Wenn etwas haften bleiben soll, bitte den Bot, es ins Memory zu schreiben.
Automatischer Memory-Flush (Pre-Compaction-Ping)
Wenn eine Sitzung kurz vor der Auto-Compaction steht, loest OpenClaw einen stillen,
agentischen Turn aus, der das Modell daran erinnert, dauerhaftes Memory zu schreiben, bevor der
Kontext komprimiert wird. Die Standard-Prompts sagen explizit, dass das Modell antworten darf,
aber normalerweise ist NO_REPLY die richtige Antwort, damit der Nutzer diesen Turn nie sieht.
Das wird gesteuert durch agents.defaults.compaction.memoryFlush:
{
agents: {
defaults: {
compaction: {
reserveTokensFloor: 20000,
memoryFlush: {
enabled: true,
softThresholdTokens: 4000,
systemPrompt: "Session nearing compaction. Store durable memories now.",
prompt: "Write any lasting notes to memory/YYYY-MM-DD.md; reply with NO_REPLY if nothing to store.",
},
},
},
},
}
Details:
- Soft Threshold: Der Flush wird ausgeloest, wenn die geschaetzte Token-Anzahl der Sitzung
contextWindow - reserveTokensFloor - softThresholdTokensueberschreitet. - Standardmaessig still: Die Prompts enthalten
NO_REPLY, sodass nichts zugestellt wird. - Zwei Prompts: Ein User-Prompt plus ein System-Prompt haengen die Erinnerung an.
- Ein Flush pro Compaction-Zyklus (getrackt in
sessions.json). - Workspace muss beschreibbar sein: Wenn die Sitzung in einer Sandbox mit
workspaceAccess: "ro"oder"none"laeuft, wird der Flush uebersprungen.
Fuer den vollstaendigen Compaction-Lebenszyklus siehe Sitzungsverwaltung und Compaction.
Vektor-Memory-Suche
OpenClaw kann einen kleinen Vektorindex ueber MEMORY.md und memory/*.md aufbauen, damit
semantische Abfragen verwandte Notizen finden koennen, auch wenn die Formulierung abweicht.
Standardwerte:
- Standardmaessig aktiviert.
- Ueberwacht Memory-Dateien auf Aenderungen (mit Debounce).
- Konfiguriere die Memory-Suche unter
agents.defaults.memorySearch(nicht auf Top-LevelmemorySearch). - Nutzt standardmaessig Remote-Embeddings. Wenn
memorySearch.providernicht gesetzt ist, waehlt OpenClaw automatisch:local, wenn einmemorySearch.local.modelPathkonfiguriert ist und die Datei existiert.openai, wenn ein OpenAI-Schluessel aufgeloest werden kann.gemini, wenn ein Gemini-Schluessel aufgeloest werden kann.voyage, wenn ein Voyage-Schluessel aufgeloest werden kann.mistral, wenn ein Mistral-Schluessel aufgeloest werden kann.- Andernfalls bleibt die Memory-Suche deaktiviert, bis sie konfiguriert wird.
- Der lokale Modus nutzt node-llama-cpp und erfordert moeglicherweise
pnpm approve-builds. - Nutzt sqlite-vec (wenn verfuegbar) zur Beschleunigung der Vektorsuche in SQLite.
memorySearch.provider = "ollama"wird ebenfalls unterstuetzt fuer lokale/selbstgehostete Ollama-Embeddings (/api/embeddings), wird aber nicht automatisch gewaehlt.
Remote-Embeddings erfordern einen API-Schluessel fuer den Embedding-Provider. OpenClaw
loest Schluessel aus Auth-Profilen, models.providers.*.apiKey oder Umgebungsvariablen auf. Codex-OAuth deckt nur Chat/Completions ab und genuegt nicht fuer
Embeddings bei der Memory-Suche. Fuer Gemini verwende GEMINI_API_KEY oder
models.providers.google.apiKey. Fuer Voyage verwende VOYAGE_API_KEY oder
models.providers.voyage.apiKey. Fuer Mistral verwende MISTRAL_API_KEY oder
models.providers.mistral.apiKey. Ollama braucht typischerweise keinen echten API-Schluessel (ein Platzhalter wie OLLAMA_API_KEY=ollama-local reicht, wenn es von der lokalen Policy verlangt wird).
Bei einem benutzerdefinierten OpenAI-kompatiblen Endpunkt
setze memorySearch.remote.apiKey (und optional memorySearch.remote.headers).
QMD-Backend (experimentell)
Setze memory.backend = "qmd", um den eingebauten SQLite-Indexer durch
QMD zu ersetzen: einen lokalen Such-Sidecar, der
BM25 + Vektoren + Reranking kombiniert. Markdown bleibt die Wahrheitsquelle; OpenClaw
nutzt QMD per Shell fuer den Abruf. Wichtige Punkte:
Voraussetzungen
- Standardmaessig deaktiviert. Per Config aktivieren (
memory.backend = "qmd"). - Installiere die QMD-CLI separat (
bun install -g https://github.com/tobi/qmdoder ein Release herunterladen) und stelle sicher, dass dieqmd-Binary imPATHdes Gateways liegt. - QMD braucht einen SQLite-Build, der Extensions erlaubt (
brew install sqliteauf macOS). - QMD laeuft vollstaendig lokal ueber Bun +
node-llama-cppund laedt GGUF-Modelle automatisch von HuggingFace beim ersten Gebrauch herunter (kein separater Ollama-Daemon noetig). - Das Gateway betreibt QMD in einem eigenstaendigen XDG-Home unter
~/.openclaw/agents/<agentId>/qmd/, indem esXDG_CONFIG_HOMEundXDG_CACHE_HOMEsetzt. - OS-Unterstuetzung: macOS und Linux funktionieren sofort nach Installation von Bun + SQLite. Windows wird am besten ueber WSL2 unterstuetzt.
Wie der Sidecar laeuft
- Das Gateway schreibt ein eigenstaendiges QMD-Home unter
~/.openclaw/agents/<agentId>/qmd/(Config + Cache + SQLite-DB). - Collections werden ueber
qmd collection addausmemory.qmd.paths(plus Standard-Workspace-Memory-Dateien) erstellt, dann laufenqmd update+qmd embedbeim Start und in einem konfigurierbaren Intervall (memory.qmd.update.interval, Standard 5 Min.). - Das Gateway initialisiert den QMD-Manager jetzt beim Start, sodass periodische Update-Timer
auch vor dem ersten
memory_search-Aufruf aktiv sind. - Boot-Refresh laeuft jetzt standardmaessig im Hintergrund, damit der Chat-Start nicht
blockiert wird; setze
memory.qmd.update.waitForBootSync = true, um das vorherige blockierende Verhalten beizubehalten. - Suchen laufen ueber
memory.qmd.searchMode(Standardqmd search --json; unterstuetzt auchvsearchundquery). Wenn der gewaehlte Modus Flags auf deinem QMD-Build ablehnt, versucht OpenClaw es erneut mitqmd query. Wenn QMD fehlschlaegt oder die Binary fehlt, faellt OpenClaw automatisch auf den eingebauten SQLite-Manager zurueck, damit die Memory-Tools weiter funktionieren. - OpenClaw bietet heute kein Tuning der QMD-Embed-Batch-Groesse an; das Batch-Verhalten wird von QMD selbst gesteuert.
- Die erste Suche kann langsam sein: QMD laedt moeglicherweise lokale GGUF-Modelle (Reranker/Query
Expansion) beim ersten
qmd query-Durchlauf herunter.-
OpenClaw setzt
XDG_CONFIG_HOME/XDG_CACHE_HOMEautomatisch, wenn es QMD ausfuehrt. -
Wenn du Modelle manuell vorab herunterladen (und denselben Index aufwaermen) willst, den OpenClaw nutzt, fuehre eine einmalige Query mit den XDG-Dirs des Agenten aus.
OpenClaws QMD-State liegt unter deinem State-Dir (Standard
~/.openclaw). Du kannstqmdauf exakt denselben Index zeigen lassen, indem du dieselben XDG-Variablen exportierst, die OpenClaw nutzt:# Dasselbe State-Dir wie OpenClaw verwenden STATE_DIR="${OPENCLAW_STATE_DIR:-$HOME/.openclaw}" export XDG_CONFIG_HOME="$STATE_DIR/agents/main/qmd/xdg-config" export XDG_CACHE_HOME="$STATE_DIR/agents/main/qmd/xdg-cache" # (Optional) Index-Refresh + Embeddings erzwingen qmd update qmd embed # Aufwaermen / erstmaliges Herunterladen der Modelle ausloesen qmd query "test" -c memory-root --json >/dev/null 2>&1
-
Config-Oberflaeche (memory.qmd.*)
command(Standardqmd): ueberschreibt den Pfad zur Executable.searchMode(Standardsearch): waehlt, welcher QMD-Befehl hintermemory_searchsteht (search,vsearch,query).includeDefaultMemory(Standardtrue): indiziert automatischMEMORY.md+memory/**/*.md.paths[]: zusaetzliche Verzeichnisse/Dateien hinzufuegen (path, optionalespattern, optionaler stabilername).sessions: Session-JSONL-Indizierung aktivieren (enabled,retentionDays,exportDir).update: steuert Refresh-Kadenz und Wartungsausfuehrung: (interval,debounceMs,onBoot,waitForBootSync,embedInterval,commandTimeoutMs,updateTimeoutMs,embedTimeoutMs).limits: begrenzt den Recall-Payload (maxResults,maxSnippetChars,maxInjectedChars,timeoutMs).scope: gleiches Schema wiesession.sendPolicy. Standard ist DM-only (denyalle,allowDirektchats); lockere es, um QMD-Treffer in Gruppen/Kanaelen anzuzeigen.match.keyPrefixmatcht den normalisierten Sitzungsschluessel (Kleinbuchstaben, fuehrendesagent:<id>:entfernt). Beispiel:discord:channel:.match.rawKeyPrefixmatcht den rohen Sitzungsschluessel (Kleinbuchstaben), einschliesslichagent:<id>:. Beispiel:agent:main:discord:.- Legacy:
match.keyPrefix: "agent:..."wird weiterhin als Raw-Key-Praefix behandelt, aber bevorzugerawKeyPrefixfuer Klarheit.
- Wenn
scopeeine Suche verweigert, loggt OpenClaw eine Warnung mit dem abgeleitetenchannel/chatType, damit leere Ergebnisse leichter zu debuggen sind. - Snippets von ausserhalb des Workspaces erscheinen als
qmd/<collection>/<relativer-pfad>inmemory_search-Ergebnissen;memory_getversteht diesen Praefix und liest aus dem konfigurierten QMD-Collection-Root. - Wenn
memory.qmd.sessions.enabled = true, exportiert OpenClaw bereinigte Sitzungstranskripte (User/Assistant-Turns) in eine dedizierte QMD-Collection unter~/.openclaw/agents/<id>/qmd/sessions/, damitmemory_searchkuerzliche Konversationen abrufen kann, ohne den eingebauten SQLite-Index zu beruehren. memory_search-Snippets enthalten jetzt einenSource: <pfad#zeile>-Footer, wennmemory.citationsaufauto/onsteht; setzememory.citations = "off", um die Pfad-Metadaten intern zu halten (der Agent empfaengt den Pfad weiterhin fuermemory_get, aber der Snippet-Text laesst den Footer weg und der System-Prompt warnt den Agenten, ihn nicht zu zitieren).
Beispiel
memory: {
backend: "qmd",
citations: "auto",
qmd: {
includeDefaultMemory: true,
update: { interval: "5m", debounceMs: 15000 },
limits: { maxResults: 6, timeoutMs: 4000 },
scope: {
default: "deny",
rules: [
{ action: "allow", match: { chatType: "direct" } },
// Normalisierter Sitzungsschluessel-Praefix (entfernt `agent:<id>:`).
{ action: "deny", match: { keyPrefix: "discord:channel:" } },
// Roher Sitzungsschluessel-Praefix (einschliesslich `agent:<id>:`).
{ action: "deny", match: { rawKeyPrefix: "agent:main:discord:" } },
]
},
paths: [
{ name: "docs", path: "~/notes", pattern: "**/*.md" }
]
}
}
Zitate und Fallback
memory.citationsgilt unabhaengig vom Backend (auto/on/off).- Wenn
qmdlaeuft, setzen wirstatus().backend = "qmd", damit die Diagnose zeigt, welche Engine die Ergebnisse geliefert hat. Wenn der QMD-Subprocess beendet wird oder die JSON-Ausgabe nicht geparst werden kann, loggt der Search-Manager eine Warnung und gibt den eingebauten Provider (bestehende Markdown-Embeddings) zurueck, bis QMD sich erholt.
Zusaetzliche Memory-Pfade
Wenn du Markdown-Dateien ausserhalb des Standard-Workspace-Layouts indizieren willst, fuege explizite Pfade hinzu:
agents: {
defaults: {
memorySearch: {
extraPaths: ["../team-docs", "/srv/shared-notes/overview.md"]
}
}
}
Hinweise:
- Pfade koennen absolut oder workspace-relativ sein.
- Verzeichnisse werden rekursiv nach
.md-Dateien durchsucht. - Standardmaessig werden nur Markdown-Dateien indiziert.
- Wenn
memorySearch.multimodal.enabled = true, indiziert OpenClaw auch unterstuetzte Bild-/Audiodateien unterextraPaths. Standard-Memory-Roots (MEMORY.md,memory.md,memory/**/*.md) bleiben rein Markdown. - Symlinks werden ignoriert (Dateien oder Verzeichnisse).
Multimodale Memory-Dateien (Gemini Bild + Audio)
OpenClaw kann Bild- und Audiodateien aus memorySearch.extraPaths indizieren, wenn Gemini Embedding 2 verwendet wird:
agents: {
defaults: {
memorySearch: {
provider: "gemini",
model: "gemini-embedding-2-preview",
extraPaths: ["assets/reference", "voice-notes"],
multimodal: {
enabled: true,
modalities: ["image", "audio"], // oder ["all"]
maxFileBytes: 10000000
},
remote: {
apiKey: "YOUR_GEMINI_API_KEY"
}
}
}
}
Hinweise:
- Multimodales Memory wird aktuell nur fuer
gemini-embedding-2-previewunterstuetzt. - Multimodale Indizierung gilt nur fuer Dateien, die ueber
memorySearch.extraPathsgefunden werden. - Unterstuetzte Modalitaeten in dieser Phase: Bild und Audio.
memorySearch.fallbackmuss auf"none"bleiben, solange multimodales Memory aktiviert ist.- Passende Bild-/Audio-Datei-Bytes werden beim Indizieren zum konfigurierten Gemini-Embedding-Endpunkt hochgeladen.
- Unterstuetzte Bildformate:
.jpg,.jpeg,.png,.webp,.gif,.heic,.heif. - Unterstuetzte Audioformate:
.mp3,.wav,.ogg,.opus,.m4a,.aac,.flac. - Suchanfragen bleiben Text, aber Gemini kann diese Textanfragen mit indizierten Bild-/Audio-Embeddings vergleichen.
memory_getliest weiterhin nur Markdown; Binaerdateien sind durchsuchbar, aber werden nicht als Rohdatei-Inhalte zurueckgegeben.
Gemini-Embeddings (nativ)
Setze den Provider auf gemini, um die Gemini-Embeddings-API direkt zu nutzen:
agents: {
defaults: {
memorySearch: {
provider: "gemini",
model: "gemini-embedding-001",
remote: {
apiKey: "YOUR_GEMINI_API_KEY"
}
}
}
}
Hinweise:
remote.baseUrlist optional (Standard ist die Gemini-API-Basis-URL).remote.headerserlaubt zusaetzliche Header bei Bedarf.- Standard-Modell:
gemini-embedding-001. gemini-embedding-2-previewwird ebenfalls unterstuetzt: 8192 Token-Limit und konfigurierbare Dimensionen (768 / 1536 / 3072, Standard 3072).
Gemini Embedding 2 (Preview)
agents: {
defaults: {
memorySearch: {
provider: "gemini",
model: "gemini-embedding-2-preview",
outputDimensionality: 3072, // optional: 768, 1536 oder 3072 (Standard)
remote: {
apiKey: "YOUR_GEMINI_API_KEY"
}
}
}
}
Achtung — Neuindizierung erforderlich: Der Wechsel von
gemini-embedding-001(768 Dimensionen) zugemini-embedding-2-preview(3072 Dimensionen) aendert die Vektorgroesse. Das gilt auch, wenn duoutputDimensionalityzwischen 768, 1536 und 3072 aenderst. OpenClaw indiziert automatisch neu, wenn eine Modell- oder Dimensionsaenderung erkannt wird.
Wenn du einen benutzerdefinierten OpenAI-kompatiblen Endpunkt nutzen willst (OpenRouter, vLLM oder einen Proxy),
kannst du die remote-Konfiguration mit dem OpenAI-Provider verwenden:
agents: {
defaults: {
memorySearch: {
provider: "openai",
model: "text-embedding-3-small",
remote: {
baseUrl: "https://api.example.com/v1/",
apiKey: "YOUR_OPENAI_COMPAT_API_KEY",
headers: { "X-Custom-Header": "value" }
}
}
}
}
Wenn du keinen API-Schluessel setzen willst, verwende memorySearch.provider = "local" oder setze
memorySearch.fallback = "none".
Fallbacks:
memorySearch.fallbackkannopenai,gemini,voyage,mistral,ollama,localodernonesein.- Der Fallback-Provider wird nur genutzt, wenn der primaere Embedding-Provider fehlschlaegt.
Batch-Indizierung (OpenAI + Gemini + Voyage):
- Standardmaessig deaktiviert. Setze
agents.defaults.memorySearch.remote.batch.enabled = truezum Aktivieren fuer grosse Korpora (OpenAI, Gemini und Voyage). - Das Standardverhalten wartet auf den Batch-Abschluss; passe
remote.batch.wait,remote.batch.pollIntervalMsundremote.batch.timeoutMinutesnach Bedarf an. - Setze
remote.batch.concurrency, um zu steuern, wie viele Batch-Jobs parallel eingereicht werden (Standard: 2). - Der Batch-Modus greift, wenn
memorySearch.provider = "openai"oder"gemini"und nutzt den entsprechenden API-Schluessel. - Gemini-Batch-Jobs nutzen den asynchronen Embeddings-Batch-Endpunkt und erfordern Gemini-Batch-API-Verfuegbarkeit.
Warum OpenAI-Batch schnell und guenstig ist:
- Fuer grosse Backfills ist OpenAI typischerweise die schnellste Option, die wir unterstuetzen, weil wir viele Embedding-Requests in einem einzigen Batch-Job einreichen und OpenAI sie asynchron verarbeiten lassen koennen.
- OpenAI bietet vergueenstigte Preise fuer Batch-API-Workloads, sodass grosse Indizierungslaeufe normalerweise guenstiger sind als das synchrone Senden derselben Requests.
- Siehe die OpenAI-Batch-API-Dokumentation und Preise fuer Details:
Config-Beispiel:
agents: {
defaults: {
memorySearch: {
provider: "openai",
model: "text-embedding-3-small",
fallback: "openai",
remote: {
batch: { enabled: true, concurrency: 2 }
},
sync: { watch: true }
}
}
}
Tools:
memory_search— gibt Snippets mit Datei- und Zeilenbereichen zurueck.memory_get— liest Memory-Datei-Inhalt nach Pfad.
Lokaler Modus:
- Setze
agents.defaults.memorySearch.provider = "local". - Gib
agents.defaults.memorySearch.local.modelPathan (GGUF oderhf:-URI). - Optional: Setze
agents.defaults.memorySearch.fallback = "none", um Remote-Fallback zu vermeiden.
Wie die Memory-Tools funktionieren
memory_searchdurchsucht semantisch Markdown-Chunks (~400 Token Ziel, 80-Token Ueberlappung) ausMEMORY.md+memory/**/*.md. Es gibt Snippet-Text (begrenzt auf ~700 Zeichen), Dateipfad, Zeilenbereich, Score, Provider/Modell und ob von local auf Remote-Embeddings zurueckgegriffen wurde, zurueck. Kein vollstaendiger Datei-Payload wird zurueckgegeben.memory_getliest eine bestimmte Memory-Markdown-Datei (workspace-relativ), optional ab einer Startzeile und fuer N Zeilen. Pfade ausserhalb vonMEMORY.md/memory/werden abgelehnt.- Beide Tools sind nur aktiviert, wenn
memorySearch.enabledfuer den Agenten true ergibt.
Was indiziert wird (und wann)
- Dateityp: nur Markdown (
MEMORY.md,memory/**/*.md). - Index-Speicher: pro Agent SQLite unter
~/.openclaw/memory/<agentId>.sqlite(konfigurierbar ueberagents.defaults.memorySearch.store.path, unterstuetzt{agentId}-Token). - Aktualitaet: Watcher auf
MEMORY.md+memory/markiert den Index als dirty (Debounce 1,5s). Sync wird beim Sitzungsstart, bei der Suche oder in einem Intervall geplant und laeuft asynchron. Sitzungstranskripte nutzen Delta-Schwellenwerte, um die Hintergrund-Synchronisation auszuloesen. - Neuindizierungs-Trigger: Der Index speichert den Embedding-Provider/Modell- + Endpunkt-Fingerprint + Chunking-Parameter. Wenn sich einer davon aendert, setzt OpenClaw den gesamten Store automatisch zurueck und indiziert neu.
Hybridsuche (BM25 + Vektor)
Wenn aktiviert, kombiniert OpenClaw:
- Vektor-Aehnlichkeit (semantischer Abgleich, Formulierung kann abweichen)
- BM25-Keyword-Relevanz (exakte Tokens wie IDs, Env-Vars, Code-Symbole)
Wenn Volltextsuche auf deiner Plattform nicht verfuegbar ist, faellt OpenClaw auf reine Vektorsuche zurueck.
Warum Hybrid?
Vektorsuche ist grossartig bei “das bedeutet dasselbe”:
- “Mac Studio Gateway Host” vs. “die Maschine, auf der das Gateway laeuft”
- “Dateiaktualisierungen entprellen” vs. “nicht bei jedem Schreiben indizieren”
Aber sie kann schwach sein bei exakten, hochsignifikanten Tokens:
- IDs (
a828e60,b3b9895a...) - Code-Symbole (
memorySearch.query.hybrid) - Fehlerstrings (“sqlite-vec unavailable”)
BM25 (Volltext) ist das Gegenteil: stark bei exakten Tokens, schwaecher bei Umschreibungen. Hybridsuche ist der pragmatische Mittelweg: beide Retrieval-Signale nutzen, damit du gute Ergebnisse sowohl fuer “natuerlichsprachige” Anfragen als auch fuer “Nadel im Heuhaufen”-Anfragen bekommst.
Wie wir Ergebnisse zusammenfuehren (aktuelles Design)
Implementierungsskizze:
- Einen Kandidatenpool von beiden Seiten abrufen:
- Vektor: Top
maxResults * candidateMultipliernach Kosinus-Aehnlichkeit. - BM25: Top
maxResults * candidateMultipliernach FTS5-BM25-Rang (niedriger ist besser).
- BM25-Rang in einen 0..1-Score umrechnen:
textScore = 1 / (1 + max(0, bm25Rank))
- Kandidaten nach Chunk-ID vereinigen und einen gewichteten Score berechnen:
finalScore = vectorWeight * vectorScore + textWeight * textScore
Hinweise:
vectorWeight+textWeightwird in der Config-Aufloesung auf 1.0 normalisiert, sodass Gewichte als Prozente funktionieren.- Wenn Embeddings nicht verfuegbar sind (oder der Provider einen Null-Vektor liefert), fuehren wir trotzdem BM25 aus und geben Keyword-Treffer zurueck.
- Wenn FTS5 nicht erstellt werden kann, behalten wir reine Vektorsuche bei (kein harter Fehler).
Das ist nicht “IR-theoretisch perfekt”, aber es ist einfach, schnell und verbessert tendenziell Recall/Precision bei echten Notizen. Wenn wir spaeter anspruchsvoller werden wollen, sind gaengige naechste Schritte Reciprocal Rank Fusion (RRF) oder Score-Normalisierung (Min/Max oder z-Score) vor dem Mischen.
Nachbearbeitungs-Pipeline
Nach dem Zusammenfuehren von Vektor- und Keyword-Scores verfeinern zwei optionale Nachbearbeitungsstufen die Ergebnisliste, bevor sie den Agenten erreicht:
Vektor + Keyword -> Gewichtetes Merging -> Temporal Decay -> Sortierung -> MMR -> Top-K-Ergebnisse
Beide Stufen sind standardmaessig deaktiviert und koennen unabhaengig voneinander aktiviert werden.
MMR-Re-Ranking (Diversitaet)
Wenn die Hybridsuche Ergebnisse liefert, koennen mehrere Chunks aehnlichen oder ueberlappenden Inhalt enthalten. Zum Beispiel koennte die Suche nach “Heimnetzwerk-Setup” fuenf nahezu identische Snippets aus verschiedenen Tagesnotizen liefern, die alle dieselbe Router-Konfiguration erwaehnen.
MMR (Maximal Marginal Relevance) rankt die Ergebnisse neu, um Relevanz mit Diversitaet auszubalancieren, und stellt sicher, dass die Top-Ergebnisse verschiedene Aspekte der Anfrage abdecken, statt dieselben Informationen zu wiederholen.
Funktionsweise:
- Ergebnisse werden nach ihrem urspruenglichen Relevanz-Score bewertet (Vektor + BM25 gewichteter Score).
- MMR waehlt iterativ Ergebnisse, die maximieren:
lambda x Relevanz - (1-lambda) x max_Aehnlichkeit_zu_Ausgewaehlten. - Die Aehnlichkeit zwischen Ergebnissen wird mittels Jaccard-Text-Aehnlichkeit auf tokenisiertem Inhalt gemessen.
Der lambda-Parameter steuert den Kompromiss:
lambda = 1.0-> reine Relevanz (keine Diversitaetsstrafe)lambda = 0.0-> maximale Diversitaet (ignoriert Relevanz)- Standard:
0.7(ausgewogen, leichte Relevanz-Tendenz)
Beispiel — Anfrage: “home network setup”
Gegeben diese Memory-Dateien:
memory/2026-02-10.md -> "Configured Omada router, set VLAN 10 for IoT devices"
memory/2026-02-08.md -> "Configured Omada router, moved IoT to VLAN 10"
memory/2026-02-05.md -> "Set up AdGuard DNS on 192.168.10.2"
memory/network.md -> "Router: Omada ER605, AdGuard: 192.168.10.2, VLAN 10: IoT"
Ohne MMR — Top 3 Ergebnisse:
1. memory/2026-02-10.md (score: 0.92) <- Router + VLAN
2. memory/2026-02-08.md (score: 0.89) <- Router + VLAN (fast-Duplikat!)
3. memory/network.md (score: 0.85) <- Referenzdokument
Mit MMR (lambda=0.7) — Top 3 Ergebnisse:
1. memory/2026-02-10.md (score: 0.92) <- Router + VLAN
2. memory/network.md (score: 0.85) <- Referenzdokument (divers!)
3. memory/2026-02-05.md (score: 0.78) <- AdGuard DNS (divers!)
Das Fast-Duplikat vom 8. Februar faellt heraus, und der Agent bekommt drei verschiedene Informationsstuecke.
Wann aktivieren: Wenn memory_search redundante oder fast-doppelte Snippets zurueckgibt,
besonders bei Tagesnotizen, die oft aehnliche Informationen ueber Tage hinweg wiederholen.
Temporal Decay (Aktualitaetsboost)
Agenten mit Tagesnotizen sammeln im Laufe der Zeit hunderte datierte Dateien an. Ohne Decay kann eine gut formulierte Notiz von vor sechs Monaten das gestrige Update zum selben Thema uebertrumpfen.
Temporal Decay wendet einen exponentiellen Multiplikator auf Scores basierend auf dem Alter jedes Ergebnisses an, sodass neuere Erinnerungen natuerlich hoeher ranken, waehrend alte verblassen:
decayedScore = score x e^(-lambda x ageInDays)
wobei lambda = ln(2) / halfLifeDays.
Mit der Standard-Halbwertszeit von 30 Tagen:
- Heutige Notizen: 100% des Original-Scores
- Vor 7 Tagen: ~84%
- Vor 30 Tagen: 50%
- Vor 90 Tagen: 12,5%
- Vor 180 Tagen: ~1,6%
Evergreen-Dateien werden nie gealtert:
MEMORY.md(Root-Memory-Datei)- Nicht-datierte Dateien in
memory/(z.B.memory/projects.md,memory/network.md) - Diese enthalten dauerhafte Referenzinformationen, die immer normal ranken sollen.
Datierte Tagesdateien (memory/YYYY-MM-DD.md) verwenden das aus dem Dateinamen extrahierte Datum.
Andere Quellen (z.B. Sitzungstranskripte) greifen auf die Dateimodifikationszeit (mtime) zurueck.
Beispiel — Anfrage: “was ist Rods Arbeitsplan?”
Gegeben diese Memory-Dateien (heute ist der 10. Feb):
memory/2025-09-15.md -> "Rod works Mon-Fri, standup at 10am, pairing at 2pm" (148 Tage alt)
memory/2026-02-10.md -> "Rod has standup at 14:15, 1:1 with Zeb at 14:45" (heute)
memory/2026-02-03.md -> "Rod started new team, standup moved to 14:15" (7 Tage alt)
Ohne Decay:
1. memory/2025-09-15.md (score: 0.91) <- bester semantischer Match, aber veraltet!
2. memory/2026-02-10.md (score: 0.82)
3. memory/2026-02-03.md (score: 0.80)
Mit Decay (halfLife=30):
1. memory/2026-02-10.md (score: 0.82 x 1.00 = 0.82) <- heute, kein Decay
2. memory/2026-02-03.md (score: 0.80 x 0.85 = 0.68) <- 7 Tage, milder Decay
3. memory/2025-09-15.md (score: 0.91 x 0.03 = 0.03) <- 148 Tage, fast verschwunden
Die veraltete September-Notiz rutscht trotz des besten rohen semantischen Matches ans Ende.
Wann aktivieren: Wenn dein Agent Monate von Tagesnotizen hat und alte, veraltete Informationen neueren Kontext uebertrumpfen. Eine Halbwertszeit von 30 Tagen funktioniert gut fuer tagesnotiz-lastige Workflows; erhoehe sie (z.B. 90 Tage), wenn du oefter auf aeltere Notizen zurueckgreifst.
Konfiguration
Beide Features werden unter memorySearch.query.hybrid konfiguriert:
agents: {
defaults: {
memorySearch: {
query: {
hybrid: {
enabled: true,
vectorWeight: 0.7,
textWeight: 0.3,
candidateMultiplier: 4,
// Diversitaet: redundante Ergebnisse reduzieren
mmr: {
enabled: true, // Standard: false
lambda: 0.7 // 0 = max. Diversitaet, 1 = max. Relevanz
},
// Aktualitaet: neuere Erinnerungen boosten
temporalDecay: {
enabled: true, // Standard: false
halfLifeDays: 30 // Score halbiert sich alle 30 Tage
}
}
}
}
}
}
Du kannst jedes Feature unabhaengig aktivieren:
- Nur MMR — nuetzlich, wenn du viele aehnliche Notizen hast, aber das Alter egal ist.
- Nur Temporal Decay — nuetzlich, wenn Aktualitaet wichtig ist, aber deine Ergebnisse bereits divers sind.
- Beide — empfohlen fuer Agenten mit grossen, langlebigen Tagesnotiz-Historien.
Embedding-Cache
OpenClaw kann Chunk-Embeddings in SQLite cachen, damit Neuindizierung und haeufige Updates (besonders Sitzungstranskripte) unveraenderten Text nicht erneut embedden.
Config:
agents: {
defaults: {
memorySearch: {
cache: {
enabled: true,
maxEntries: 50000
}
}
}
}
Session-Memory-Suche (experimentell)
Du kannst optional Sitzungstranskripte indizieren und ueber memory_search verfuegbar machen.
Das ist hinter einem experimentellen Flag geschuetzt.
agents: {
defaults: {
memorySearch: {
experimental: { sessionMemory: true },
sources: ["memory", "sessions"]
}
}
}
Hinweise:
- Session-Indizierung ist opt-in (standardmaessig deaktiviert).
- Session-Updates werden entprellt und asynchron indiziert, sobald sie Delta-Schwellenwerte ueberschreiten (Best-Effort).
memory_searchblockiert nie auf Indizierung; Ergebnisse koennen leicht veraltet sein, bis die Hintergrund-Synchronisation abgeschlossen ist.- Ergebnisse enthalten weiterhin nur Snippets;
memory_getbleibt auf Memory-Dateien beschraenkt. - Session-Indizierung ist pro Agent isoliert (nur die Sitzungsprotokolle dieses Agenten werden indiziert).
- Sitzungsprotokolle liegen auf der Festplatte (
~/.openclaw/agents/<agentId>/sessions/*.jsonl). Jeder Prozess/Nutzer mit Dateisystemzugriff kann sie lesen, daher behandle Festplattenzugriff als Vertrauensgrenze. Fuer strengere Isolation fuehre Agenten unter separaten OS-Nutzern oder Hosts aus.
Delta-Schwellenwerte (Standardwerte gezeigt):
agents: {
defaults: {
memorySearch: {
sync: {
sessions: {
deltaBytes: 100000, // ~100 KB
deltaMessages: 50 // JSONL-Zeilen
}
}
}
}
}
SQLite-Vektor-Beschleunigung (sqlite-vec)
Wenn die sqlite-vec-Extension verfuegbar ist, speichert OpenClaw Embeddings in einer
SQLite-virtuellen Tabelle (vec0) und fuehrt Vektor-Distanz-Abfragen in der
Datenbank aus. Das haelt die Suche schnell, ohne jedes Embedding in JS laden zu muessen.
Konfiguration (optional):
agents: {
defaults: {
memorySearch: {
store: {
vector: {
enabled: true,
extensionPath: "/path/to/sqlite-vec"
}
}
}
}
}
Hinweise:
enabledist standardmaessig true; wenn deaktiviert, faellt die Suche auf In-Process-Kosinus-Aehnlichkeit ueber gespeicherte Embeddings zurueck.- Wenn die sqlite-vec-Extension fehlt oder nicht geladen werden kann, loggt OpenClaw den Fehler und faehrt mit dem JS-Fallback fort (keine Vektortabelle).
extensionPathueberschreibt den mitgelieferten sqlite-vec-Pfad (nuetzlich fuer eigene Builds oder nicht-standardmaessige Installationsorte).
Lokaler Embedding-Auto-Download
- Standard lokales Embedding-Modell:
hf:ggml-org/embeddinggemma-300m-qat-q8_0-GGUF/embeddinggemma-300m-qat-Q8_0.gguf(~0,6 GB). - Wenn
memorySearch.provider = "local", loestnode-llama-cppdenmodelPathauf; wenn das GGUF fehlt, wird es automatisch heruntergeladen in den Cache (oderlocal.modelCacheDir, wenn gesetzt), dann geladen. Downloads werden beim Retry fortgesetzt. - Native-Build-Anforderung: Fuehre
pnpm approve-buildsaus, waehlenode-llama-cpp, dannpnpm rebuild node-llama-cpp. - Fallback: Wenn das lokale Setup fehlschlaegt und
memorySearch.fallback = "openai", wechseln wir automatisch zu Remote-Embeddings (openai/text-embedding-3-small, sofern nicht ueberschrieben) und protokollieren den Grund.
Beispiel fuer benutzerdefinierten OpenAI-kompatiblen Endpunkt
agents: {
defaults: {
memorySearch: {
provider: "openai",
model: "text-embedding-3-small",
remote: {
baseUrl: "https://api.example.com/v1/",
apiKey: "YOUR_REMOTE_API_KEY",
headers: {
"X-Organization": "org-id",
"X-Project": "project-id"
}
}
}
}
}
Hinweise:
remote.*hat Vorrang vormodels.providers.openai.*.remote.headerswerden mit OpenAI-Headern zusammengefuehrt; Remote gewinnt bei Schluesselkonflikten. Lasseremote.headersweg, um die OpenAI-Standards zu nutzen.