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:
@@ -1064,6 +1064,23 @@ def _fold_umlauts(s: str) -> str:
|
|||||||
.replace("ß", "ss"))
|
.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:
|
def _normalize_for_fast_match(text: str) -> str:
|
||||||
norm = _strip_leading_hint_blocks(text).lower()
|
norm = _strip_leading_hint_blocks(text).lower()
|
||||||
norm = _fold_umlauts(norm)
|
norm = _fold_umlauts(norm)
|
||||||
@@ -1273,6 +1290,11 @@ class Agent:
|
|||||||
# dem Manifest (Steuerbefehl-Skill=False → stumm, Antwort-Skill=True).
|
# dem Manifest (Steuerbefehl-Skill=False → stumm, Antwort-Skill=True).
|
||||||
# Info-Tools (web_search/memory_search/trigger_timer) lassen es bei True.
|
# Info-Tools (web_search/memory_search/trigger_timer) lassen es bei True.
|
||||||
self._local_turn_speak = 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_only = bool(cfg.get("localOnly"))
|
||||||
local_model = cfg.get("localLlmModel") or "qwen3-8b" # B0.5: llama-swap-Key
|
local_model = cfg.get("localLlmModel") or "qwen3-8b" # B0.5: llama-swap-Key
|
||||||
tools = self._build_local_tools() # B1b: kuratierte Tools
|
tools = self._build_local_tools() # B1b: kuratierte Tools
|
||||||
@@ -1325,6 +1347,7 @@ class Agent:
|
|||||||
# Antwort-Skill=vorlesen). Letzter Skill gewinnt.
|
# Antwort-Skill=vorlesen). Letzter Skill gewinnt.
|
||||||
if tname.startswith("run_") and not self._local_tool_failed(tname, tresult):
|
if tname.startswith("run_") and not self._local_tool_failed(tname, tresult):
|
||||||
self._local_turn_speak = self._skill_speak_flag(tname)
|
self._local_turn_speak = self._skill_speak_flag(tname)
|
||||||
|
self._local_ran_skill = True
|
||||||
if self._local_tool_failed(tname, tresult):
|
if self._local_tool_failed(tname, tresult):
|
||||||
had_error = True
|
had_error = True
|
||||||
messages.append({"role": "tool",
|
messages.append({"role": "tool",
|
||||||
@@ -1370,6 +1393,14 @@ class Agent:
|
|||||||
logger.info("[router] lokal eskaliert → Claude")
|
logger.info("[router] lokal eskaliert → Claude")
|
||||||
return None
|
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))
|
logger.info("[router] lokal beantwortet (%d Zeichen)", len(final))
|
||||||
self.conversation.add("assistant", final, project_id=active_project_id)
|
self.conversation.add("assistant", final, project_id=active_project_id)
|
||||||
return final
|
return final
|
||||||
|
|||||||
@@ -153,10 +153,18 @@ def build_local_system_prompt(identity_anchor: str, has_tools: bool = False,
|
|||||||
"GPS-Hinweis in der Nachricht.",
|
"GPS-Hinweis in der Nachricht.",
|
||||||
"- `memory_search`: in ARIAs Gedaechtnis nachsehen (lesen).",
|
"- `memory_search`: in ARIAs Gedaechtnis nachsehen (lesen).",
|
||||||
"- `trigger_timer`: Timer/Erinnerung setzen ('in 10 Minuten…').",
|
"- `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) "
|
"WICHTIG: Bei reinem Smalltalk ('wie gehts', Begruessung, Meinung) "
|
||||||
"KEIN Werkzeug — einfach direkt antworten. Werkzeuge nur bei echtem "
|
"KEIN Werkzeug — einfach direkt antworten. Werkzeuge nur bei echtem "
|
||||||
"Bedarf; erfinde keine.",
|
"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 += [
|
parts += [
|
||||||
"",
|
"",
|
||||||
|
|||||||
Reference in New Issue
Block a user