Files
ARIA-AGENT/docs/plan-local-llm-router.md
T
duffyduckandClaude Opus 4.8 f93dd58a68 docs: Verschieben-Button reboot-sicher via Platzierungs-Config + dummem Reconcile-Agent
Button = remote start/stop = braucht Agent pro Host, aber dumme Variante:
gpu_placement.json als Single Source of Truth, Agent reconciled nur (Mensch =
Scheduler). Loest die Reboot-Falle (Laufzeit-Move vs statische Config). Deploy:
Code ueberall via git, Config entscheidet Platzierung. Nach B0/B1; Fallback =
COMPOSE_PROFILES manuell.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 11:45:56 +02:00

14 KiB
Raw Blame History

Plan B — Lokaler LLM-Router (Gamebox) neben Claude

Ziel: „Gemini-Feeling" für den Alltag, ohne die Claude-Max-Subscription aufzugeben. Ein schnelles lokales LLM beantwortet die einfachen ~80 % der Turns in <1 s; nur die schweren 20 % (Tiefe, Code, Tools, Pentest, langer Kontext) gehen an Claude. Claude bleibt das Tiefen-Hirn.

Warum das der einzige realistische Weg zu „live" ist

Gemessen (10.07.2026): CLI-Round-trip über den Claude-Max-Proxy hat einen harten Boden von ~3,5 s (Subprozess-Start pro Turn). Streaming-API würde das brechen, kostet aber API-Geld → verliert die Max-Subscription. Ein lokales LLM für die einfachen Turns umgeht den 3,5-s-Boden komplett und ist gratis (läuft auf vorhandener Gamebox-GPU). Echtes Speech-to-Speech-Duplex (Gemini Live nativ) ist mit einem Text-Modell als Hirn prinzipiell nicht drin.

Modell & Serving (entschieden)

  • Modell: Qwen3 8B, GGUF Q4_K_M (~6 GB). Bestes Tool-Calling der 7/8B- Klasse, solides Deutsch, Apache-2.0. Alt.: Mistral Small 3 7B (schneller, weniger Tool-Calling).
  • Serving: llama.cpp llama-server im Docker-Container auf der Gamebox (kein Ollama nötig — nativer OpenAI-kompatibler /v1/chat/completions).
  • VRAM-Budget: 12-GB-Karte, Whisper-small (~12 GB) + F5-TTS (~12 GB) → ~89 GB frei → passt. (FLUX ist auf 12 GB eh raus.)

Anbindung: über den RVS, wie TTS/STT (kein IP-Pflegen)

Die Gamebox ist ein anderer Host als das Brain. Statt direktem HTTP (IP/Port/ Firewall) läuft das LLM über den RVS-Token-Room, exakt wie Whisper/F5-TTS:

  • llama.cpp hört nur auf localhost der Gamebox.
  • Ein dünner RVS-Adapter daneben (Vorbild: whisper-/xtts-Bridge) verbindet sich mit dem RVS-Token, lauscht auf llm_request, ruft lokal llama-server, schickt llm_response (korreliert per requestId) zurück.
  • rvs/server.js ALLOWED_TYPES um llm_request, llm_response und (Phase 2) llm_partial erweitern.
  • Das Brain bekommt einen zweiten „Proxy" — nur über RVS statt direktem HTTP.

Router-Logik im Brain

Reihenfolge pro Turn (früh raus = schnell):

  • Tier 0 — Fast-Path (existiert): reine Steuerbefehle (Spotify, Licht) → Skill direkt, kein LLM. <1 s.
  • Tier 1 — Lokal (Qwen3): einfache Konversation, kurze Fakten, Smalltalk, Bestätigungen. Ziel <1 s.
  • Tier 2 — Claude: tief/technisch, Code, Tool-Use nötig, Pentest-Projekt, langer/komplexer Kontext.

Routing-Signal (heuristisch zuerst, deterministisch & schnell): Nachrichtenlänge, Schlüsselwörter, ob ein Tool nötig scheint, Projekt-Kontext (Pentest-Projekt → immer Claude), Konversationstiefe.

Escalation statt perfekter Vorab-Klassifikation: Das lokale Modell bekommt die Anweisung, bei Unsicherheit oder Tool-Bedarf NICHT zu raten, sondern zu eskalieren (z.B. Antwort <<ESCALATE>>). Das Brain routet den Turn dann an Claude. So sind Fehlklassifikationen billig — lieber einmal lokal→Claude als eine falsche lokale Antwort.

Modus „Nur lokales LLM" (Diagnostic-Checkbox, Eval-Schalter): Ein Flag localLlmOnly (in Diagnostic setzbar, vom Brain beim Routen gelesen). Ist es an: JEDER Turn geht ans lokale LLM, <<ESCALATE>> / „zu schwer" werden ignoriert (kein Claude-Fallback) — damit Stefan die echte Staerke/Schwaeche des lokalen Modells sieht, ohne dass Claude die schweren Turns rettet. Haken aus = normale Heuristik + Escalation. Ehrlicher Hinweis: im Nur-lokal-Modus funktionieren werkzeug-abhaengige Turns (Wetter, Timer, Memory, Bild) nicht — das lokale Tier hat keine Tools; das ist ein Gespraechs-Eval-Modus, kein Voll-ARIA. Fast-Path (Spotify etc.) laeuft davon unberuehrt weiter.

Persona auf BEIDEN Modellen

Das lokale Modell braucht ARIAs Identität, sonst bricht es aus der Rolle (gelernt aus dem --system-prompt-Debakel). Aber schlanker:

  • IDENTITY_SEED + Kern-Persona: ja.
  • Volles Memory / ALLE Skill-Schemas: nein — nur eine kuratierte, kleine Tool-Auswahl (siehe unten). Haelt den lokalen Prompt klein → schnell.
  • Persona kommt lokal auch als echter System-Prompt (llama.cpp system-Rolle).

Tool-Calling lokal (kuratierte Auswahl)

Das lokale LLM DARF Werkzeuge nutzen (Qwen3 = natives OpenAI-Tool-Calling, von llama.cpp --jinja unterstuetzt). Ablauf wie bei Claude: Brain schickt messages + tools → Qwen antwortet mit tool_calls → Brain fuehrt via _dispatch_tool aus → Ergebnis zurueck → finale Antwort. Tool-Loop im Brain, Ziel = lokales LLM statt Claude-Proxy.

Awareness ≠ Authority. Das lokale Modell soll WISSEN, was ARIA alles kann (damit es gezielt eskaliert statt zu halluzinieren), aber nicht alles ausfuehren.

Harte Grenze = Kontext/VRAM, nicht Misstrauen. Das volle Tool-Schema sind ~15-20 K Tokens. Qwens Kontext steht auf 8 K (LLM_CTX=8192) — es passt nicht rein. Hochdrehen auf 32 K kostet mehrere GB KV-Cache extra → OOM auf der geteilten 12-GB-3060 (Whisper + F5-TTS liegen mit drauf). Claude im RZ hat 200 K-1 M Kontext und ist zuverlaessig → kann sich das ganze Arsenal leisten; das lokale 8B auf Heim-Hardware nicht. Andere Hardware-Klasse, anderes Budget.

Design (gibt „im Bilde" ohne VRAM zu sprengen):

  • Ausfuehrbar lokal: kleiner, risikoarmer Start-Satz — Wetter, Uhrzeit, memory_search (lesen), trigger_timer, Spotify-Steuerung, Licht/Smart-Home.
  • Awareness-Liste (billig, ~paar hundert Tokens im System-Prompt): kurze Aufzaehlung des Rests — „ARIA kann ausserdem: Skills bauen, OAuth, Projekte, Bilder, ins Gedaechtnis schreiben — dafuer <<ESCALATE>>." Kein volles Schema.
  • Bleibt bei Claude (Authority): skill_create/update/delete, oauth_*, project_*, flux_generate, memory_save.

Escalation-Netz bleibt: braucht ein Turn ein Tool, das lokal nicht ausfuehrbar ist → <<ESCALATE>> → Claude mit vollem Arsenal. Der „Nur lokales LLM"-Haken dient dazu, spaeter datengetrieben zu messen, ob der ausfuehrbare Satz erweitert werden kann.

Implementierung (B1): Adapter reicht tools an llama.cpp + gibt tool_calls zurueck; Bridge schleust beides durch (llm_request/llm_response); Brain-Tool-Loop mit Ziel lokal.

Phasen

  • B0 — Infra: llama.cpp-Container + RVS-Adapter auf der Gamebox, ALLOWED_TYPES, local_llm_chat() im Brain. Isoliert testen („sag hallo").
  • B1 — Router + lokale Tools: Heuristik Tier-1/2 + Escalation, schlanke Persona lokal, kuratierte Tool-Auswahl lokal (Adapter/Bridge/Brain-Tool- Loop, siehe oben) + „Nur lokales LLM"-Checkbox. Einfache Turns → lokal. Messen: Trefferquote, Tool-Zuverlaessigkeit & Latenz.
  • B2 — Streaming/Voice: llm_partial → TTS beginnt beim ersten Satz → der „live"-Sprung. Hier den Gong-/Ohr-Re-Arm-Bug mit-fixen (Barge-In, sauberes Re-Listen).
  • B3 (optional): lokalen Tool-Satz erweitern, sobald Qwen sich als zuverlaessig erweist (z.B. memory_save).

Offene Entscheidungen (für Stefan)

  1. Modell: Qwen3 8B (Tool-Calling) — oder doch Mistral Small 3 7B (Speed)?
  2. Routing v1: rein heuristisch + Escalation (entschieden).
  3. Tools lokal: kuratierte kleine Auswahl (entschieden — Start-Satz oben; Stefan bestaetigt/justiert die konkrete Liste vor dem B1-Bau).

Folge-Baustein: Modell-Auswahl in ARIA Diagnostic (B0.5)

Ziel: In Diagnostic ein Modell auswählen; ist es nicht da, lädt der Container es on-demand und aktiviert es. Spiegelt zwei bestehende Muster: den whisperModel-Hotswap (RVS-Config-Broadcast → Bridge hot-swapped) und die kuratierte Claude-Tier-Liste aus models.json.

Kernproblem: llama.cpp-Server serviert ein Modell pro Prozess — „anderes aktivieren" = neu laden/swappen.

Lösung: llama-swap (Proxy vor llama.cpp): kennt eine Liste von Modellen, lädt bei Anfrage das gewünschte on-demand (Download via -hf beim ersten Mal), swappt bei VRAM-Knappheit das alte raus. OpenAI-kompatibel — der llm-adapter zeigt statt auf llama:8081 auf llama-swap.

Bausteine:

  • llama-swap-Service in xtts/docker-compose.yml (ersetzt/ergänzt llama), Config mit den verfügbaren Modellen (Name → -hf-Command).
  • Kuratierte Liste local_models.json (analog models.json) — Diagnostic-UI liest sie, zeigt Dropdown „Lokales Modell".
  • Diagnostic → RVS-Config-Broadcast localLlmModel → llm-adapter setzt das model-Feld seiner llama-swap-Requests → swap/Download passiert automatisch.
  • Status zurück an Diagnostic (lädt / bereit / VRAM-OOM), analog whisper-Status.

Konkret gewünschte UI (Stefan):

  • Modell-Status sichtbar: lädt (mit Fortschrittsbalken) → heruntergeladen → aktiviert. Ist ein Modell schon im Cache: nicht neu laden, nur aktivieren (llama.cpp/llama-swap macht das nativ ueber den Cache).
  • Testchat-Zeile in Diagnostic: kurze Nachricht direkt ans lokale LLM schicken, Antwort + Latenz anzeigen. Nutzt denselben RVS-Pfad (llm_request/llm_response) wie der Self-Test — kein neuer Kanal noetig.

Bis dahin: ein Modell via -hf Auto-Download (B0, erledigt). Erst end-to-end grün, dann dieser Komfort-Layer.

Skalierung: VRAM, Multi-GPU, „Cluster"

Wichtige Klarstellung: Roher VRAM/GPU ist NICHT ueber RVS teilbar. RVS ist ein Nachrichten-Relay; GPUs werden lokal per CUDA/PCIe angesprochen. Ueber RVS teilt man Inferenz-Faehigkeit (transkribiere/vervollstaendige), nicht VRAM. Es gibt daher keinen „GPU-Broker-Container", der Karten uebers Netz verleiht.

Skalierungspfade (echt):

  • Mehr Karten in EINER Box → VRAM-Pool. llama.cpp/vLLM splitten ein Modell ueber mehrere GPUs (--tensor-split). 2×3060 = 24 GB → groesseres Modell ODER Qwen8B mit grossem Kontext → volles Tool-Schema passt rein. Das ist der Weg zum „vollen Arsenal lokal".
  • Ein Modell ueber mehrere HOSTS splitten (llama.cpp --rpc): moeglich, aber langsam (Layer-Grenzen ueber's Netz) — nur schnelles LAN, fuer „schnell" ungeeignet. Nicht empfohlen.
  • Mehrere eigenstaendige Modell-Server, je einer pro GPU/Host, Router waehlt: einfach, = unser RVS-Muster. Zweiter GPU-Host = noch ein llm-adapter, meldet sich am RVS an, Router load-balanced. Das ist der sinnvolle „Cluster".
  • Innerhalb eines Hosts: ein geteilter Inferenz-Server (llama-swap/vLLM) statt VRAM-Duplikat pro Container — kommt mit B0.5.

Diagnostic ⓘ (Feature): Checkbox „volleres Arsenal" + Info-Icon mit VRAM-Bedarf: 12 GB (1×3060) = kuratierte Tools; 24 GB (2×3060, eine Box) = Qwen mit grossem Kontext/volles Schema oder groesseres Modell; Cluster = weitere GPU-Hosts als Modell-Server ueber RVS. (B0.5/B1-UI.)

„Waechter" / Orchestrator (Ausbaustufe, gestaffelt)

Idee: ein Dienst, der auf den am RVS angemeldeten Hosts Container startet/stoppt. Zerfaellt in zwei Teile:

  • Billig & bald nuetzlich — Registrierung + Heartbeat: jeder GPU-Host meldet dem RVS „lebe, GPUs, VRAM frei, laufende Dienste" (kleine Erweiterung der Adapter; whisper broadcastet schon Status). Nutzen: Diagnostic zeigt die Flotte (Live-Daten fuers ⓘ), Router weiss ob lokal erreichbar (sonst Claude).
  • Teuer & aufschiebbar — Steuerung (Container start/stop): Agent pro Host (Docker-Socket) + Controller mit Placement-Policy + Reconciliation + Broadcast-Kollisions-Vermeidung (nicht 2× dieselbe Faehigkeit). = Mini-Nomad.

Empfehlung: Fuer 2 Gameboxen NICHT bauen — statische Platzierung reicht (Gamebox1=LLM, Gamebox2=Voice). Dynamisches Laden/Entladen zum VRAM-Freimachen deckt llama-swap innerhalb eines Hosts (B0.5). Waechst die Flotte: erst den billigen Heartbeat-Teil; fuer echte Orchestrierung Docker Swarm / Nomad nehmen statt selbst einen Scheduler zu bauen.

ENTSCHIEDEN: manuelle Platzierung + read-only GPU-Dashboard (kein Auto)

Statt Auto-Controller (Semi-Auto verworfen — Host wechselt selten, Komplexitaet lohnt nicht):

  • Pin = Docker Compose Profiles. Services kriegen profiles: [...], jeder Host setzt COMPOSE_PROFILES=<seins> in der .env; docker compose up startet nur die eigenen. „In Config gepinnt", nativ, kein Code.
  • Verschiebe-Regel: up auf neuem Host + docker compose rm -sf <svc> auf altem (sonst holt restart: unless-stopped den Dienst beim Reboot zurueck → Broadcast-Kollision; Profile gelten nur beim up, nicht beim Daemon-Restart).
  • GPU-Dashboard in Diagnostic (read-only): jeder GPU-Host sendet periodisch einen Heartbeat via RVS (Host, GPU-Util, VRAM frei/belegt, laufende GPU-Container). Diagnostic zeigt pro Host VRAM-Balken + Dienste + „Host X hat N GB frei". Kein Start/Stop, nur Sicht + Hinweis wohin verschiebbar.
  • Zukunft (Gamebox3, 4×3060 = 48 GB): neuer Host, eigenes Profil, up → erscheint im Dashboard; grosses lokales LLM oder FLUX-Vollausbau dorthin. Ohne Orchestrator.

Verschieben-Button (Semi-Auto) — reboot-sicher via Platzierungs-Config

Wenn ein „Verschieben"-Button in Diagnostic gewuenscht ist (Dropdown Ziel-Host + Button = hier stoppen, dort starten), braucht das remote Container-Steuerung → kleiner Agent pro GPU-Host (Docker-Zugriff, hoert RVS-Befehle). Das ist der zuvor „teure" Teil, aber in der DUMMEN Variante:

  • Eine Platzierungs-Config ist Single Source of Truth: /shared/config/gpu_placement.json = {service: host}.
  • Dummer Reconcile-Agent pro Host: bei Start UND Config-Aenderung — starte die mir zugewiesenen Dienste, stoppe die anderen. Keine Policy, kein VRAM-Placement. Mensch = Scheduler (Button), Agent = befolgt nur Config.
  • Button aendert nur die Config → Agenten reconcilen (alt stoppt, neu startet). Reboot liest Config → kein Divergieren, keine Kollision.
  • Reboot-Falle vermieden: NIE Laufzeit-Move ohne Config-Update (sonst holt restart: unless-stopped den Dienst beim Reboot zurueck). Config = Wahrheit.

Deploy-Story: Code liegt via git auf allen Hosts (pull+build), aber up -d startet nichts GPU-maessig von selbst — die Platzierungs-Config (bzw. COMPOSE_PROFILES) entscheidet, was wo laeuft. Neuer Host = zuweisen, Agent startet.

Reihenfolge: NACH B0/B1. Fallback ohne Button: reine COMPOSE_PROFILES pro Host + Verschieben von Hand (null neue Infra).

Nicht-Ziele

  • Kein echter Gemini-Live-Duplex-Klon (Text-Modell als Hirn).
  • FLUX bleibt optional/später (dickere GPU). Bild-Generierung separat als pluggbarer Provider (ChatGPT/DALL·E-Alternative) — eigenes Feature, nicht Teil B.