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

264 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.