fix(local-llm): Anti-Halluzination — behauptete Skill-Aktion ohne Tool → Claude

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 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 22:30:00 +02:00
co-authored by Claude Opus 4.8
parent 08bb257791
commit fb06ed928c
2 changed files with 40 additions and 1 deletions
+31
View File
@@ -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
+9 -1
View File
@@ -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 += [
"",