From fb06ed928cca9b9a670a1b20a62c6085c8fe3d89 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sat, 11 Jul 2026 22:30:00 +0200 Subject: [PATCH] =?UTF-8?q?fix(local-llm):=20Anti-Halluzination=20?= =?UTF-8?q?=E2=80=94=20behauptete=20Skill-Aktion=20ohne=20Tool=20=E2=86=92?= =?UTF-8?q?=20Claude?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Local hat "Spotify: überspringe 3 Titel" / "Playlist X abspielen" geantwortet OHNE run_spotify je zu rufen (Skill-Logs: kein Aufruf) — reine Fabrikation, real passierte nichts. Zwei Ursachen behoben: - Prompt: run_spotify-Beschreibung von "next/pause/weiter" auf JEDE Musiksteuerung erweitert (Playlist, Gerät, überspringen mehrerer, Lautstärke) + harte Anti-Halluzinations-Regel (nie eine Aktion behaupten ohne Tool-Call). - Guard: sagt local eine Steuerbefehl-Quittung ('Spotify: …', 'Playlist … abspielen', 'überspringe … Titel') aber hat KEINEN run_*-Skill gerufen (_local_ran_skill=False) → eskalieren, Claude ruft das Tool zuverlässig. _looks_like_skill_action bewusst eng (Doppelpunkt-Praefix / spezifische Verben), trifft normale Musik-Konversation nicht. Co-Authored-By: Claude Opus 4.8 --- aria-brain/agent.py | 31 +++++++++++++++++++++++++++++++ aria-brain/router.py | 10 +++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/aria-brain/agent.py b/aria-brain/agent.py index 8c584e9..ac85812 100644 --- a/aria-brain/agent.py +++ b/aria-brain/agent.py @@ -1064,6 +1064,23 @@ def _fold_umlauts(s: str) -> str: .replace("ß", "ss")) +def _looks_like_skill_action(text: str) -> bool: + """Erkennt eine BEHAUPTETE Steuerbefehl-Quittung (v.a. Spotify) im Reply. + Genutzt fuer den Anti-Halluzinations-Guard: sagt local sowas, hat aber KEIN + Werkzeug gerufen, ist real nichts passiert → eskalieren. Bewusst eng, um + normale Konversation ueber Musik nicht zu treffen.""" + t = (text or "").strip().lower() + if not t: + return False + if t.startswith("spotify:") or t.startswith("spotify -") or t.startswith("musik:"): + return True + if "playlist" in t and ("abspiel" in t or "spiele" in t or "starte" in t): + return True + if ("ueberspring" in t or "überspring" in t) and ("titel" in t or "lied" in t or "song" in t): + return True + return False + + def _normalize_for_fast_match(text: str) -> str: norm = _strip_leading_hint_blocks(text).lower() norm = _fold_umlauts(norm) @@ -1273,6 +1290,11 @@ class Agent: # dem Manifest (Steuerbefehl-Skill=False → stumm, Antwort-Skill=True). # Info-Tools (web_search/memory_search/trigger_timer) lassen es bei True. self._local_turn_speak = True + # Hat local in diesem Turn ueberhaupt einen echten Skill (run_*) gerufen? + # Fuer den Anti-Halluzinations-Guard: behauptet local eine Spotify-/Skill- + # Aktion OHNE das Tool zu rufen → eskalieren statt eine erfundene + # Bestaetigung durchzulassen. + self._local_ran_skill = False local_only = bool(cfg.get("localOnly")) local_model = cfg.get("localLlmModel") or "qwen3-8b" # B0.5: llama-swap-Key tools = self._build_local_tools() # B1b: kuratierte Tools @@ -1325,6 +1347,7 @@ class Agent: # Antwort-Skill=vorlesen). Letzter Skill gewinnt. if tname.startswith("run_") and not self._local_tool_failed(tname, tresult): self._local_turn_speak = self._skill_speak_flag(tname) + self._local_ran_skill = True if self._local_tool_failed(tname, tresult): had_error = True messages.append({"role": "tool", @@ -1370,6 +1393,14 @@ class Agent: logger.info("[router] lokal eskaliert → Claude") return None + # Anti-Halluzination: local behauptet manchmal eine Musik-/Skill-Aktion + # ('Spotify: …', 'Playlist … abspielen'), OHNE run_spotify gerufen zu + # haben → es ist real nichts passiert. Dann eskalieren: Claude ruft das + # Tool zuverlaessig. (Echte Skill-Ausfuehrung → _local_ran_skill=True.) + if not self._local_ran_skill and _looks_like_skill_action(final): + logger.info("[router] lokal: Skill-Aktion behauptet ohne Tool-Call → Claude") + return None + logger.info("[router] lokal beantwortet (%d Zeichen)", len(final)) self.conversation.add("assistant", final, project_id=active_project_id) return final diff --git a/aria-brain/router.py b/aria-brain/router.py index 840a22b..ce22c85 100644 --- a/aria-brain/router.py +++ b/aria-brain/router.py @@ -153,10 +153,18 @@ def build_local_system_prompt(identity_anchor: str, has_tools: bool = False, "GPS-Hinweis in der Nachricht.", "- `memory_search`: in ARIAs Gedaechtnis nachsehen (lesen).", "- `trigger_timer`: Timer/Erinnerung setzen ('in 10 Minuten…').", - "- `run_spotify`: Musik steuern (naechster Titel, Pause, weiter).", + "- `run_spotify`: JEDE Musiksteuerung — Titel wechseln/ueberspringen " + "(auch mehrere), Pause/Weiter, eine PLAYLIST abspielen, auf einem " + "GERAET abspielen, Lautstaerke, 'was laeuft gerade'. Fuer ALLES rund " + "um Musik/Spotify IMMER dieses Werkzeug rufen — nie selbst antworten.", "WICHTIG: Bei reinem Smalltalk ('wie gehts', Begruessung, Meinung) " "KEIN Werkzeug — einfach direkt antworten. Werkzeuge nur bei echtem " "Bedarf; erfinde keine.", + "ANTI-HALLUZINATION (kritisch): Behaupte NIEMALS eine Aktion (Musik, " + "Timer, Suche) als erledigt, ohne das Werkzeug WIRKLICH aufgerufen zu " + "haben. Kein 'Spotify: …' / 'Playlist abspielen' o.ae. ohne echten " + "run_spotify-Aufruf. Kannst/willst du es nicht per Werkzeug tun, sag " + "es ehrlich oder eskaliere — erfinde keine Bestaetigung.", ] parts += [ "",