Add design spec for alpha-id ICD code suggestion PoC
Brainstormed design: free-text dictation (marker-segmented by doctate) -> single prioritized top-10 ICD-10-GM list with tag filtering. Two modes (C lexical baseline, A hybrid retrieval + IONOS bge-m3 + Qwen3-VL-Reranker-8B), eval harness via synthetic transcription errors, one-time cost-guarded embedding. Licensed BfArM data dirs and local settings are gitignored. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
# Design-Spec: alpha-id — ICD-Code-Vorschläge aus Diktat
|
||||
|
||||
**Datum:** 2026-05-18
|
||||
**Status:** Entwurf zur Freigabe (Brainstorming abgeschlossen)
|
||||
**Stack:** Rust — Core-Library + dünne CLI (PoC)
|
||||
|
||||
---
|
||||
|
||||
## 1. Ziel & Kontext
|
||||
|
||||
Aus einem (bereits transkribierten, fehlerbehafteten) ärztlichen Diktat eine **einzige priorisierte Liste von max. 10 ICD-10-GM-Codes** mit Beschreibung erzeugen, mit Filterung nach Tags.
|
||||
|
||||
Zwei Zwecke:
|
||||
|
||||
1. **Vergessene abrechenbare Codes aufdecken** — im Stress vergessene Codes kosten Erlös. Das ist primär ein **Recall-Problem**. Leitmetrik: **Recall@10 abrechenbarer Codes**.
|
||||
2. **Hilfreiche klinische Impulse** — die bloße Sichtbarkeit verwandter Einträge (ICD-Hierarchie-Geschwister, Alpha-ID-Synonyme) erweitert das Bild und wirkt absichernd.
|
||||
|
||||
Das System ist ein **PoC**. Übernahme in das Schwesterprojekt `doctate` erfolgt **nur, wenn es nachweisbar taugt** (gemessen am Eval-Harness).
|
||||
|
||||
### I/O-Vertrag
|
||||
|
||||
- **Eingabe:** das *komplette* Diktat in *einem* Aufruf, durch **leere Zeilen (Markdown-Absätze)** in Befund-Segmente getrennt. Die Segmentierung erfolgt upstream durch doctates LLM; **dieses Projekt segmentiert nicht und enthält keinen generativen LLM**.
|
||||
- **Ausgabe:** **eine** priorisierte Liste von max. 10 ICD-Codes **für das gesamte Diktat** — nicht pro Segment. Marker werden intern genutzt (fokussierte Retrieval-Einheiten), danach global fusioniert.
|
||||
|
||||
### Datenquellen (vorhanden in `/home/brummel/dev/alpha-id/`)
|
||||
|
||||
- `data/icd10gm2026_alphaidse_edvtxt_20250926.txt` — Alpha-ID-SE, 90.898 Sätze, 8 `|`-getrennte Felder (UTF-8). Feld 8 = ~90k natürlichsprachliche Diagnose-Formulierungen inkl. Synonyme (primärer Match-Korpus). Feld 1 = Gültigkeit (0/1). Felder 3–6 = ICD-Codes (Primär/Stern/Ausrufezeichen/Primär2). Feld 7 = Orpha.
|
||||
- `icd-claml/Klassifikationsdateien/icd10gm2026syst_claml_20250912.xml` — ICD-10-GM 2026 als ClaML 2.0.0 (14 MB): Beschreibungen, Hierarchie, `<Meta>`-Tags (Para295/Para301, Kapitel/Gruppe, Exotic, Sex, Age, IfSG, Content).
|
||||
|
||||
### Modelle / Infrastruktur
|
||||
|
||||
IONOS AI Model Hub (`https://openai.inference.de-txl.ionos.com/v1`), Token `~/.ionos_token`. Nur zwei Modelle:
|
||||
|
||||
- **Embeddings:** `BAAI/bge-m3` (multilingual, deutscher Medizintext)
|
||||
- **Reranker:** `Qwen/Qwen3-VL-Reranker-8B` (Cross-Encoder)
|
||||
|
||||
Kein generativer Chat-LLM. IONOS-Client-Pattern aus `doctate/server/src/analyze/backend.rs` und `doctate/docs/ionos-llm-api.md` wiederverwenden (Token-Handling, Retry, Quirks).
|
||||
|
||||
---
|
||||
|
||||
## 2. Architektur
|
||||
|
||||
Zwei Modi über Config-Schalter, **gleiche Schnittstelle**, im selben Eval-Harness vergleichbar:
|
||||
|
||||
- **Modus C — Lexik-Baseline:** BM25/Trigramm + Tag-Filter. Kein IONOS, offline, sofort. Die Baseline, die A schlagen muss.
|
||||
- **Modus A — Hybrid + Reranker (Kern):** Lexik ∪ Semantik (`bge-m3`) → `Qwen3-VL-Reranker-8B` → Fusion → Tag-Filter → Top-10.
|
||||
|
||||
**Modus B (generativer LLM-Rerank/-Vorschlag) ist explizit NICHT Scope.** Embedding- und Reranker-Modelle sind keine generativen LLMs und bleiben in A.
|
||||
|
||||
### Datenfluss
|
||||
|
||||
```
|
||||
Komplettes Diktat (Befunde durch Leerzeilen getrennt)
|
||||
│
|
||||
▼ [1] Parser → Segmente (Split an Leerzeilen)
|
||||
▼ [2] Normalisierung → pro Segment: lowercasing, Medizin-Abkürzungen,
|
||||
│ Tokenisierung (kein LLM)
|
||||
├─►[3a] Lexik (tantivy BM25/Trigramm) ┐ pro Segment:
|
||||
└─►[3b] Semantik (bge-m3-Query-Embedding vs. ┘ Pool ~50–100 (RRF-Mix)
|
||||
vorab persistierter Vektorindex) [A; in C nur 3a]
|
||||
▼ [4] Reranker → Qwen3-VL-Reranker-8B: (Segmenttext, Alpha-ID-Text)
|
||||
│ paarweise → rerank-Score [nur A]
|
||||
▼ [5] Fusion → alle Segment-Kandidaten in EINE Liste; je
|
||||
│ normalisiertem ICD-Code dedupliziert
|
||||
│ (score=max, alle Herkünfte erhalten)
|
||||
▼ [6] Tag-Filter → harte Constraints: Abrechenbarkeit (Para295/301),
|
||||
│ Gültigkeit (Feld 1), Kapitel/Gruppe,
|
||||
│ klinische Marker (Exot/Sex/Alter/IfSG/Content)
|
||||
▼ [7] Top-10 → globales Budget; je Treffer: Code, Beschreibung,
|
||||
Score, Quell-Phrase, Quell-Segmente, Tags
|
||||
```
|
||||
|
||||
### Module (klar abgegrenzt)
|
||||
|
||||
| Modul | Aufgabe | Abhängigkeit |
|
||||
|---|---|---|
|
||||
| `corpus` | Alpha-ID parsen → In-Memory-Index (ICD↔Phrasen, Gültigkeit, Orpha) | `data/` |
|
||||
| `claml` | ClaML-XML einmal parsen → `code → IcdMeta` | `icd-claml/` |
|
||||
| `embed` | IONOS `bge-m3`-Client (Batch, Cache, Retry, Resume) | IONOS |
|
||||
| `rerank` | IONOS `Qwen3-VL-Reranker-8B`-Client (Paar-Scoring, Batch, Retry) | IONOS |
|
||||
| `index` | Lexik-Index (tantivy) + persistierter Vektorindex (ANN) bauen/laden | `corpus`,`embed` |
|
||||
| `tags` | Filter-Prädikate aus ClaML-Meta + Alpha-ID-Gültigkeit | `claml` |
|
||||
| `pipeline` | Orchestrierung [1]–[7], Modus C/A per Config | alle obigen |
|
||||
| `eval` | Gold-Set-Generator + Recall@k-Metrik | `pipeline` |
|
||||
| `bin` (CLI) | dünner Wrapper: `index`, `suggest`, `eval` | `pipeline`,`eval` |
|
||||
|
||||
Jedes Modul ist für sich testbar; `pipeline` ist die einzige Stelle, die Modi kennt. Die Core-Library bleibt frei von CLI-Belangen, damit doctate sie später unverändert einbinden kann.
|
||||
|
||||
---
|
||||
|
||||
## 3. Datenmodell (Kerntypen)
|
||||
|
||||
```
|
||||
AlphaIdEntry { alpha_id, valid: bool, icd_primary, icd_star,
|
||||
icd_addon, icd_primary2, orpha, text }
|
||||
// icd_addon = Zusatzschlüssel (Feld 5, „!"); NICHT ICD-Exklusiva
|
||||
IcdMeta { code, description, chapter, group, para295, para301,
|
||||
exotic, sex, age_low, age_high, ifsg, content }
|
||||
Candidate { icd_code, alpha_text, segment_idx,
|
||||
lexical: Option<f32>, semantic: Option<f32>,
|
||||
rerank: Option<f32> }
|
||||
Suggestion { icd_code, description, score, matched_phrase,
|
||||
source_segments: Vec<usize>, tags: Tags }
|
||||
Filter { billable_only, valid_only, chapters: Option<Set>,
|
||||
exclude_exotic, … }
|
||||
Config { mode: C|A, top_k, pool_size, filter, ionos }
|
||||
```
|
||||
|
||||
**Dedupe-Schlüssel = normalisierter ICD-Code** (ohne Kreuz `+`, Stern `*`, Ausrufezeichen `!`). Ein getroffener Alpha-ID-Eintrag steuert seinen Primärcode bei; Stern-/`!`-Codes werden als ergänzend markiert. Abrechenbarkeit entscheidet der Tag-Filter (Schritt 6), nicht der Match.
|
||||
|
||||
### Fusion (Schritt 5, präzisiert)
|
||||
|
||||
- **Innerhalb** eines Segments: Kandidatenpool = Lexik-Top-N ∪ Semantik-Top-N, gemischt per **Reciprocal Rank Fusion (RRF)**. In Modus C ist die RRF-Ordnung die finale Vor-Filter-Reihung; in A folgt der Reranker.
|
||||
- **Über** Segmente: dedupliziert je normalisiertem ICD-Code, `score = max` über alle Vorkommen, **alle Herkunfts-(Segment, Phrase) erhalten**. Zahl getroffener Segmente = Tiebreaker und „Breiten"-Signal für Ziel 2.
|
||||
|
||||
---
|
||||
|
||||
## 4. Einmaliges, kostensicheres Embedding (kritische Anforderung)
|
||||
|
||||
Das Embedden von ~90k Texten via IONOS kostet Geld und soll **genau einmal** erfolgen. Maßnahmen:
|
||||
|
||||
1. **Content-Hash-gekeyter, persistenter Cache.** Jeder Alpha-ID-Text → Embedding, gekeyt durch einen stabilen Hash (z. B. SHA-256 von normalisiertem Text + Modellname + Modellversion). Persistenz on-disk (z. B. `index/embeddings.db` o. ä.). Vor jedem Embed-Aufruf Cache-Lookup; nur fehlende Keys werden gesendet.
|
||||
2. **Resumierbar / idempotent.** Bricht der Full-Run ab (Crash, Netz, Timeout), setzt der nächste Lauf bei den fehlenden Keys fort — **kein Neustart von 0**, kein doppelt bezahltes Token.
|
||||
3. **Cost-Guard vor dem Full-Run.** `index build` schätzt zuerst Items, Tokens und Kosten und zeigt sie an; der **Full-Run erfordert explizite Bestätigung** (`--confirm` Flag oder interaktiv).
|
||||
4. **Verpflichtender Smoke-Test zuerst.** `index build --sample N` (Default klein, z. B. 50) embedded nur ein Sample, validiert: HTTP-OK, korrekte Vektordimension, Cache schreibt/liest, ANN-Index baut, eine Beispiel-`suggest`-Abfrage liefert plausible Treffer. **Erst nach grünem Sample** wird der Full-Run freigegeben.
|
||||
5. **Delta-fähig.** Ändert sich künftig der Korpus (neue Alpha-ID-Jahresversion), werden nur geänderte/neue Texte nachembedded — der Hash-Key macht das automatisch.
|
||||
|
||||
Reihenfolge im Staging: **C zuerst** (braucht kein Embedding, sofort lauffähig + Eval-Harness), dann **A-Embedding-Pfad am Sample verifiziert**, dann **ein einziger** Full-Embedding-Run.
|
||||
|
||||
---
|
||||
|
||||
## 5. Fehlerbehandlung
|
||||
|
||||
**Leitprinzip: degradieren, nie hart scheitern** — fehlende Codes kosten Geld; der Arzt bekommt *immer* eine Liste.
|
||||
|
||||
- IONOS Timeout/5xx: Retry mit Backoff (doctate-Pattern). Dauerhafter Ausfall in Modus A → **automatischer Fallback auf Modus C**; Ergebnis trägt `degraded: true`.
|
||||
- Token `~/.ionos_token` fehlt → klare Meldung; A nicht verfügbar, C läuft.
|
||||
- Leeres/kaputtes Diktat → leeres Ergebnis mit Begründung. Segment ohne Kandidaten → übersprungen, in `diagnostics` vermerkt.
|
||||
- Korpus-/ClaML-Ladefehler → **harter Startfehler** (ohne Daten kein Betrieb).
|
||||
- **Keine Halluzination per Konstruktion:** Treffer stammen ausschließlich aus Korpus-Einträgen; der Reranker sortiert nur um, erfindet nichts.
|
||||
|
||||
---
|
||||
|
||||
## 6. Eval-Harness (erstklassige Komponente)
|
||||
|
||||
Kein gelabeltes Datenmaterial vorhanden → der Harness ist Teil des PoC, kein Nachgedanke. Der Alpha-ID-Korpus ist sein eigenes Gold-Set (jeder Text hat einen bekannten ICD-Code).
|
||||
|
||||
- **Generator:** sampelt gültige, abrechenbare Alpha-ID-Einträge → `(text, erwarteter ICD)`. Injiziert **deterministisch (Seed)** realistische Transkriptionsfehler: deutsche Medizin-Homophone, Wortgrenzen-Merge/Split, Zifferndreher, fehlende Umlaute/Diakritika, typische ASR-Verwechslungen. Konfigurierbare Fehlerrate, mehrere Varianten/Eintrag.
|
||||
- **Plus** ein kleines, handgelabeltes Set realistischer Mehr-Befund-Diktate (leerzeilengetrennt) — die echte Eingabeform.
|
||||
- **Metriken:** Recall@1/5/10 gesamt; **gesondert Recall@10 abrechenbarer Codes** (Leitzahl Ziel 1); Modus C vs. A nebeneinander; Latenz + IONOS-Calls/Kosten; Breiten-Maß (distinkte assoziierte Codes) für Ziel 2.
|
||||
|
||||
„Taugt's" ist eine **Go/No-Go-Entscheidung des Nutzers** anhand der Eval-Tabelle: A vs. C bei Recall@10 abrechenbarer Codes auf dem fehlerinjizierten Gold-Set, abgewogen gegen Latenz und IONOS-Kosten. Eine feste numerische Schwelle wird hier bewusst **nicht** erfunden; sie wird nach dem ersten Eval-Lauf gemeinsam festgelegt.
|
||||
|
||||
---
|
||||
|
||||
## 7. CLI-Oberfläche
|
||||
|
||||
```
|
||||
alpha-id index build [--sample N | --full] [--confirm]
|
||||
# Smoke-Test (Sample) bzw. einmaliger Full-Run;
|
||||
# Korpus+ClaML parsen, bge-m3-Embeddings,
|
||||
# Lexik-+Vektorindex persistieren (resumierbar)
|
||||
alpha-id suggest [FILE|-] # Diktat → Top-10
|
||||
--mode c|a --top 10 --billable-only --valid-only
|
||||
--chapters I,J --exclude-exotic --json|--table
|
||||
alpha-id eval --mode c|a --cases N --seed S [--gold PATH]
|
||||
```
|
||||
|
||||
- Config-TOML: IONOS-Endpoint/Modellnamen/Pool-Größen/Pfad-Defaults, per Flags überschreibbar.
|
||||
- `suggest`-Ausgabe (JSON) je Treffer: `icd_code, description, score, matched_phrase, source_segments, tags{billable, chapter, exotic, …}` + Top-Level `diagnostics{ mode, degraded, timings, segment_count, ionos_calls }`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Abgrenzung (Out of Scope)
|
||||
|
||||
- Generativer Chat-LLM in der Pipeline (Modus B).
|
||||
- Segmentierung des Diktats (kommt fertig aus doctate).
|
||||
- Eigene/projektspezifische Tag-Whitelists (nur ClaML-abgeleitete Filter).
|
||||
- Produktions-Deployment / doctate-Integration (erst nach erfolgreichem PoC).
|
||||
- UI (CLI genügt für den PoC).
|
||||
|
||||
---
|
||||
|
||||
## 9. Offene Punkte / Annahmen
|
||||
|
||||
- Genaues IONOS-Antwortformat von `bge-m3` (Vektordimension) und `Qwen3-VL-Reranker-8B` (Score-Skala, Batch-API) wird im Sample-Smoke-Test (Abschnitt 4.4) empirisch verifiziert, bevor der Full-Run läuft.
|
||||
- Wahl der konkreten Rust-Crates (tantivy für Lexik; ANN-Lib z. B. `usearch`/`hnsw_rs`; Embedding-Persistenz) wird im Implementierungsplan fixiert.
|
||||
- Das Repo ist derzeit **kein** Git-Repository; vor dem Commit dieses Specs ggf. `git init`.
|
||||
Reference in New Issue
Block a user