Frische-/Web-Such-Funktion: Das zentrale Modell entscheidet selbst via web_search-Tool, ob es tagesaktuelle Fakten braucht, holt sie über perplexity/sonar und formuliert die Antwort in Persona (Augment). - SonarTool (app/tools/web_search.py): Fakten via perplexity/sonar, Citations als Metadaten, honest-punt-Sentinel bei Fehler/Timeout. - ToolCallingLLM (app/providers/llm/tool_calling.py): agentischer Loop als LLMProvider; complete() + gestreamtes stream() mit SSE-Tool-Assembler; Persona- + Trigger- + Vorrang-Prompt (Tool-Ergebnis schlaegt Gedaechtnis). - Verdrahtung: Registry-Eintrag openrouter-tools; web_search_enabled (global an, pro Nutzer/Profil abschaltbar) via Route-Layering; build_orchestrator waehlt tool-faehig vs. plain, Fallback-Kette erhalten. - Filler: ephemerer Beruhigungssatz beim Tool-Start (sofort angezeigt UND gesprochen als Satz null), nie in semantic_response/History; on_tool_start defensiv durch die stream()-Kette gefaedelt (kein Bruch bestehender Provider). - Modellwechsel: Standard auf mistralai/mistral-small-3.2-24b-instruct (tool-faehig; im Eval einziger Recall-Gate-Passer). 2501 ist tool-unfaehig. - Eval-Harness (eval/tool_calling/): Datensatz + Runner zur Modellauswahl. Doc: Docs/weg2-tool-calling.md. Tests: 290 gruen. Offen (Schritt 5): Koreferenz-Vorstufe (nl-Pronomen) + Metrik-Zaehler. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
8 KiB
Design: Web-Suche per Tool-Calling (Weg 2)
Stand: Entwurf. Beschreibt das Konzept für die Frische-/Web-Such-Funktion auf Basis von agentischem Tool-Calling. Modellwahl und Schwellen sind durch den Eval-Harness (
eval/tool_calling/) belegt; Implementierung folgt diesem Dokument.
1. Ziel & Leitidee
Das zentrale LLM kennt nur Wissen bis zu seinem Trainings-Cutoff. Fragt der Nutzer nach etwas, das sich seither geändert haben könnte (aktuelle Amtsträger, Preise, Wetter, Nachrichten, „lebt X noch", Öffnungszeiten …), soll das Modell selbst eine Web-Suche auslösen, die frischen Fakten holen und die Antwort in der Alexis-Persona formulieren.
Weg 2 = Tool-Calling + Anreichern: Das Antwortmodell entscheidet via Werkzeug, ob es sucht; die Suche liefert nur Fakten, das Modell formuliert. Vorteil: durchgängige Stimme, Persona/Safety/History bleiben beim Hauptmodell. Bewusst verworfen wurde der separate Vorab-Klassifikator („Weg 1"), weil das gewählte Modell sich zuverlässig genug selbst triggert (siehe §7).
2. Modellwahl (belegt)
mistralai/mistral-small-3.2-24b-instruct über OpenRouter. Eval-Ergebnis
(69 Fälle × 5 Läufe): Trigger-Recall 93 %, Vorrang-Treue 100 %,
Wohlgeformtheit 100 %, gefährliche Kategorien (Amtsträger/Preise/Nachrichten/
Sport/Releases) 100 %, Wetter inkl. implizitem Ort 98 %. Günstig und klein →
niedrige Latenz (im Senioren-Kontext doppelt wertvoll). Details und Vergleich
(Flash/Pro/alte Baseline) in der Memory-Notiz bzw. eval/tool_calling/.
Achtung: Die bisherige TOML-Baseline
mistral-small-24b-instruct-2501kann kein Tool-Calling (OpenRouter 404 „No endpoints found that support tool use"). Ein Revert darauf würde Weg 2 abschalten.
3. Keystone: Der Tool-Loop lebt im LLM-Provider
Ein neuer Wrapper ToolCallingLLM implementiert die bestehende
LLMProvider-Schnittstelle (complete()/stream()), wickelt aber intern die
Agenten-Schleife ab. Der Orchestrator ruft weiter nur self.llm.stream(...) und
weiß nichts von Tools. Folge: ein neuer LLM_REGISTRY-Eintrag, keine
Kern-Änderung, volle Kompatibilität mit resolve_route, Fallback-Ketten und
Profilen. Das ist das Leitprinzip „jede Achse austauschbar".
ToolCallingLLM enthält einen eigenen tool-fähigen OpenRouter-Client (Keim:
eval/tool_calling/client.py) — nicht den plain OpenRouterLLMProvider, da
dessen complete() keine tools kennt.
4. Datenfluss
Kein-Tool-Turn (Normalfall, kein Latenz-Regress):
stream() → Modell mit tools=[…] → Text-Deltas → 1:1 an on_token/SentenceChunker
Such-Turn (augment):
(0) Koreferenz-Vorstufe: Pronomen aus History auflösen (gegated)
(1) Modell → tool_call("web_search", query)
(2) on_tool_start(language) → Filler SOFORT sprechen/anzeigen
(3) SonarTool(query) → Fakten (~1–3 s, vom Filler überdeckt)
(4) Modell-Runde 2 mit tool-Ergebnis → finale Antwort, gestreamt, in Persona
5. Komponenten
5.1 SonarTool
Ruft perplexity/sonar über OpenRouter (gleiche Chat-API wie OpenRouterLLMProvider).
Prompt an Sonar: knapp, Zielsprache, reine Fakten. Rückgabe als tool-Message ans
Modell. Citations als strukturierte Metadaten an die UI-Bubble (nicht ins TTS).
Timeout/Fehler → Sentinel „keine aktuellen Daten verfügbar", damit das Modell
ehrlich punktet statt zu hängen.
5.2 ToolCallingLLM
Begrenzte Schleife (max. ~3 Runden gegen Runaway). System-Prompt =
Senioren-Persona (SYSTEM_PROMPT aus openrouter.py) + kalibrierte
Trigger-Leitlinie + Vorrang-Regel („Tool-Ergebnis ist maßgeblich und neuer
als dein Wissen; bei Widerspruch folge dem Tool, nicht mischen"). Implementiert
complete() (für chat_text/translate) und stream().
5.3 Koreferenz-Vorstufe (in v1)
Schließt die einzige nicht-triviale Eval-Restkante (ctx_alive_nl:
nl + Pronomen-aus-History; benannt + de-Pronomen sind 5/5). Ein
dekontextualisierender Rewrite macht die letzte Äußerung mit Hilfe des Verlaufs
selbstständig („hij" → „Rutger Hauer"). Gegated: nur bei kurzer Folgefrage
mit Pronomen und vorhandener History, damit er nicht jeden Turn kostet.
5.4 Filler / Beruhigung
stream() erhält einen on_tool_start(language)-Callback. Der Orchestrator
verdrahtet ihn auf: lokalisierten Satz (rotierender Pool je Sprache, Muster wie
die _NOTICE-Dicts) → spoken_adapter/tts_normalizer/tts → on_audio und
on_token. Ephemeralität (Invariante): Der Filler läuft über diesen
Callback, nicht über den Delta-Stream — dadurch landet er nicht in
trace.semantic_response und nicht im gespeicherten History-Turn. Bei langen/
mehreren Tools gestaffelt eskalieren („Moment …" → „Bitte noch einen Augenblick …").
Server-TTS ist ein kleiner Eingriff; „Im Gerät" ist Fast-follow (eigener
Event-Typ → Browser-speak(), Gesten-/Voices-Absicherung).
5.5 Konfiguration & Opt-out
web_search_enabled: bool = True — global an per Default, pro Nutzer/Profil
abschaltbar (Opt-out greift über die bestehende Präzedenz
Defaults < Profil < Nutzer-Prefs). Der Schalter ist die nutzerfreundliche
Admin-/Profil-Option; intern wählt build_orchestrator daraufhin den
tool-fähigen vs. den plain Provider — beide mit demselben konfigurierten
Modell (openrouter_llm_model), damit der Modellwechsel an einer Stelle bleibt.
5.6 Resilienz & Metriken
Fallback: Sonar-Ausfall → honest-punt im Loop; totaler Modellausfall → bestehende
llm_fallback-Kette. Metriken (app/metrics.py): Tool-Calls und Sonar-Calls
separat zählen — Kostensicht und Live-Beobachtung der Trigger-Rate.
Kein eigenes Sonar-Kontingent in v1 (erst Metrik-Sicht; quota.py-Anbindung
später bei Bedarf).
6. Die zwei harten Stellen (bewusst benannt)
- Streaming + Tool-Erkennung. Beim gestreamten ersten Call kommen
tool_call-Deltas fragmentiert. Der Wrapper setzt sie zusammen und unterscheidet: Text-Deltas → durchreichen (kein Regress im Normalfall); materialisiert sich ein Tool-Call → Stream verwerfen, Filler, Sonar, zweite Runde streamen. mistral emittiert entweder Tool-Call oder Text — das macht es handhabbar; das Delta-Zusammensetzen ist die eigentliche Arbeit. - Filler-Ephemeralität. Spielt in Bubble und TTS, darf aber nie in
semantic_response/Store/history. Der Callback-Weg löst das by-design — beim Implementieren strikt einhalten.
7. Warum nicht Weg 1 (separater Klassifikator)
Der Eval zeigte: das Modell self-triggert auf den gefährlichen, konfident- veraltbaren Kategorien zu 100 %; die Recall-Lücke (→93 %) liegt nur in low-/medium-harm-Slices (Logistik honest-punt, eine nl-Koreferenz-Kante). Tool-Calling spart den Vorab-Roundtrip und ist zugleich die Basis für den späteren Multitool-Ausbau (weitere Werkzeuge in dieselbe Schleife, Vorrang-Regel wird zur Tool-Rangordnung).
8. Phasenplan
- v1:
SonarTool+ToolCallingLLM(Loop, Persona+Vorrang-Prompt, eigener Tool-Client) + Koreferenz-Vorstufe + Registry-Eintrag +web_search_enabled(global an, Opt-out) + Filler für Server-TTS + Tool/Sonar-Metriken. schedule_hours-honest-punt akzeptiert. - Fast-follow: Filler für Gerät-TTS (Event-Protokoll) → Citations in die UI-Bubble → ggf. deterministischer Logistik-Nudge.
- Später (Multitool): weitere Tools in dieselbe
ToolCallingLLM-Schleife (Kalender, Erinnerungen, Medizin-Safety) mit Tool-Rangordnung.
9. Berührte Dateien (Implementierungs-Landkarte)
- neu:
app/providers/llm/tool_calling.py(ToolCallingLLM),app/tools/web_search.py(SonarTool),app/pipeline/decontextualizer.py(Koreferenz-Vorstufe) app/dependencies.py—LLM_REGISTRY-Eintrag;build_orchestrator(tool vs. plain jeweb_search_enabled)app/core/orchestrator.py—on_tool_start-Callback inchat_stream, Filler-Emission (ephemer)app/config.py/app/runtime_config.py—web_search_enabled(Default true, RUNTIME_SETTABLE + Nutzer-Pref-Opt-out)app/metrics.py— Zählertool_calls_total,sonar_calls_total- Filler-Texte je Sprache (Pool)