Ctrl K
ComunidadGuías…

docs/novaberg-architecture_l_connector.md

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Lesson: Connector-Segregation — Modellunabhaengigkeit durch Textdateien

**Projekt:** Novaberg — The Nova Anima Resonance System
**Dokument:** Lesson — Connector-Segregation
**Stand:** 8. November 2026, 14:10 UTC (`date -u`; §12 neu — Qwen 3.8 antwortet im Gespräch über den Connector `qwen38`, und er bleibt; §9 mit Vermerk). Davor 5. September 2026, 20:28 UTC (§7 neu — die Modellebene haengt seither am **antwortenden** Modell, nicht mehr am konfigurierten GPU-Modell, und ein Modellname mit Schraegstrich wird flach gemacht). Davor 23. August 2026 (die Prompt-Segregation hat eine Modellebene bekommen — ~~der Gespraechspfad haengt am GPU-Modell~~, siehe §7; davor: 15. April 2026)
**Pfad:** novaberg/docs/novaberg-architecture_l_connector.md
**Kontext:** Migration von Mistral Small 3.2 auf Google Gemma 4 26B MoE

---

## 1. Ausgangssituation

Novaberg lief auf Mistral Small 3.2 (24B Dense). Google Gemma 4 26B MoE versprach deutliche Verbesserungen: 2.5x schneller (nur 3.8B aktive Parameter pro Token), doppeltes Kontextfenster (32768 statt 16384), explizite Optimierung fuer Agentic Workflows.

Problem: Ein Modellwechsel betrifft jeden Prompt in jedem Node. Prompts die fuer Mistral optimiert sind, funktionieren nicht zwingend fuer Gemma4 — anderes RLHF-Conditioning, andere JSON-Faehigkeiten, andere Thinking-Mechanismen. Ohne Segregation waeren Mistral-Prompts bei jedem Gemma4-Tuning ueberschrieben und unwiederbringlich verloren.

## 2. Loesung: Zwei Schichten

### Schicht 1 — Modell-Connector (config.py)

Ein Dict definiert Modell-Profile. Ein Env-Schalter waehlt das aktive Profil. Die bestehenden Variablen werden beim Start aufgeloest:

```python
OLLAMA_CONNECTORS = {
    "mistral": {"gpu_model": "mistral-small3.2-gpu", ...},
    "gemma4":  {"gpu_model": "gemma4-gpu", ...},
}
_connector = OLLAMA_CONNECTORS[OLLAMA_CONNECTOR]
OLLAMA_MODEL = _connector["gpu_model"]
```

Kein Node-Code aendert sich. Umschalten: `OLLAMA_CONNECTOR=mistral` in .env, Neustart.

### Schicht 2 — Prompt-Segregation (prompts/)

Statische Prompt-Bloecke als Textdateien. Default-Verzeichnis + Override-Verzeichnisse:

```
prompts/
  default/        ← 91 Bloecke (funktioniert mit jedem Modell)
  gemma4-gpu/     ← 7 Overrides (verschaerfte JSON-Regeln)
```

Ein Loader liest beim Start alle Defaults und ueberschreibt sie. Dictionary auf `PROMPTS` in config.py. Nodes greifen ueber `PROMPTS["router.rules"]` zu.

> **Die Segregation nach Connector war für Gesprächs-Blöcke die falsche Größe, und das ist am 23.08.2026 gemessen worden.** Der Gesprächspfad hängt am **GPU-Modell**; zwei der drei Connectoren fahren dort dasselbe (`gemma4` und `qwen36` beide `gemma4-gpu`). Die sieben Overrides lagen deshalb unter dem aktiven Connector `qwen36` still, während Gemma4 antwortete — im Betriebslog als *„Keine Overrides fuer Connector 'qwen36'"* nachlesbar, und diese Null sah aus wie *nichts zu tun*.
>
> **Seither drei Ebenen:** `default` → `{gpu_model}` → `{connector}`. Der Connector bleibt die letzte, weil er der **engere** Schlüssel ist: Zwei Connectoren teilen sich ein Modell, kein Modell einen Connector. Für Hintergrund-Blöcke ist er weiterhin die richtige Ebene — dort unterscheiden sich die Connectoren wirklich (`cpu_model`).
>
> **Das Prinzip aus §4 trägt unverändert**, es hatte nur eine Ebene zu wenig. Ein Verzeichnis `mistral/` hat es übrigens nie gegeben; die Zeile stand hier, weil sie zur Symmetrie passte.

## 2a. Was von den Overrides trägt — gemessen statt vermutet

Die sieben Gemma4-Blöcke stammen vom April 2026 und tun zwei verschiedene Dinge. Am 23.08.2026 direkt gegen Ollama gemessen, drei Tribunal-Blöcke × 12 Läufe je Fassung, `think=False`, temperature 0.2 (`labor/2026-08-23_prompt_overrides_wirkung.py`):

| | Override | Default |
|---|---|---|
| **mit** `format="json"` | 36/36 gültiges JSON | **36/36** |
| **ohne** `format` — die Kontrolle | 36/36 | **0/36** |

**Die Kontrolle ist der Teil, der die Messung trägt.** Ohne sie hieße „kein Unterschied" auch: „die Sonde sieht nichts". Mit ihr steht fest, dass die sechs Formatzeilen genau das leisten, wofür sie gebaut wurden — und dass `format="json"` dasselbe strukturell erzwingt.

**Sie bleiben trotzdem.** Eine Redundanz, die im Fehlerfall trägt, ist keine: Käme ein Aufrufer ohne `expect_json` hinzu, ist der Unterschied 36 zu 0.

**Die Thinking-Zeile ist entfernt.** *„Halte deine internen Ueberlegungen unter 100 Tokens"* stand in 7 von 7 Overrides und 0 von 91 Defaults. Sie stammt aus einer Zeit, in der `think` kein Feld der Anfrage war — die Klasse entstand erst am **20.05.2026**, fünf Wochen später. Heute setzen alle verbrauchenden Knoten `think=False`, und Ollama liefert dann kein Denkfeld; im Betriebslog kommt `<think` in **0 von 31** Dateien vor.

Ihre gemessene Wirkung traf deshalb den **sichtbaren** Text: Vor dem Entfernen war der Override in **3 von 3** Blöcken kürzer als der Default, danach in **1 von 3**. Der Vorzeichenwechsel ist die Aussage, nicht der Mittelwert — die Default-Fassung schwankte zwischen zwei Läufen bei unverändertem Prompt selbst um 7 %, und das ist die Rauschgrenze.

## 3. Warum Textdateien statt YAML/JSON?

Die Diskussion in Chat 45 war kurz:

> "Ob das jetzt YAML ist oder JSON oder einfach ASCII-Text, das ist doch sekundaer."

Und genau so ist es. Ein Prompt ist ein String. Eine Textdatei enthaelt einen String. Kein Parser, kein Schema, kein Framework noetig. `open(datei).read().strip()` — fertig. Die Einfachheit ist das Feature.

## 4. Prinzip: Default + Override

Das Pattern ist bewusst analog zu CSS-Kaskadierung oder Linux-Konfiguration (/etc/default + /etc/override):

1. Default funktioniert immer (Baseline)
2. Override aendert nur was noetig ist (Delta)
3. Fehlendes Override = Default gilt

Das bedeutet: Ein neuer Connector braucht nur die Dateien die abweichen. Fuer Mistral null Dateien, fuer Gemma4 sieben. Wenn ein dritter Connector kommt (z.B. Llama 4), braucht er nur seine spezifischen Overrides.

## 5. Ergebnis

| Vorher | Nachher |
|--------|---------|
| Prompts als Python-Konstanten in 15+ Dateien | Prompts als Textdateien in einem Verzeichnis |
| Modellwechsel = Code-Aenderungen in jedem Node | Modellwechsel = 1 Env-Variable |
| Prompt-Tuning ueberschreibt Originale | Prompt-Tuning in separatem Override-Verzeichnis |
| Nicht i18n-faehig | Verzeichnisstruktur ist i18n-ready |

## 6. Anwendbarkeit

Das Pattern ist universell fuer jedes System das mit wechselnden LLM-Backends arbeitet. Die Kosten (Textdateien lesen, Dictionary fuellen) sind vernachlaessigbar. Der Gewinn (Modellunabhaengigkeit, verlustfreies Tuning, einfache Erweiterbarkeit) ist permanent.

---

## 7. Die Modellebene haengt am antwortenden Modell (05.09.2026)

**Der Satz im Kopf dieses Dokuments war eine Vereinfachung, die genau so lange trug, wie
jedes Backend ein lokales war.** Die Modellebene wurde mit `OLLAMA_MODEL` geschluesselt —
dem **konfigurierten** GPU-Modell des Connectors. Welches Modell tatsaechlich antwortet,
entscheidet aber `MODEL_WORKER_BACKENDS["chat"]`.

Solange dort ein Ollama-Backend stand, waren beide Werte gleich. Mit dem ersten fremden
Rueckhalt faellt das auseinander: Das neue Modell bekaeme die Bloecke unter
`prompts/gemma4-gpu/` — Regeln, die fuer ein anderes Modell geschrieben sind.

`config.antwortendes_chat_modell()` loest den Namen seither ueber `MODELL_NACH_BACKEND`
aus dem Backend auf. Bei `ollama_gpu` liefert es unveraendert `OLLAMA_MODEL`; ein Zeuge
haelt diese Gleichheit fest, damit ein Abschalten nicht wie eine Umstellung aussieht.

### Ein Modellname kann einen Schraegstrich tragen

Ein Fernmodell nennt den Anbieter mit: `deepseek/deepseek-v4-flash-0731`. Unveraendert in
den Pfad eingesetzt ergaebe das ein **verschachteltes** Verzeichnis, und das Zwischenglied
`prompts/deepseek/` waere eine Ebene, die niemand nachschlaegt. Der Lader macht den Namen
deshalb flach (`/` → `_`); gesucht wird unter
`prompts/deepseek_deepseek-v4-flash-0731/`.

**Gefunden hat das ein Zeuge, der seit dem 23.08.2026 steht** — *„ein Verzeichnis, das
keine Ebene je nachschlaegt, ist totes Material"*. Seine Menge der erreichbaren Namen kam
aus `OLLAMA_CONNECTORS` und damit **nur von der eigenen Maschine**; sie kennt jetzt auch
die Fernmodelle.

> **Die Ebene ersetzt einen Block, sie ergaenzt ihn nicht.** Wer einen Block ueberschreibt,
> um einen Absatz zu aendern, uebernimmt die Pflege aller uebrigen —
> `tests/test_prompt_override_deckung.py` haelt fest, dass jedes Absatz-Stichwort des
> Defaults im Override wiederkehrt.

---

→ Architektur: novaberg-architecture.md §2.1.1
→ Connector-Dict: config.py OLLAMA_CONNECTORS
→ Modell je Backend: config.py MODELL_NACH_BACKEND
→ Loader: server/prompt_loader.py

---

## 8. Was jeder Anbieter fuehrt — gemessen am 07.09.2026

**Der Kanon `OPENROUTER_GEFUEHRTE_PARAMETER` in `config.py` stand von Hand fuer *einen*
Anbieter da.** Er stammt aus `GET /models/{id}/endpoints`, und dieselbe Quelle beantwortet
die Frage fuer **alle** — `labor/werkzeug/anbieter_faehigkeiten.py` liest sie aus.

`[gemessen]` ueber **29 Anbieter** desselben Modells. Die Auskunft ist schaerfer als die
Preistabelle:

| Anbieter | in/M | out/M | cache/M | `repetition_penalty` |
|---|---:|---:|---:|---|
| DeepInfra | 0,060 | 0,180 | 0,015 | ja |
| CoreWeave | 0,130 | 0,280 | 0,070 | ja |
| Parasail | 0,140 | 0,280 | 0,050 | ja |
| **Baidu** | 0,140 | 0,280 | 0,028 | **nein** |

> **Der genutzte Anbieter fuehrt keine der drei Sampling-Schrauben** — weder
> `repetition_penalty` noch `presence_penalty` noch `frequency_penalty`. Responder und
> Verfasser setzen zwei davon gegen Wiederholungen; sie gehen ins Leere. Der Kanon faengt
> das ab, damit sie nicht stumm verworfen werden — **aber die Wirkung fehlt trotzdem**, und
> das ist ein Grund zum Anbieterwechsel unabhaengig vom Preis.

**Der Preis war der laute Grund, nicht der einzige.** Am 07.09.2026 fiel der Rabatt weg:
konfiguriert $0.04998/$0.09996 je Million, lebend $0.14000/$0.28000 — **Faktor 2,80**.

## 9. Warum die Modellwahl an der **aktiven** Parameterzahl haengt

**`ollama show` nennt die Gesamtzahl, nicht die aktive** — und bei einem MoE entscheidet die
aktive ueber den Durchsatz. `[gemessen 07.09.2026]` auf der eigenen Maschine:

| Modell | Bauart | aktiv | Durchsatz |
|---|---|---:|---:|
| `gemma4-gpu` (GPU) | MoE 26B | 3,8 Mrd. | **96,4 tps** |
| `gemma4-a4b-gpu` (GPU) | MoE 26B | 4 Mrd. | **99,3 tps** |
| `qwen36-cpu` (CPU) | MoE 35B | 3 Mrd. | **18,3 tps** |

**Ein dense-Modell derselben Groessenordnung waere um ein Vielfaches langsamer.** Qwen 3.8
27B ist dense — 27 Mrd. aktiv, also rund neunmal die Rechenarbeit von 3 Mrd.; auf der CPU
hochgerechnet **rund 2 tps**, und ein `pixie/hash`-Lauf ginge von 22 s auf etwa 200 s. Die
MoE-Variante desselben Hauses (Flash-Next, 512 Experten, 10 aktiv) traegt die richtige
Bauart und **103 GB** — bei 61 GB Arbeitsspeicher ausser Reichweite.

> **Vermerk vom 08.11.2026:** Die Rechnung gilt für die CPU und bleibt. Auf der **GPU** antwortet
> `qwen3.8:27b` seit dem 07.11.2026 im Gespräch (§12) — in freiem Text mit **35,6 tps** gegen
> 100,3 tps bei `gemma4-a4b-gpu` (`labor/2026-11-07_qwen_ergebnis.md`, T3).

> **Der Ort fuer ein klugeres und langsameres Modell ist `analyse_model`**, das der Connector
> ohnehin getrennt fuehrt. Die Destillation laeuft taeglich, nicht je Turn; dort sind 200 s
> belanglos — und dort sitzen die Defekte, die ein besseres Modell beheben wuerde
> (`PROFIL-VERALLGEMEINERT-EINZELBELEG`, unbelegte Zitate).

## 10. Der Prefix-Cache — gemessen, nicht konfiguriert

`llama.cpp` haelt den KV-Cache eines wiederkehrenden Prompt-Anfangs von selbst.
`[gemessen 07.09.2026]`, derselbe Prompt dreimal:

| | kalt | warm | Faktor |
|---|---:|---:|---:|
| GPU (1934 Token) | 787,7 ms | 88 ms | **8–9** |
| CPU (1915 Token) | 11,61 s | 0,09 s | **115** |

**Auf der CPU wiegt er das Vielfache**, weil dort das Lesen teuer ist — und genau dort laeuft
der Hintergrund. Auf `pixie/hash` hochgerechnet (5418 ein / 403 aus): **72,8 s** mit
entladenem Modell, **54,9 s** geladen mit kaltem Prompt, **22,3 s** warm.

**Zwei Stellschrauben sind unkonfiguriert:** `OLLAMA_KEEP_ALIVE` ist nicht gesetzt, es gilt
der Default von fuenf Minuten — **jeder Kaltstart kostet 17,9 s Ladezeit** auf der CPU.
`OLLAMA_KV_CACHE_TYPE` steht auf `f16`; `q8_0` halbiert den KV-Speicher bei kaum messbarem
Verlust. Der Cache selbst braucht keine Einstellung, sondern eine Eigenschaft der Prompts:
**das Stabile muss vorn stehen.**

## 11. Der Cache ist jetzt sichtbar — und er trifft nicht

**Seit dem 07.09.2026 traegt jede Verbrauchszeile die Lesezeit des Prompts.**
`prompt_eval_duration` stand in jeder Ollama-Antwort und wurde nirgends gelesen; erfasst war
allein `prompt_eval_count`, und **die Tokenzahl ist bei kaltem und warmem Prefix dieselbe.**
Neu sind `prompt_lesen_s` und `prompt_lesen_tps` in der `token`-Zeile.

> **Dabei fiel eine zweite Luecke auf: `record_usage` hatte genau einen Aufrufer, und der lag
> im Fern-Weg.** Jede Verbrauchszeile des Bestands stammt von OpenRouter; der lokale Pfad
> buchte nicht. Ein Kostenvergleich lokal gegen fern war damit unmoeglich, ohne dass es
> jemandem auffiel.

**Erster Betriebsbeleg, zwei Aufrufe eines Turns:**

| Knoten | Token | Lesen | Rate |
|---|---:|---:|---:|
| `salienz` | 2177 | 0,847 s | **2 570 tps** |
| `perzeption` | 1442 | 0,579 s | **2 489 tps** |

**Beide liegen in der kalten Groessenordnung** (§10: GPU kalt 2 455 tps, warm 19 000 bis
22 000). Der Cache trifft nicht — und der Grund steht in der Aufruferliste: **neun
verschiedene Prompt-Typen teilen sich ein Modell**, bei `OLLAMA_NUM_PARALLEL=2` also zwei
Slots. Sie verdraengen einander.

**Was daraus folgt, ist eine Architekturfrage und keine Einstellung.** Entweder mehr Slots —
jeder haelt einen weiteren Prefix, aber jeder wird kleiner — oder ein **gemeinsamer stabiler
Prompt-Kopf** ueber alle Aufrufer, der von allen geteilt wird. **Die Zahlen dafuer stehen
jetzt im Bestand**; vorher waere die Entscheidung geraten worden.

## 12. Qwen 3.8 im Gespräch — der Connector `qwen38` (07./08.11.2026)

**Seit dem 07.11.2026, 11:06 UTC antwortet `qwen3.8:27b`.** Der Connector `qwen38` (committet als `b32a37a`) unterscheidet sich von `gemma4a4b` in einer Zeile, dem GPU-Modell; Fenster (32768), CPU- und Analysemodell (`qwen36-cpu`, 262144) bleiben. Der Wechsel bewegt damit eine Größe, und der Vergleich der beiden bleibt trennbar.

> **Der Name folgt hier dem Modell, das antwortet** — anders als bei `qwen36`, das nach seinem CPU-Modell heißt und im Gespräch `gemma4-gpu` fährt (§2 und der Kommentar über `OLLAMA_CONNECTORS`). Wer `qwen38` setzt, bekommt Qwen; wer `qwen36` setzt, bekommt Gemma.

| Wo | Wert |
|---|---|
| Betrieb, Umgebung des Servers | `OLLAMA_CONNECTOR=qwen38`, seit 07.11.2026, 11:06 UTC |
| Rückweg | `OLLAMA_CONNECTOR=gemma4a4b`, ohne Code-Änderung |
| Vorgabe im Code (`config.py`) | `qwen36` — **nicht umgestellt**; ohne Umgebungsvariable antwortet `gemma4-gpu` |
| Vorlage `docker-compose.template.yml` | `qwen36` |

**Ob die Vorgabe im Code auf `qwen38` geht, ist offen.**

**Was mitzog und was nicht:**

- **Die Prompt-Ebenen** (§2, §7): Für `qwen3.8:27b` gibt es weder ein Verzeichnis der Modell- noch eines der Connector-Ebene; es gelten die Defaults, wie schon unter `gemma4-a4b-gpu`. Die sieben Blöcke unter `gemma4-gpu/` greifen nur, wenn `gemma4-gpu` antwortet `[Code]`.
- **Der Split-Abfang des Thinkers** (`tools/thinking_normalizer.py`, `_MODELLE_MIT_SPLIT`): Die Stämme sind seit `b32a37a` `gemma4` und `qwen3.8`. Ob `qwen3.8:27b` unter `think=True` den Split zeigt, ist **nicht gemessen**; gesetzt ist der Abfang, weil er nur bei leerem Inhalt mit gefülltem Denken handelt und sonst nichts tut. Der kürzere Stamm `qwen3` träfe `qwen36-cpu` und `qwen3-32b-cpu` mit.
- **Die Kosten:** `LOKALE_MODELLE` wird aus der Connector-Tabelle abgeleitet; `qwen3.8:27b` kostet null, ohne eigene Zeile.
- **Die Zeugen:** 14 in `tests/test_qwen38_connector.py` halten den Eintrag, die Kosten, den Weg von der Umgebung zum antwortenden Modell in beide Richtungen und die Wahl des Abfangs fest.

**Die Gründe — die Reihe vom 07.11.2026**, 08:49–09:48 UTC, direkt am Modell, ohne Server und ohne Turn (`labor/2026-11-07_qwen_ergebnis.md`):

| Reihe | `gemma4-a4b-gpu` | `qwen3.8:27b` |
|---|---|---|
| T1 — Fehlalarme mit Eintrag (Reihe E) | 2 von 4 | 0 von 4 |
| T1 — Tor-Treffer, Reihen (a) mit 28 und 32 Sätzen | 26 von 28, 30 von 32 | 28 von 28, 32 von 32 |
| T1 — Ausfälle der Sachlage, 65 Sätze | 2 | 0 |
| T2 — Feldwerte der Perzeption außerhalb des Kanons | 5 von 96 | 0 von 96 |
| T3 — Durchsatz in freiem Text | 100,3 tps | 35,6 tps |
| T0 — Fenster 32768 mit Embedding-Runner auf der GPU | 100 % | 100 %, gefüllt nur gestuft |

**Die Sollwerte der Reihe sind erfüllt; der Preis stand nicht im Soll.** Qwen schreibt freien Text knapp dreimal langsamer, liest einen vollen Prompt rund 3,6-mal langsamer und läuft wärmer. Über das Verhalten im Gespräch sagt die Reihe nichts — kein Turn, kein Systemprompt in T3.

**Entschieden am 08.11.2026, vom Meister:** *„Ich denke, Qwen bleibt, Gemma ist von der Leistung einfach viel zu veraltet“*. `qwen38` bleibt im Betrieb, `gemma4a4b` der Rückweg.