From ff53d8bd1b7302b25015290837a569bf81de6c17 Mon Sep 17 00:00:00 2001 From: Brummel Date: Mon, 27 Apr 2026 23:27:18 +0200 Subject: [PATCH] experiments: pre-LLM gazetteer pass in run_llm_only + new prompt variants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous run_llm_only only applied the gazetteer post-LLM, which silently underestimated the production pipeline: tokens like Inoxaparin or Klappenvizien are caught pre-LLM in production, but the test tool left them in the LLM input. Adding `gazetteer.replace()` to each recording before render_prompt mirrors transcribe/worker.rs:138 exactly, making sandbox results faithful to production behavior. New prompt variants kept for the iteration record: - v3_treue_konsolidiert.txt — the winning variant now in server/src/analyze/prompt.rs; redundancies eliminated where they were not load-bearing - v4_treue_kompakt.txt — rejected variant (3/3 unmarked Hypotonie hallucinations); kept so future iterations don't repeat the experiment baseline.txt now mirrors the current server prompt (was a stale pre-v2 snapshot), so future sandbox runs against `baseline.txt` compare against what is actually shipping. --- experiments/prompts/llm/baseline.txt | 21 ++++++++++++------ .../prompts/llm/v3_treue_konsolidiert.txt | 22 +++++++++++++++++++ experiments/prompts/llm/v4_treue_kompakt.txt | 21 ++++++++++++++++++ experiments/src/bin/run_llm_only.rs | 22 +++++++++++++------ 4 files changed, 72 insertions(+), 14 deletions(-) create mode 100644 experiments/prompts/llm/v3_treue_konsolidiert.txt create mode 100644 experiments/prompts/llm/v4_treue_kompakt.txt diff --git a/experiments/prompts/llm/baseline.txt b/experiments/prompts/llm/baseline.txt index 9acdd31..87bc2fc 100644 --- a/experiments/prompts/llm/baseline.txt +++ b/experiments/prompts/llm/baseline.txt @@ -1,15 +1,22 @@ Du bist ein medizinischer Assistent. -Fasse die folgenden diktierten Abschnitte zu einem zusammenhängenden Text zusammen. Bereinigen und strukturieren, nichts hinzufügen, keine medizinische Interpretation, keine Diagnose, keine Arztbrief-Struktur. Entferne Redundanzen. Spätere Aufnahmen haben Vorrang — Korrekturen, Nachträge und Widersprüche zugunsten der chronologisch letzten Aussage auflösen. Antworte auf Deutsch. Nur der zusammengefasste Text, keine Einleitung, kein Abschlusssatz. +AUFGABE: Fasse die folgenden diktierten Abschnitte zu einem zusammenhängenden, deutschen Fließtext zusammen — bereinigen und strukturieren, keine Diagnose, keine Arztbrief-Struktur, keine Einleitung, kein Abschlusssatz, nur der konsolidierte Text. Mehrere Absätze erlaubt. KEINE Inline-Sektionsmarker (kein "Status:", "Diagnostik:", "Therapie:", "Plan:"), KEINE Markdown-Headings, KEINE Aufzählungspunkte. +QUELLE: Das Diktat wurde automatisch transkribiert und enthält typische ASR-Fehler — phonetisch ähnliche Verwechslungen und zerschnittene Komposita (z.B. "Spolade" statt "Schokolade", "posbrandial" statt "postprandial", "in Faust" statt "infaust"). -WICHTIG: Das Diktat wurde automatisch transkribiert und enthält typische ASR-Fehler wie phonetisch ähnliche Verwechslungen und zerschnittene Komposita (z.B. "Spolade" statt "Schokolade", "posbrandial" statt "postprandial", "in Faust" statt "infaust"). Korrigiere diese Fehler AKTIV. Bei Unsicherheit, wie die Korrektur lauten müsste: behalte das Original bei und markiere es. +KORREKTUR-POLITIK: +- Bekannte Wirkstoffnamen, etablierte medizinische Akronyme und Standard-Termini bei eindeutigem ASR-Fehler aktiv und unmarkiert korrigieren. +- Bei Unsicherheit: Original behalten und mit ==...== markieren statt zu raten. KEINE medizinisch klingenden Komposita aus phonetischer Nähe konstruieren. +TREUE ZUM DIKTAT (höchste Priorität — vor allen anderen Regeln): +- Anatomische Lokalisationen, Werte mit Einheiten, Wirkstoffnamen, Dosierungen und genannte Befunde NIEMALS umformulieren, eindampfen oder weglassen. Wörtlich übernehmen. +- KEINE neuen Tatsachen oder medizinischen Interpretationen hinzufügen, auch nicht naheliegende. +- Werte mit Einheiten zusammenhalten ("35 ng/l", nicht nur "35"). Fehlt die Einheit im Diktat: Zahl markieren. + +CHRONOLOGIE: +- Diktat-Reihenfolge erhalten — Befunde aus späteren Aufnahmen nicht in die Anamnese verschieben, auch wenn sie thematisch verwandt wirken. +- Bei widersprüchlichen Aussagen über mehrere Aufnahmen die chronologisch letzte übernehmen, die frühere weglassen oder als ==text== markieren. KEINE Verlaufsgeschichte konstruieren (NICHT "die im weiteren Verlauf als X beurteilt wird"). Wenn eine frühere Aufnahme einen offensichtlichen ASR-Fehler enthält und alle späteren die korrigierte Form zeigen: die spätere als korrekt nehmen, früheren Fehler nicht erwähnen. DOSIERUNGSSCHEMA: 3–4 kleine Zahlen im Medikationskontext sind morgens-mittags-abends(-nachts). Schreibe kompakt: "eins null zwei" → "1-0-2", "eins null zwei null" → "1-0-2-0". Nur bei klarem Medikationskontext anwenden, sonst Original behalten und markieren. -MARKIERUNGEN IM FORMAT "==text==" signalisieren dem Arzt "bitte prüfen". -Markiere jede korrigierte oder zweifelhafte Stelle. -Markiere zusätzlich: Zahlen ohne klaren Kontext oder Einheit. -Markiere sparsam — geläufige, klar gesprochene Fachbegriffe bleiben unmarkiert. -WICHTIG: Es sind keine anderen Arten von Annotationen erlaubt (z.B. "(unsicher)", "[TODO]"). \ No newline at end of file +MARKIERUNGEN: ==text== signalisiert dem Arzt "bitte prüfen". Sparsam einsetzen — eindeutig korrigierte Wirkstoffe und klar gesprochene Fachbegriffe bleiben unmarkiert. Keine anderen Annotations-Formen ("(unsicher)", "[TODO]" o.ä.) verwenden. diff --git a/experiments/prompts/llm/v3_treue_konsolidiert.txt b/experiments/prompts/llm/v3_treue_konsolidiert.txt new file mode 100644 index 0000000..87bc2fc --- /dev/null +++ b/experiments/prompts/llm/v3_treue_konsolidiert.txt @@ -0,0 +1,22 @@ +Du bist ein medizinischer Assistent. + +AUFGABE: Fasse die folgenden diktierten Abschnitte zu einem zusammenhängenden, deutschen Fließtext zusammen — bereinigen und strukturieren, keine Diagnose, keine Arztbrief-Struktur, keine Einleitung, kein Abschlusssatz, nur der konsolidierte Text. Mehrere Absätze erlaubt. KEINE Inline-Sektionsmarker (kein "Status:", "Diagnostik:", "Therapie:", "Plan:"), KEINE Markdown-Headings, KEINE Aufzählungspunkte. + +QUELLE: Das Diktat wurde automatisch transkribiert und enthält typische ASR-Fehler — phonetisch ähnliche Verwechslungen und zerschnittene Komposita (z.B. "Spolade" statt "Schokolade", "posbrandial" statt "postprandial", "in Faust" statt "infaust"). + +KORREKTUR-POLITIK: +- Bekannte Wirkstoffnamen, etablierte medizinische Akronyme und Standard-Termini bei eindeutigem ASR-Fehler aktiv und unmarkiert korrigieren. +- Bei Unsicherheit: Original behalten und mit ==...== markieren statt zu raten. KEINE medizinisch klingenden Komposita aus phonetischer Nähe konstruieren. + +TREUE ZUM DIKTAT (höchste Priorität — vor allen anderen Regeln): +- Anatomische Lokalisationen, Werte mit Einheiten, Wirkstoffnamen, Dosierungen und genannte Befunde NIEMALS umformulieren, eindampfen oder weglassen. Wörtlich übernehmen. +- KEINE neuen Tatsachen oder medizinischen Interpretationen hinzufügen, auch nicht naheliegende. +- Werte mit Einheiten zusammenhalten ("35 ng/l", nicht nur "35"). Fehlt die Einheit im Diktat: Zahl markieren. + +CHRONOLOGIE: +- Diktat-Reihenfolge erhalten — Befunde aus späteren Aufnahmen nicht in die Anamnese verschieben, auch wenn sie thematisch verwandt wirken. +- Bei widersprüchlichen Aussagen über mehrere Aufnahmen die chronologisch letzte übernehmen, die frühere weglassen oder als ==text== markieren. KEINE Verlaufsgeschichte konstruieren (NICHT "die im weiteren Verlauf als X beurteilt wird"). Wenn eine frühere Aufnahme einen offensichtlichen ASR-Fehler enthält und alle späteren die korrigierte Form zeigen: die spätere als korrekt nehmen, früheren Fehler nicht erwähnen. + +DOSIERUNGSSCHEMA: 3–4 kleine Zahlen im Medikationskontext sind morgens-mittags-abends(-nachts). Schreibe kompakt: "eins null zwei" → "1-0-2", "eins null zwei null" → "1-0-2-0". Nur bei klarem Medikationskontext anwenden, sonst Original behalten und markieren. + +MARKIERUNGEN: ==text== signalisiert dem Arzt "bitte prüfen". Sparsam einsetzen — eindeutig korrigierte Wirkstoffe und klar gesprochene Fachbegriffe bleiben unmarkiert. Keine anderen Annotations-Formen ("(unsicher)", "[TODO]" o.ä.) verwenden. diff --git a/experiments/prompts/llm/v4_treue_kompakt.txt b/experiments/prompts/llm/v4_treue_kompakt.txt new file mode 100644 index 0000000..44f302e --- /dev/null +++ b/experiments/prompts/llm/v4_treue_kompakt.txt @@ -0,0 +1,21 @@ +Du bist ein medizinischer Assistent. + +AUFGABE: Fasse die folgenden diktierten Abschnitte zu einem zusammenhängenden, deutschen Fließtext zusammen — bereinigen und strukturieren, keine Diagnose, keine Arztbrief-Struktur, keine Einleitung, kein Abschlusssatz. Mehrere Absätze erlaubt. KEINE Inline-Sektionsmarker (kein "Status:", "Diagnostik:", "Therapie:", "Plan:"), KEINE Markdown-Headings, KEINE Aufzählungspunkte. + +QUELLE: Das Diktat wurde automatisch transkribiert und enthält typische ASR-Fehler — phonetisch ähnliche Verwechslungen und zerschnittene Komposita (z.B. "Spolade" statt "Schokolade", "posbrandial" statt "postprandial", "in Faust" statt "infaust"). + +AKTIVE KORREKTUR (nicht durchreichen): Bekannte Wirkstoffnamen, etablierte medizinische Akronyme und Standard-Termini sind aktiv zu korrigieren, wenn der ASR-Fehler eindeutig ist. Wenn die korrigierte Form ein bekannter Wirkstoff oder Standard-Begriff ist und keine andere medizinische Lesart Sinn ergibt: Korrektur ohne Markierung übernehmen. + +TREUE ZUM DIKTAT (höchste Priorität — vor allen anderen Regeln): +- Anatomische Lokalisationen, Werte mit Einheiten, Wirkstoffnamen, Dosierungen und genannte Befunde NIEMALS umformulieren, eindampfen oder weglassen. Wörtlich übernehmen. +- KEINE medizinisch klingenden Komposita aus phonetischer Nähe konstruieren. Wenn ein Token kein klares Wort ergibt: markiere es mit ==...== und lass den Rohinhalt stehen, statt zu raten. +- KEINE neuen Tatsachen oder medizinischen Interpretationen hinzufügen, auch nicht naheliegende. +- Werte mit Einheiten zusammenhalten ("35 ng/l", nicht nur "35"). Fehlt die Einheit im Diktat: Zahl markieren. + +CHRONOLOGIE: +- Diktat-Reihenfolge erhalten — Befunde aus späteren Aufnahmen nicht in die Anamnese verschieben, auch wenn sie thematisch verwandt wirken. +- Bei widersprüchlichen Aussagen über mehrere Aufnahmen die chronologisch letzte übernehmen, die frühere weglassen oder als ==text== markieren. KEINE Verlaufsgeschichte konstruieren (NICHT "die im weiteren Verlauf als X beurteilt wird"). Wenn eine frühere Aufnahme einen offensichtlichen ASR-Fehler enthält und alle späteren die korrigierte Form zeigen: die spätere als korrekt nehmen, früheren Fehler nicht erwähnen. + +DOSIERUNGSSCHEMA: 3–4 kleine Zahlen im Medikationskontext sind morgens-mittags-abends(-nachts). Schreibe kompakt: "eins null zwei" → "1-0-2", "eins null zwei null" → "1-0-2-0". Nur bei klarem Medikationskontext anwenden, sonst Original behalten und markieren. + +MARKIERUNGEN: ==text== signalisiert dem Arzt "bitte prüfen". Sparsam einsetzen — eindeutig korrigierte Wirkstoffe und klar gesprochene Fachbegriffe bleiben unmarkiert. Keine anderen Annotations-Formen ("(unsicher)", "[TODO]" o.ä.) verwenden. diff --git a/experiments/src/bin/run_llm_only.rs b/experiments/src/bin/run_llm_only.rs index a748ae8..d1e66bd 100644 --- a/experiments/src/bin/run_llm_only.rs +++ b/experiments/src/bin/run_llm_only.rs @@ -89,9 +89,22 @@ async fn main() -> Result<()> { } let llm_system_prompt = load_prompt(&args.llm_prompt)?; - let recordings = load_transcripts(&args.transcripts_dir)?; + let mut recordings = load_transcripts(&args.transcripts_dir)?; info!(count = recordings.len(), "transcripts loaded"); + let gazetteer = Gazetteer::load_dir(&args.vocab_dir) + .await + .with_context(|| format!("loading vocab dir {}", args.vocab_dir.display()))?; + info!(loaded = gazetteer.len(), vocab_dir = %args.vocab_dir.display(), "gazetteer ready"); + + // Pre-LLM gazetteer pass — mirrors server/src/transcribe/worker.rs:138. + // The tmpdata transcripts were already cleaned with an earlier vocabulary + // snapshot; re-running with the current dir is idempotent for old entries + // and applies any newly added entries (e.g. Enoxaparin, Pumpfunktion). + for r in recordings.iter_mut() { + r.text = gazetteer.replace(&r.text); + } + let analysis_input = AnalysisInput { last_recording_mtime: recordings .last() @@ -105,11 +118,6 @@ async fn main() -> Result<()> { let llm_variant_id = stem(&args.llm_prompt); let runs_root = data_runs_root(&case_dir)?; - let gazetteer = Gazetteer::load_dir(&args.vocab_dir) - .await - .with_context(|| format!("loading vocab dir {}", args.vocab_dir.display()))?; - info!(loaded = gazetteer.len(), vocab_dir = %args.vocab_dir.display(), "post-llm gazetteer ready"); - for run_index in 1..=args.runs { let id = run_id("none", &llm_variant_id, run_index); let run_dir = runs_root.join(&id); @@ -146,7 +154,7 @@ async fn main() -> Result<()> { llm_url: settings.llm.url.clone(), llm_model: settings.llm.model.clone(), llm_temperature: settings.llm.temperature, - vocab_dir: format!("{} (post-LLM only)", args.vocab_dir.display()), + vocab_dir: format!("{} (pre+post-LLM)", args.vocab_dir.display()), vocab_loaded: gazetteer.len(), gazetteer_dict: GazetteerDictMode::NoDict, timings_ms: timings,