Documents the corpus↔ClaML 5th-digit gap, the decided Modifier/ ModifierClass expansion fix (Goal 1), and the deterministic / seeded-sample eval corrections from the final review. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
15 KiB
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:
- Vergessene abrechenbare Codes aufdecken — im Stress vergessene Codes kosten Erlös. Das ist primär ein Recall-Problem. Leitmetrik: Recall@10 abrechenbarer Codes.
- 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.
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 <Class kind="category">, sondern über <Modifier>/<ModifierClass> (Subklassifikation), referenziert per <ModifiedBy code=… all=…> am übergeordneten <Class>. 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 <Modifier> (mit <SubClass>), <ModifierClass> (mit <SuperClass>, preferred <Rubric><Label>, ggf. eigenem <ModifiedBy> zur Verkettung der nächsten Stelle) und <ModifiedBy> an <Class> aus. Für jeden <Class> mit ModifiedBy-Referenzen werden die terminalen Stellen-Kombinationen synthetisiert (4. dann 5. Stelle, der ClaML-Subklassifikations-Algorithmus) und als zusätzliche IcdMeta unter dem kombinierten Code (z. B. E11+.7+2 → E11.72) in die Map eingetragen. Regeln:
- Explizite
<Class>-Einträge haben Vorrang; Synthese füllt nur fehlende Schlüssel (additiv, kein Überschreiben). - Abrechenbarkeit: Terminale synthetisierte 5.-Stellen-Codes dieser modifizierten Gruppen sind
Para295=P/Para301=P(der bare 3-Steller bleibtV).Contentdes Parent (J= „erfordert Subklassifikation") ist das Signal, dass erst die terminale Stelle kodierbar ist. - Beschreibung: zusammengesetzt aus Parent-
preferred-Label + 4.-Stellen-ModifierClass-Label + 5.-Stellen-ModifierClass-Label. chapter/exotic/ifsgvom Parent geerbt, sofern der Modifier nichts Spezifischeres trägt.- Keine kartesische Explosion über fremde Modifier: nur die vom jeweiligen
<Class>perModifiedBy(in Reihenfolge) referenzierten Modifier kombinieren; ModifierClass-eigeneModifiedByverketten die nächste Stelle.
Alle nötigen Informationen liegen allein in der ClaML-XML (die gelöschte Meta-TXT ist nicht erforderlich). Konsequenz im Plan: eigener Task „ClaML-Modifier-Expansion" + Revision des Mode-C-Pipeline-Lookups (kein Strip-/Root-Hack mehr) + Abrechenbarkeits-Regressionstest (z. B. E11.x unter --billable-only sichtbar & billable=true).
4. Einmaliges, kostensicheres Embedding (kritische Anforderung)
Das Embedden von ~90k Texten via IONOS kostet Geld und soll genau einmal erfolgen. Maßnahmen:
- 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.dbo. ä.). Vor jedem Embed-Aufruf Cache-Lookup; nur fehlende Keys werden gesendet. - 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.
- Cost-Guard vor dem Full-Run.
index buildschätzt zuerst Items, Tokens und Kosten und zeigt sie an; der Full-Run erfordert explizite Bestätigung (--confirmFlag oder interaktiv). - 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. - 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_tokenfehlt → klare Meldung; A nicht verfügbar, C läuft. - Leeres/kaputtes Diktat → leeres Ergebnis mit Begründung. Segment ohne Kandidaten → übersprungen, in
diagnosticsvermerkt. - 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-Leveldiagnostics{ 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) undQwen3-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. - Aufgelöst 2026-05-18: Korpus↔ClaML-Code-Granularität (5. Stelle nur via
<ModifierClass>) — siehe §3a; korrekte ClaML-Modifier-Expansion beschlossen und in den Plan aufgenommen.