Phase 8: Lokale KI

Wiki mit semantischer Suche ueber pgvector, Diktat ueber faster-whisper,
Berichtsassistent und Belegerfassung — alles gegen selbst betriebene
Modelle. Dazu zwei Protokollansichten: was die Maschine getan hat und was
Menschen getan haben.

Nichts, was ein Modell erzeugt, gelangt ungeprueft in einen Nachweis. Jede
Ausgabe ist zuerst ein eigener Datensatz, gekennzeichnet, mit zwei gleich
grossen Knoepfen davor. Die Datenbank haelt gegen: Eingabe, Ausgabe und
Modell sind unveraenderlich, geloescht wird nicht, entschieden wird einmal.

Kein Rueckfall auf einen Anbieter: fehlen die Container, fehlt die Funktion.
Dafuer bleibt alles andere ohne Modell vollstaendig benutzbar — geprueft von
vierzehn E2E-Tests, die ohne laufendes Modell durchlaufen.

Postgres bringt jetzt PostGIS und pgvector mit (eigenes Image), Ollama und
faster-whisper stehen im Compose-Profil "ki".

73 neue Unit-Tests, 14 neue E2E-Tests, Version 0.9.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-09-04 11:06:03 +02:00
co-authored by Claude Opus 5
parent 12243f177d
commit f7ccd9a908
134 changed files with 8624 additions and 72 deletions
+193 -2
View File
@@ -31,7 +31,7 @@ Anmelden mit `SEED_ADMIN_EMAIL` und `SEED_ADMIN_PASSWORT` aus der `.env`
| Phase 5 — Atemschutz (FwDV 7) | fertig |
| Phase 6 — Kartenmodule | fertig |
| Phase 7 — Benachrichtigungen und Termine | fertig |
| Phase 8 — Lokale KI | offen |
| Phase 8 — Lokale KI | fertig |
| Phase 9 — Kassenverwaltung | offen |
Vollständige Planung mit Aufwänden: [docs/ROADMAP.md](docs/ROADMAP.md) und
@@ -973,6 +973,188 @@ Meldungen bleiben einfach aus. Beide Schlüssel gehören deshalb ins Backup.
---
## Phase 8 — Lokale KI
Ein Satz bestimmt alles, was hier gebaut wurde:
> **Nichts, was ein Modell erzeugt, gelangt ungeprüft in einen Nachweis.**
Daraus folgt der Aufbau: jede KI-Ausgabe ist zuerst ein eigener Datensatz und
nicht ein Feld im Zieldokument. Ein Einsatzbericht bekommt seinen Text erst,
wenn ein Mensch „übernehmen" gedrückt hat — bis dahin liegt der Entwurf daneben
und ist als Entwurf erkennbar, mit gestricheltem Rahmen und dem Modellnamen
darüber. Das ist der Unterschied zwischen einer Hilfe und einer Fälschung.
Der zweite Grundsatz: **es läuft alles im Haus.** Ollama und faster-whisper als
Container daneben, kein Anbieter, kein Schlüssel, keine Zeile Text, die das
Gerätehaus verlässt. Es gibt **keinen Rückfall** auf einen Cloud-Dienst: fehlen
die Container, fehlt die Funktion. Das ist die einzige Bauart, bei der der Satz
„die Daten bleiben im Haus" auch dann noch stimmt, wenn der eigene Server aus
ist.
### Ohne Modelle geht alles andere weiter
Die wichtigste Eigenschaft der Phase, und die am meisten Arbeit gemacht hat:
**FLORIAN bleibt ohne Sprachmodell vollständig benutzbar.** Wiki lesen und
schreiben, Wortsuche, Belege ablegen und von Hand ausfüllen, beide Protokolle —
alles läuft. Wo etwas fehlt, steht ein Satz, der sagt *was* fehlt und *wie es
ohne geht*, statt eines kaputten Knopfes.
Das ist kein Zugeständnis, sondern der Normalfall: eine Wehr setzt FLORIAN auf,
lange bevor sie über eine Grafikkarte nachdenkt.
### Das Wiki
Dienstvorschriften, Ausbildungsunterlagen, Merkblätter — das, was sonst als PDF
im Ordner steht und im Einsatz niemand findet. Inhalt ist **Markdown**, weil es
sich ohne Werkzeug lesen lässt, auch in zwanzig Jahren, auch ohne FLORIAN.
**Zwei Suchen nebeneinander**, und beide werden gebraucht:
| Suche | Findet | Braucht ein Modell |
|---|---|---|
| **Wortsuche** | was dasteht — „FwDV 7" findet die Seite mit dieser Zeichenfolge | nein |
| **Frage** | was gemeint ist — „wie lange hält ein Schlauch" findet den Abschnitt über Ablegereife | ja |
Die Frage wird **ausschließlich aus den eigenen Seiten** beantwortet, mit
Fundstellen darunter. Steht die Antwort nicht drin, sagt FLORIAN genau das. Ein
Modell, das auf die Frage nach der Prüffrist von Steckleitern eine Zahl nennt,
die nirgends steht, wäre gefährlicher als gar keine Antwort — deshalb wird ohne
passende Auszüge gar nicht erst gefragt.
Zerlegt wird an den **Überschriften**, nicht nach Zeichenzahl: eine Fundstelle
soll ein Kapitel sein, kein aus der Mitte gerissener Absatz. Und ein kurzer
Abschnitt wird nie unter eine fremde Überschrift gehängt — sonst zitierte die
Antwort eine Fundstelle, in der der Satz gar nicht steht.
Jede Änderung schreibt die vorherige Fassung weg. Eine Dienstvorschrift, die
sich unbemerkt ändert, ist schlimmer als keine.
### Diktat
Der Weg, den es in jeder Wehr schon gibt: der Einsatzleiter spricht auf der
Rückfahrt in sein Telefon, und daraus wird abends ein Bericht. Neu ist nur, dass
abends schon ein Text dasteht.
Ein Knopf, keine Einstellungen. Die Aufnahme bleibt abgelegt, solange über den
Text nicht entschieden ist — wer dem Transkript nicht traut, hört nach. Und das
Modell darf **nichts verbessern**: „Hohlstrahlrohr" bleibt „Hohlstrahlrohr",
auch wenn es das Wort nicht kennt.
### Berichtsassistent
Stichpunkte hinein, Entwurf heraus. Was das Modell **nicht** bekommt, ist ebenso
wichtig: Zeitstempel und Einsatznummer stehen schon als Felder im Bericht und
stünden sonst zweimal da, beim zweiten Mal womöglich falsch.
Der Entwurf landet nicht im Berichtsfeld. Er steht daneben, gekennzeichnet, mit
zwei gleich großen Knöpfen — „Übernehmen" ist nicht der Standardweg und sieht
auch nicht so aus.
### Belegerfassung
Quittung fotografieren, Angaben prüfen, gegenzeichnen. Alle Felder sind
änderbar, und was das Modell nicht erkannt hat, bleibt **leer und gelb
hinterlegt** statt geraten zu werden. Ein leeres Feld ist eine Aufforderung; ein
geratener Betrag ist eine falsche Buchung, die niemand bemerkt.
Die Buchung daraus kommt mit der Kasse in Phase 9 — hier entsteht erst einmal
ein sauber erfasster Beleg mit Foto.
### Zwei Protokolle nebeneinander
| Ansicht | Beantwortet |
|---|---|
| **KI-Protokoll** | Was hat die Maschine getan? Jede Befragung, jedes Modell, jede Entscheidung mit Namen und Zeitpunkt. |
| **Protokoll** (Audit-Log) | Was haben Menschen getan? Wird seit Phase 0 geschrieben; die Ansicht dazu kam jetzt. |
Eingabe, Ausgabe, Modell und Art eines KI-Vorgangs sind nach dem Anlegen
**unveränderlich** — ein Trigger hält dagegen. Löschen ist der Anwendungsrolle
entzogen, und über einen Vorschlag wird genau einmal entschieden. Ohne das
ließe sich in einem Jahr nicht mehr sagen, welcher Satz in einem Einsatzbericht
von einem Menschen stammt.
### Einrichtung
Postgres braucht seit dieser Phase **pgvector** neben PostGIS. Es gibt kein
offizielles Image mit beidem, deshalb baut FLORIAN eines
(`docker/postgres/Dockerfile`):
```bash
docker compose build postgres # bei einer bestehenden Installation
docker compose up -d postgres # Datenverzeichnis bleibt unberührt
npm run db:migrate
```
Dann die Modelle:
```bash
docker compose --profile ki up -d
docker compose exec ollama ollama pull qwen2.5:14b # Text
docker compose exec ollama ollama pull nomic-embed-text # Einbettungen
docker compose exec ollama ollama pull llama3.2-vision # Belege (optional)
```
Auf einem Rechner ohne Grafikkarte läuft das Einbettungsmodell noch flott, das
Textmodell zäh und das Vision-Modell praktisch nicht. Deshalb sind alle drei
einzeln konfigurierbar: eine Wehr kann das Wiki durchsuchbar machen, ohne
Berichte schreiben zu lassen.
Für eine Nvidia-Karte in die `docker-compose.override.yml`:
```yaml
services:
ollama:
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
```
### Ein anderes Einbettungsmodell
Die Spalte `wikiabschnitt.einbettung` ist auf **768 Dimensionen** festgelegt —
das liefert `nomic-embed-text`. Ein Modell mit anderer Länge braucht eine
Migration:
```sql
ALTER TABLE "wikiabschnitt" DROP COLUMN "einbettung";
ALTER TABLE "wikiabschnitt" ADD COLUMN "einbettung" vector(1024);
CREATE INDEX "wikiabschnitt_einbettung_idx"
ON "wikiabschnitt" USING hnsw ("einbettung" vector_cosine_ops);
```
Danach `LLM_EINBETTUNG_DIMENSIONEN` anpassen und alle Seiten neu einbetten. Der
nächtliche Lauf holt das von selbst nach; wer nicht warten will, drückt auf
jeder Seite „jetzt nachziehen".
Ohne Anpassung weist FLORIAN das Modell mit einer klaren Meldung ab, statt eine
Fehlermeldung der Datenbank durchzureichen, die niemand versteht.
### Zusätzliche Umgebungsvariablen
| Variable | Vorgabe | Bedeutung |
|---|---|---|
| `LLM_ENABLED` | `false` | Schaltet alle Textfunktionen ein |
| `LLM_BASE_URL` | — | OpenAI-kompatibler Endpunkt, z. B. `http://ollama:11434/v1` |
| `LLM_MODELL` | — | Modell für Berichte und Antworten |
| `LLM_TIMEOUT` | `120` | Zeitlimit in Sekunden |
| `LLM_EINBETTUNG_MODELL` | `nomic-embed-text` | Modell für die semantische Suche |
| `LLM_EINBETTUNG_DIMENSIONEN` | `768` | **Muss zur Spalte passen** |
| `LLM_VISION_MODELL` | — | Modell für die Belegerfassung |
| `WHISPER_ENABLED` | `false` | Schaltet das Diktat ein |
| `WHISPER_URL` | — | z. B. `http://whisper:8000` |
| `WHISPER_MODELL` | `base` | `tiny`, `base`, `small` |
| `DIKTAT_MAX_SEKUNDEN` | `600` | Höchstlänge einer Aufnahme |
| `OLLAMA_PORT` | `11435` | Port des Ollama-Containers (Profil `ki`) |
| `WHISPER_PORT` | `8087` | Port des Whisper-Containers (Profil `ki`) |
---
## Befehle
```bash
@@ -1003,6 +1185,9 @@ laufen kann. Alle Werte stehen in der `.env`.
| MinIO Weboberfläche | http://localhost:9011 |
| Gotenberg | http://localhost:3101 |
| Mailpit (nur Entwicklung) | http://localhost:8125 |
| ntfy (Profil `push`) | http://localhost:8086 |
| Ollama (Profil `ki`) | http://localhost:11435 |
| faster-whisper (Profil `ki`) | http://localhost:8087 |
| Adminer (nur Entwicklung) | http://localhost:8081 |
## Produktionsbetrieb
@@ -1025,9 +1210,15 @@ tut das bewusst nicht, damit nicht zwei Prozesse gleichzeitig migrieren.
Schriften verwendet, die auf dem Gerät vorhanden sind.
- Aufbewahrungsfristen konfigurierbar (`AUFBEWAHRUNG_JAHRE`,
`AUDIT_AUFBEWAHRUNG_TAGE`, `PERSON_ANONYMISIERUNG_TAGE`).
- Externe Dienste (Mail, Telegram, ntfy, Web-Push, LLM) sitzen hinter
- Externe Dienste (Mail, Telegram, ntfy, Web-Push) sitzen hinter
Schnittstellen in `src/lib/integrations/` und sind einzeln abschaltbar. Ohne
Freischaltung wird nur ins Log geschrieben.
- **Die KI-Funktionen laufen ausschließlich gegen selbst betriebene Modelle.**
Es gibt keinen Rückfall auf einen Anbieter: fehlt der eigene Endpunkt, fehlt
die Funktion. Kein Text verlässt das Gerätehaus.
- Jede KI-Ausgabe ist gekennzeichnet, wird als eigener Datensatz festgehalten
und muss von einem Menschen übernommen werden, bevor sie in einen Nachweis
gelangt. Nachvollziehbar im KI-Protokoll.
- Über Kanäle, die das Netz der Wehr verlassen (Telegram, Web-Push), gehen nur
Kurzfassungen ohne Namen, Gerätebezeichnungen und Einsatzdetails.
- Kein WhatsApp — die Begründung steht oben unter „Warum kein WhatsApp".