Ctrl K
ComunidadGuías…

docs/novaberg-microservice-modell-queue_t.md

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

Archivo original ↗Preguntar a KiBot
# Novaberg — Microservice-Modell-Queue (Ausarbeitung)

**Teil 2 von 5 — Ausarbeitung.** Absicht und Kopfblock: [`novaberg-microservice-modell-queue_k.md`](novaberg-microservice-modell-queue_k.md) · Bauplan und Umstellung: [`novaberg-microservice-modell-queue_b.md`](novaberg-microservice-modell-queue_b.md) · Diskussion und Ergänzungen: [`novaberg-microservice-modell-queue_e.md`](novaberg-microservice-modell-queue_e.md) · Messungen: [`novaberg-microservice-modell-queue_m.md`](novaberg-microservice-modell-queue_m.md). Die Abschnittsnummern sind die des ungeteilten Konzepts; welche Datei einen Abschnitt trägt, sagt die Tabelle *„§ → Datei“* in `_k`.

---

## 4. Drei-Schichten-Architektur

### 4.1 Schicht 1 — Konsument

Konsumenten sind Nodes (Enricher, Responder, Thinker, Tribunal, Salienz, …), Agenten (RechercheAgent, CharakterAgent, PromotionAgent, …) und Tools (Embedding-Suche, Web-Recherche). Sie alle reden mit der Modell-Schicht über genau eine API:

```python
# Beispiel: Klassifikations-Aufruf eines Agenten

from microservices.model_service import model_service

response = await model_service.background.submit(
    prompt="Klassifiziere diesen Text: ...",
    system="Du bist ein Klassifikator. Antworte nur mit JSON.",
    expect_json=True,
)
# response.text → der LLM-Output (bereits JSON-validiert)
# response.parsed → Python-dict (falls expect_json=True)
```

Was der Konsument nicht weiß: welches Modell hinter `background` steckt (heute Qwen 3.6, morgen vielleicht ein anderes), dass `think=False` als Default greift, dass der CJK-Guard läuft, dass JSON-Repair-Versuche unternommen werden.

Was der Konsument optional sagen kann: `think=True` als Override, wenn er bewusst reasoning braucht; `num_ctx=8192` als Override, wenn er weiß, dass sein Prompt klein ist; `max_tokens=200`, wenn er die Antwort beschränken will.

### 4.2 Schicht 2 — Worker und Queue

Pro Modell existiert ein Worker, jeder mit seiner eigenen Queue. Die Queue ist eine `asyncio.Queue` — kein Redis, keine Persistenz, einfach in-process FIFO. Konsumenten reichen `ModelRequest`-Objekte mit angehängtem `Future` ein; der Worker zieht sequentiell ab, ruft das Modell an, schreibt das Ergebnis ins Future zurück.

```python
@dataclass
class ModelRequest:
    prompt: str
    system: Optional[str] = None
    overrides: dict = field(default_factory=dict)
    expect_json: bool = False
    future: asyncio.Future = field(default_factory=asyncio.Future)

class ModelWorker:
    """Bedient genau ein Modell. FIFO-Queue, sequentielle Abarbeitung."""

    def __init__(self, name: str, model: str, port: int, defaults: dict):
        self._name = name
        self._model = model
        self._port = port
        self._defaults = defaults  # think, num_ctx, top_p, repeat_penalty, ...
        self._queue: asyncio.Queue[ModelRequest] = asyncio.Queue()
        self._task: Optional[asyncio.Task] = None

    async def submit(self, **kwargs) -> ModelResponse:
        """Konsumenten-Schnittstelle: Anfrage eintragen, auf Future warten."""
        request = ModelRequest(**kwargs)
        await self._queue.put(request)
        return await request.future

    async def _run(self):
        """Worker-Schleife: FIFO abarbeiten, Antwort über Future zurück."""
        while True:
            request = await self._queue.get()
            try:
                response = await self._call_model(request)
                request.future.set_result(response)
            except Exception as exc:
                request.future.set_exception(exc)
            finally:
                self._queue.task_done()

    async def _call_model(self, request: ModelRequest) -> ModelResponse:
        """Hier leben Workarounds: Bug #15260, CJK-Guard, JSON-Repair."""
        params = {**self._defaults, **request.overrides}
        # ... Ollama-Call, Post-Processing, Validierung ...
```

Die Klasse ist absichtlich klein. Sie hat eine Verantwortung: zwischen Konsument-Absicht und Modell-Aufruf vermitteln, sequentiell, ohne dass Konsumenten sich gegenseitig in die Quere kommen.

~~Der Konkurrenz-Schutz auf `ollama_gpu_client` ergibt sich aus dem Worker-Pattern selbst, nicht aus expliziten Locks: solange jeweils nur ein Worker pro Modell läuft und der Worker FIFO arbeitet, kann es keine parallelen Calls am selben HTTP-Client geben.~~ Locks brauchen wir nur dort, wo zwei Worker denselben Endpoint anfassen — und das ist genau der Fall beim Embedding-Modell, das von Nova und Pixie geteilt wird (siehe Schicht 3).

> **Dieser Absatz hat den Defekt vorhergesagt und ist der Grund, warum er zwei Monate lief.** Die Bedingung *„solange jeweils nur ein Worker pro Modell läuft"* traf nicht zu: **ChatWorker und EmbedWorker hingen beide am `ollama_gpu_client`** — 2897 und 7407 Aufrufe in 42 Stunden, gemessen am 25.08.2026. Der Satz daneben nennt die Ausnahme genau richtig und **niemand hat sie gebaut**. `[gemessen]` — der ChatWorker sendete auf einer Verbindung, die der EmbedWorker 77 ms zuvor geöffnet hatte und noch benutzte. Kennung: `GPU-LOCK-SCHUETZT-EINEN-VON-FUENF`, behoben am 25.08.2026 durch `F-RIEGEL-1`: drei Ressourcen, drei Riegel, **drei Verbindungspools**.

### 4.3 Schicht 3 — Modell-Service

Hinter dem Worker liegt Ollama. Drei Modell-Instanzen, jeweils auf eigenem Port: `ollama-gpu` auf 11434 mit Gemma4 (Chat) und Nomic-Embed (Embedding); `ollama-cpu` auf 11435 mit Qwen 3.6 (Background).

Der Worker kennt seinen Port, seinen Modellnamen, und den HTTP-Client. Für ihn ist das Modell ein Endpoint, der einen Request bekommt und eine Response liefert. Alles dazwischen — Token-Streaming, Connection-Pool, HTTP-Timeouts — ist Implementierungs-Detail des Providers.

**Geteilte Modelle:** Embedding ist der Sonderfall. Sowohl Nova als auch Pixie greifen darauf zu. Architektonisch ist das transparent: Es gibt genau einen `embed`-Worker, beide rufen `model_service.embed.submit(...)` an, beide warten auf ihre Futures. Der Worker serialisiert. Welcher Konsument wartet, ist ihm egal.

### 4.4 Sequenz im Bild

```
┌─────────────┐   submit(prompt, ...)   ┌──────────────────┐
│  Konsument  │ ──────────────────────► │  Queue (FIFO)    │
│  (Node /    │                          │  asyncio.Queue   │
│   Agent)    │ ◄──── Future result ──── │                  │
└─────────────┘                          └────────┬─────────┘
                                                  │ get()
                                                  ▼
                                         ┌──────────────────┐
                                         │  ModelWorker     │
                                         │  • Defaults      │
                                         │  • Workarounds   │
                                         │  • JSON-Repair   │
                                         └────────┬─────────┘
                                                  │ HTTP
                                                  ▼
                                         ┌──────────────────┐
                                         │  Ollama          │
                                         │  (GPU oder CPU)  │
                                         └──────────────────┘
```

Drei Worker, drei Queues, drei Modelle, parallel zueinander. Die Konsumenten-Seite kennt nur die `submit`-Methode auf der jeweiligen Worker-Referenz.

### 4.5 Anmerkung zum Begriff „Microservice"

Im strikten Sinne sind Microservices eigenständige Prozesse mit Netzwerk-Kommunikation. Was wir bauen, ist eine **Microservice-Architektur in-process** — Service-Trennung, Rollen-Abstraktion und Queue-Vermittlung, aber alles in einem Python-Prozess. Wir benutzen den Begriff trotzdem, weil er die Trennung gut transportiert: Konsumenten und Worker sind Services, die nichts voneinander wissen müssen. Falls sich das System später auf mehrere Prozesse aufteilt — etwa um GPU-Aufrufe in einen eigenen Subprozess zu verlagern —, ist die Schnittstelle so vorbereitet, dass aus `await queue.put(...)` ein `await ipc.send(...)` werden kann, ohne dass Konsumenten davon merken.

---

## 5. Rollen-Katalog

Drei Rollen, drei Worker, drei Queues. Die Rolle ist die abstrakte Identität; das Modell ist Worker-internes Wissen.

| Rolle | Worker-Modell heute | Hardware | Konsumenten |
|-------|---------------------|----------|--------------|
| `chat` | `gemma4-gpu` (Gemma4 26B/3.8B aktiv, 32k ctx) | GPU 11434 | Responder, Thinker, Tribunal, GV-Node, Corrector, alle Nova-Chat-Agenten |
| `background` | `qwen36-cpu` (Qwen 3.6-35B-A3B, 32k ctx) | CPU 11435 | PromotionAgent, RechercheAgent, CharakterAgent, alle Pixie-Agenten, alle Klassifikations- und Destillations-Calls |
| `embed` | `nomic-embed-text` | GPU 11434 | Enricher, Salienz, KZG-Persist, Embedding-basierte Retrieval-Tools — sowohl Nova als auch Pixie |

**Eine Rolle ist kein `think`-Modus.** Der Default für `background` ist `think=False`, weil Klassifikation und Destillation den Workaround für Ollama Bug #15260 brauchen. Aber das ist eine Worker-Voreinstellung, kein Bestandteil der Rolle. Wer wirklich reasoning braucht — heute ist das in erster Linie der Thinker im Chat-Pfad — kann `think=True` als Override mitgeben. Die Politik dahinter ist Sache des Konsumenten, nicht der Rolle.

**`background` ersetzt zwei alte Modelle.** Heute laufen `gemma4-cpu` (für Sprache) und `qwen3-32b-cpu` (für Analyse) parallel auf CPU. Mit der Verifikation aus Chat 91 wissen wir: Qwen 3.6-35B-A3B kann beides — Sprache und Analyse — mit deutlich besserer Hardware-Last (51 % statt 90 %+, 62 °C statt Abschaltungen). Die Trennung in zwei CPU-Provider war eine Notlösung, die wir loswerden. Nach der Migration zeigen `_background_provider` und `_background_analyse_provider` auf dasselbe Modell. Mittelfristig fallen die zwei Variablen zusammen.

**Pixie nutzt nachts keine GPU.** Die alte Überlegung, Pixie nachts auf die GPU zu lassen, ist mit Qwen 3.6 obsolet. Pixies Hauptarbeit ist Reasoning und Analyse — das geht jetzt komfortabel auf CPU. Wenn Pixie eine Sprach-Ausgabe braucht (etwa für Recherche-Erkenntnisse, die in Novas Stimme formuliert werden müssen), schreibt sie das Ergebnis in eine Output-Queue, die in die Nova-Pipeline einläuft; dort übernimmt der `chat`-Worker auf GPU. Das ist der **Pixie-Output-Queue-Pfad**, der unabhängig zur Pixie-Graph-Merge-Architektur (Pfad 3, Chat 79) im Backlog steht.