Gedächtnis: nur Relevantes lernen/erinnern (Anti-Selbstbezug + Cache-Stabilität)

Der auto-lernende Mem0-Store sammelte trotz Relevanz-Direktive viel Müll:
Fakten ÜBER den Assistenten selbst (Hermes/Lucy), transiente Zustände
(Commit-Rückstand, Cronjob-Läufe, Onboarding-/Test-Fragen) und Gesprächs-Meta.
Live-Store war zu ~40% solcher Ballast; der niedrige Recall-Schwellwert (0.3)
blendete zudem marginale Fakten pro Turn ein und brach damit --cache-reuse.

- mem0_service/app.py: custom_instructions verschärft (NIEMALS Fakten über den
  Assistenten selbst / keine zeitgebundenen Zustände) + deterministischer
  Post-Extraktions-Guard (_is_junk_fact) löscht Assistenten-Meta/transiente
  Fakten, die mem0-OSS trotz Direktive extrahiert — unabhängig vom LLM.
- hermes/plugins/mc2-memory/__init__.py: RECALL_MIN_SCORE 0.3->0.5 (nur starke
  Treffer, oft leer -> stabilerer Prompt-Prefix -> --cache-reuse greift),
  MIN_USER_LEN 12->25 + Trivial-Turn-Skip (Begrüßung/Quittung/Aufwärmen).

Alles env-überschreibbar und reversibel. Backend/Frontend unberührt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-01 16:45:45 +02:00
parent cb4d7102b5
commit 6db1224d95
2 changed files with 74 additions and 8 deletions
+23 -2
View File
@@ -24,6 +24,7 @@ from __future__ import annotations
import logging import logging
import os import os
import queue import queue
import re
import threading import threading
import time import time
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
@@ -37,8 +38,11 @@ log = logging.getLogger(__name__)
MC_URL = os.environ.get("MC_URL", "http://127.0.0.1:9001").rstrip("/") MC_URL = os.environ.get("MC_URL", "http://127.0.0.1:9001").rstrip("/")
MC_TOKEN = os.environ.get("MC_TOKEN", "") MC_TOKEN = os.environ.get("MC_TOKEN", "")
RECALL_LIMIT = int(os.environ.get("MC2_MEMORY_RECALL_LIMIT", "5")) RECALL_LIMIT = int(os.environ.get("MC2_MEMORY_RECALL_LIMIT", "5"))
RECALL_MIN_SCORE = float(os.environ.get("MC2_MEMORY_RECALL_MIN_SCORE", "0.3")) # 0.5 statt 0.3: nur starke Treffer einblenden → weniger Rausch-Fakten UND ein stabilerer
MIN_USER_LEN = int(os.environ.get("MC2_MEMORY_MIN_USER_LEN", "12")) # zu Kurzes nicht lernen # Prompt-Prefix (oft leer), damit --cache-reuse über Turns greift.
RECALL_MIN_SCORE = float(os.environ.get("MC2_MEMORY_RECALL_MIN_SCORE", "0.5"))
# 25 statt 12: triviale Kurz-Turns (Begrüßung/Quittung) gar nicht erst zum Lernen schicken.
MIN_USER_LEN = int(os.environ.get("MC2_MEMORY_MIN_USER_LEN", "25")) # zu Kurzes nicht lernen
# Anti-Memory-Poisoning (Stufe 0 Security): aus Turns mit untrusted Bildschirm-/Web-Inhalt NICHT # Anti-Memory-Poisoning (Stufe 0 Security): aus Turns mit untrusted Bildschirm-/Web-Inhalt NICHT
# auto-lernen. Lucys Bildschirm-Sicht prependet diesen Marker an die User-Message (voice.py); auf dem # auto-lernen. Lucys Bildschirm-Sicht prependet diesen Marker an die User-Message (voice.py); auf dem
# Schirm sichtbarer Text könnte Injection-Anweisungen tragen, die sonst DAUERHAFT ins Gedächtnis # Schirm sichtbarer Text könnte Injection-Anweisungen tragen, die sonst DAUERHAFT ins Gedächtnis
@@ -46,6 +50,20 @@ MIN_USER_LEN = int(os.environ.get("MC2_MEMORY_MIN_USER_LEN", "12")) # zu Kurzes
SKIP_UNTRUSTED = os.environ.get("MC2_MEMORY_SKIP_UNTRUSTED", "1") not in ("0", "false", "False", "no") SKIP_UNTRUSTED = os.environ.get("MC2_MEMORY_SKIP_UNTRUSTED", "1") not in ("0", "false", "False", "no")
UNTRUSTED_MARKERS = ("[Bildschirm-Sicht",) UNTRUSTED_MARKERS = ("[Bildschirm-Sicht",)
# Triviale Turns, die keinen dauerhaften Fakt tragen (reine Begrüßung/Quittung, Aufwärm-/Test-
# Kommandos) → gar nicht erst zum Auto-Lernen schicken. Spart Extraktions-Läufe und verhindert
# Müll-Fakten aus Meta-/Ablauf-Turns.
_TRIVIAL_TURN_RE = re.compile(
r"^\s*(?:(?:hi|hallo|hey|moin|servus|guten\s+(?:morgen|tag|abend)|danke\w*|"
r"ok(?:ay)?|alles\s+klar|passt|super|top|cool|nice|ja|nein|jo|jup|jap)[\s.,!?]*)+$"
r"|^\s*(?:aufw(?:ä|ae)rmen|/?test)\b",
re.IGNORECASE,
)
def _is_trivial_turn(text: str) -> bool:
return bool(_TRIVIAL_TURN_RE.search(text or ""))
def _headers() -> dict: def _headers() -> dict:
return {"X-MC-Token": MC_TOKEN} if MC_TOKEN else {} return {"X-MC-Token": MC_TOKEN} if MC_TOKEN else {}
@@ -124,6 +142,9 @@ class MC2MemoryProvider(MemoryProvider):
u = (user_content or "").strip() u = (user_content or "").strip()
if len(u) < MIN_USER_LEN: if len(u) < MIN_USER_LEN:
return return
if _is_trivial_turn(u):
log.debug("mc2-memory: trivialer Turn (Begrüßung/Quittung/Aufwärmen) — Auto-Lernen übersprungen")
return
if SKIP_UNTRUSTED and any(m in u for m in UNTRUSTED_MARKERS): if SKIP_UNTRUSTED and any(m in u for m in UNTRUSTED_MARKERS):
log.debug("mc2-memory: Turn mit untrusted Bildschirm-/Web-Inhalt — Auto-Lernen übersprungen") log.debug("mc2-memory: Turn mit untrusted Bildschirm-/Web-Inhalt — Auto-Lernen übersprungen")
return return
+51 -6
View File
@@ -20,6 +20,7 @@ Bind: 127.0.0.1 (nur lokal; MC2 proxyt nach außen).
import logging import logging
import os import os
import re
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException from fastapi import FastAPI, HTTPException
@@ -95,12 +96,19 @@ def build_memory() -> Memory:
"custom_instructions": ( "custom_instructions": (
"RELEVANZ (ZWINGEND, WICHTIGSTE REGEL): Extrahiere NUR dauerhaft nützliche Fakten ÜBER DEN " "RELEVANZ (ZWINGEND, WICHTIGSTE REGEL): Extrahiere NUR dauerhaft nützliche Fakten ÜBER DEN "
"NUTZER (Identität, Vorlieben, Arbeitsweise), seine Projekte/Entscheidungen/Stände, stabile " "NUTZER (Identität, Vorlieben, Arbeitsweise), seine Projekte/Entscheidungen/Stände, stabile "
"Fakten über Stack/Tools/Infrastruktur sowie verbindliche Regeln und Workflows. IGNORIERE " "Fakten über Stack/Tools/Infrastruktur sowie verbindliche Regeln und Workflows. "
"VOLLSTÄNDIG und gib KEINE Fakten zurück bei: Tagesnachrichten und Welt-/Politik-/Börsen-/" "NIEMALS FAKTEN ÜBER DEN ASSISTENTEN SELBST (ZWINGEND): Extrahiere KEINE Aussagen über den "
"Promi-Ereignissen, Wetter, allgemeinem Weltwissen ohne Bezug zum Nutzer, vom Assistenten " "Assistenten/die KI (Hermes, Lucy), seine Fähigkeiten, seinen Zustand, seine Tools/MCP-Server, "
"erzeugten Zusammenfassungen oder Recherche-Ergebnissen, Smalltalk, sowie Meta-Aussagen über " "seine Cronjobs, Commit-/Versionsstände oder welche Modelle er nutzt. Das Subjekt eines Fakts "
"den Gesprächsverlauf (z.B. 'der Nutzer fragt nach X', 'der Nutzer möchte Y prüfen', 'der " "ist IMMER der Nutzer oder sein Projekt — niemals 'der Assistent', 'Hermes' oder 'Lucy'. "
"Assistent tat Z'). Speichere nur, was in Wochen noch nützlich ist; im Zweifel NICHT speichern. " "KEINE ZEITGEBUNDENEN ZUSTÄNDE: Ignoriere transiente Momentaufnahmen (Fehler-/Job-Läufe, "
"Commit-Rückstand, Versions-Updates, 'gerade dabei'-/'hochgefahren'-/'aufgewärmt'-Status, "
"Test- und Onboarding-Fragen). IGNORIERE VOLLSTÄNDIG und gib KEINE Fakten zurück bei: "
"Tagesnachrichten und Welt-/Politik-/Börsen-/Promi-Ereignissen, Wetter, allgemeinem Weltwissen "
"ohne Bezug zum Nutzer, vom Assistenten erzeugten Zusammenfassungen oder Recherche-Ergebnissen, "
"Smalltalk, sowie Meta-Aussagen über den Gesprächsverlauf (z.B. 'der Nutzer fragt nach X', "
"'der Nutzer möchte Y prüfen', 'der Assistent tat Z'). Speichere nur, was in Wochen noch "
"nützlich ist; im Zweifel NICHT speichern. "
"SPRACHE (ZWINGEND): Formuliere ALLE Fakten auf DEUTSCH — unabhängig von der " "SPRACHE (ZWINGEND): Formuliere ALLE Fakten auf DEUTSCH — unabhängig von der "
"Eingabesprache, niemals auf Englisch übersetzen. Eigennamen, Befehle, Pfade, Modell- und " "Eingabesprache, niemals auf Englisch übersetzen. Eigennamen, Befehle, Pfade, Modell- und "
"Produktnamen sowie etablierte Fachbegriffe (z.B. PowerShell-Push, Deploy, Qwen3.6, Neovim, " "Produktnamen sowie etablierte Fachbegriffe (z.B. PowerShell-Push, Deploy, Qwen3.6, Neovim, "
@@ -327,6 +335,30 @@ def classify_facts(facts: list[tuple[str, str]]) -> dict:
return {} return {}
# Deterministischer Guard: mem0-OSS extrahiert trotz custom_instructions hartnäckig Fakten ÜBER
# den Assistenten selbst (Hermes/Lucy) und transiente Zustände (Commit-Rückstand, Cronjob-Läufe,
# Onboarding-/Test-Fragen). Diese Muster werden nach der Extraktion verworfen (der Fakt wird wieder
# gelöscht) — reine Sicherungsschicht, unabhängig vom Extraktions-LLM.
_JUNK_FACT_PATTERNS = [
r"^\s*(?:der|die|das)\s+assistent", # "Der Assistent ..." (Selbstbeschreibung)
r"\bassistent(?:in)?\s+(?:hermes|lucy)\b", # "... Assistent Hermes/Lucy ..."
r"\bhermes[- ]agent\b",
r"commits?\s+hinter", # Versions-Rückstand (transient)
r"\bcronjob\b",
r"beim\s+letzten\s+(?:lauf|mal)",
r"onboarding[-\s]?frage|\btest-?frage\b",
r"hat\s+den\s+assistenten\s+gebeten",
r"auf\s+betriebstemperatur|hochgefahren",
r"aktualisiert\s+sein\s+ged(?:ä|ae)chtnis", # Selbst-Meta über das Gedächtnis
]
_JUNK_FACT_RE = re.compile("|".join(_JUNK_FACT_PATTERNS), re.IGNORECASE)
def _is_junk_fact(text: str) -> bool:
"""True, wenn der extrahierte Fakt Assistenten-Meta oder ein transienter Zustand ist."""
return bool(_JUNK_FACT_RE.search(text or ""))
@app.post("/learn") @app.post("/learn")
def learn(body: LearnIn) -> dict: def learn(body: LearnIn) -> dict:
"""Auto-Lernen: Gesprächs-Turns/Text durchreichen → Mem0 EXTRAHIERT Fakten selbst """Auto-Lernen: Gesprächs-Turns/Text durchreichen → Mem0 EXTRAHIERT Fakten selbst
@@ -342,6 +374,19 @@ def learn(body: LearnIn) -> dict:
) )
results = res.get("results", []) results = res.get("results", [])
# Guard: frisch extrahierte Assistenten-Meta/transiente Fakten sofort wieder verwerfen.
kept = []
for r in results:
if r.get("event") in ("ADD", "UPDATE") and _is_junk_fact(r.get("memory", "")):
try:
mem().delete(r["id"])
except Exception:
log.debug("Junk-Fakt-Löschung fehlgeschlagen für %s", r.get("id"))
log.info("mem0 guard: Junk-Fakt verworfen: %s", (r.get("memory") or "")[:80])
continue
kept.append(r)
results = kept
cats: dict = {} cats: dict = {}
if body.classify: if body.classify:
to_classify = [(r["id"], r.get("memory", "")) for r in results to_classify = [(r["id"], r.get("memory", "")) for r in results