# 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, ``-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, semantic: Option,
rerank: Option }
Suggestion { icd_code, description, score, matched_phrase,
source_segments: Vec, tags: Tags }
Filter { billable_only, valid_only, chapters: Option,
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.
---
## 3a. ICD-Code-Granularität & ClaML-Subklassifikation (Revision 2026-05-18)
**Befund (an Echtdaten belegt):** ~26,5 % der gültigen Alpha-ID-Primärcodes sind 6-stellig (5. ICD-Stelle, z. B. `E11.72`, `I10.91`). ClaML modelliert die 4./5. Stelle vieler Gruppen — u. a. **alle Diabetes-Codes E10–E14** — **nicht** als eigene ``, sondern über ``/`` (Subklassifikation), referenziert per `` am übergeordneten ``. Ein naiver `meta.get(code)` (bzw. Strip-/Root-Fallback) klassifiziert dadurch **1.193 distinkte gültige Codes (≈ 4.394 Alpha-ID-Zeilen)** fälschlich als nicht-abrechenbar (Rückfall auf 3-Steller mit `Para295=V`) → unter `--billable-only` werden real abrechenbare Codes still verworfen. **Das untergräbt Ziel 1.**
**Entschiedene Auflösung (Nutzer, 2026-05-18): korrekte ClaML-Modifier-Expansion.** `claml.rs` wertet zusätzlich `` (mit ``), `` (mit ``, preferred `