Skip to content

fix(orchestrator): semantischer Fallback am Stage-5-Volltext-Check (#127)#339

Merged
TillQuandel merged 1 commit into
masterfrom
fix/127-stage5-semantic-fallback
Jul 17, 2026
Merged

fix(orchestrator): semantischer Fallback am Stage-5-Volltext-Check (#127)#339
TillQuandel merged 1 commit into
masterfrom
fix/127-stage5-semantic-fallback

Conversation

@TillQuandel

Copy link
Copy Markdown
Owner

Closes #127.

Problem

Der Stage-5-Volltext-Check (run_extractors_per_concept in generative/orchestrator.py) ist rein lexikalisch: pdf_chunker.concept_text_window() sucht Titel + Titel-Tokens als Substring/Wortgrenzen-Regex im Volltext. Liefert deutscher Planner-Output ("Andragogik", "Pädagogisches Modell", "Selbstkonzept als selbstgesteuert Lernender") auf einer englischen Quelle (Knowles-PDF, 25 S.) 0 Token-Overlap, wird jedes Konzept mit [skip] '<titel>' nicht im Volltext gefunden (Halluzinations-Schutz) verworfen — bei fast-Profil (max_concepts=3) macht das 0 Draft-Notes, obwohl die Konzepte thematisch exakt zur Quelle passen.

Der cross-linguale Rettungsanker aus #66 (planner.filter_hallucinated, semantische MAX-Cosine als OR-Kanal) sitzt eine Stage vorher — die drei Konzepte überleben Stage 4 (Planner), sterben aber am rein lexikalischen Stage-5-Skip, der keinen semantischen Kanal hat.

Fix

run_extractors_per_concept versucht vor dem endgültigen [skip] denselben semantischen Rettungskanal wie planner.filter_hallucinated (#66): multilinguales MiniLM-Embedding (generative/embeddings.py, paraphrase-multilingual-MiniLM-L12-v2 — bereits als multilingual verifiziert, dasselbe Modell wie #66), MAX-Cosine zwischen Titel-Embedding und Satz-Embeddings, gegen dieselbe Schwelle TITLE_PRESENCE_COSINE_THRESHOLD (0.50) — keine neue Magic Number.

Neue Funktion pdf_chunker.semantic_concept_window(full_text, title, threshold, window_words=400):

  • Nutzt dieselbe Sliding-Window-Geometrie (_iter_word_windows, 50%-Stride) wie der lexikalische Scorer in concept_text_window — beide Funktionen wurden auf einen gemeinsamen Helper refactored, kein zweites Chunking-Schema.
  • Satz-Ebene statt Fenster-Mittelwert: erste Version hatte pro Fenster gemittelt (mean-pooling über alle Sätze eines 400-Wort-Fensters) — Kalibrierung zeigte, dass das den Score eines einzelnen treffenden Satzes verwässert (Andragogik-Fall fiel dabei auf 0.43, unter die Schwelle). Auf Satzebene (MAX-Cosine über alle Sätze, exakt die Methodik aus _default_semantic_presence/fix: cross-lingualer Recall + typ-bewusste Dedup + Body-Redundanz-Flag #66) bleibt die kalibrierte Schwelle 0.50 gültig.
  • Sätze aus überlappenden Fenstern werden dedupliziert, bevor EIN batched model.encode()-Call läuft (statt einem Call pro Fenster — erste Implementierung brauchte dadurch 80-130s pro Dokument, s.u.).
  • Ergebnis-Fenster (für den Extraktor-Kontext) + Score werden pro (Volltext, window_words) gecacht — mehrere in Stage 5 leer gebliebene Konzepte desselben Laufs (z.B. alle drei Top-Konzepte eines fast-Profils) teilen sich EIN Encode.
  • Fail-closed: Model-Load-Fehler, leerer Titel/Volltext oder Score unter Schwelle → ("", score), der Aufrufer behält den bestehenden [skip] + Funnel-Event unverändert. Anders als _default_semantic_presence (fail-open, reiner Zusatzkanal) darf dieser Fallback nicht "blind durchwinken", weil das Ergebnis direkt als LLM-Kontext weiterverwendet wird.

run_extractors_per_concept bekommt einen neuen optionalen Parameter semantic_window_fn (Default: pdf_chunker.semantic_concept_window mit der config-Schwelle) — injizierbar für deterministische Tests ohne ML-Modell-Load, analog zu filter_hallucinateds semantic_presence_fn.

Neue Log-Zeile bei Rettung: [semantic-window-fallback] '<titel>' lexikalisch nicht gefunden, semantisch gerettet (cosine=0.xx). Der bestehende [skip]-Zweig + _trace_stage_outcome(..., drop_reason="empty_extraction", detail="not in fulltext") bleiben für den echten Ablehnungsfall unverändert.

Kalibrierung (offline, keine LLM-Calls, keine Pipeline-Läufe)

Gemessen mit der finalen (satzbasierten) Implementierung gegen echte PDF-Volltexte (pdftotext-Extraktion, keine Pipeline):

Knowles — From Pedagogy to Andragogy (EN, 25 S.) — die drei echten Skip-Funde aus Issue #127:

Titel (DE, Planner-Output) Max-Cosine Schwelle 0.50
Andragogik 0.7920 gerettet
Pädagogisches Modell 0.7161 gerettet
Selbstkonzept als selbstgesteuert Lernender 0.7612 gerettet
Quantenverschränkung in Photonenpaaren (Kontrolle, themenfremd) 0.2380 verworfen
Blockchain für Lieferkettenmanagement (Kontrolle, themenfremd) 0.2819 verworfen

Porst — Fragebogen: Ein Arbeitsbuch (DE, deutsches Baseline-PDF):

Titel Max-Cosine Schwelle 0.50
Fragebogenkonstruktion (Match-Titel) 0.8160 gerettet (würde auch lexikalisch matchen)
Quantenverschränkung in Photonenpaaren (Kontrolle, themenfremd) 0.4042 verworfen

Klare Trennung: echte Konzepte 0.72–0.82, themenfremde Kontrollfälle 0.24–0.40. Die Schwelle 0.50 (identisch zu #66, dort auf n=1 mit Werten 0.575–0.825 vs. 0.357/0.358 gemessen) trennt hier mit vergleichbarem Abstand — Wiederverwendung ohne Anpassung ist tragfähig.

Performance: Erster Fallback-Aufruf pro Dokument (Cache-Miss) kostet ~50-70s CPU (Satz-Encode über alle Fenster-Sätze, dedupliziert). Folgeaufrufe für weitere Konzepte DESSELBEN Volltexts (Cache-Hit) ~0.1-0.2s. Das ist ein einmaliger Kosten pro Lauf im seltenen Totalausfall-Fall (lexikalischer Scan komplett leer) — kein Zusatzaufwand im Normalfall, da der Fallback nur bei leerem ctext überhaupt aufgerufen wird (per Test verifiziert: test_semantic_fallback_not_consulted_when_lexically_present).

Tests (RED → GREEN)

Neu: generative/tests/test_stage5_semantic_fallback.py (Orchestrator-Ebene, semantic_window_fn-Injection) + Erweiterung generative/tests/test_pdf_chunker.py (semantic_concept_window-Unit-Tests mit deterministischem One-Hot-Fake-Modell + ein @pytest.mark.slow-Test mit echtem Modell):

  • (a) deutscher Titel + englischer Volltext, semantisch passend → Fallback greift, Extractor-Task entsteht, kein dropped-Event, Log-Signatur [semantic-window-fallback] mit Cosine-Wert
  • (b) themenfremder Titel → weiterhin [skip] + dropped/empty_extraction-Funnel-Event, kein Extractor-Call
  • (c) lexikalischer Treffer → semantic_window_fn wird gar nicht erst aufgerufen (Performance-Garantie im Normalfall)
  • Default-Verdrahtung zeigt auf pdf_chunker.semantic_concept_window mit TITLE_PRESENCE_COSINE_THRESHOLD
  • semantic_concept_window: Rettung bei Treffer, leer bei Unterschreitung, Cache-Wiederverwendung über mehrere Konzepte desselben Volltexts, fail-closed bei kaputtem Modell, Kurzschluss bei leerem Titel/Text, echtes Modell (cross-lingual, @pytest.mark.slow)
  • Bestehender Test test_run_extractors_empty_ctext_emits_stage_outcome angepasst (No-Op-semantic_window_fn injiziert, damit er ohne ML-Modell schnell/deterministisch bleibt — das Rettungsverhalten selbst ist dediziert getestet)

Refactor (verhaltensneutral, per bestehender test_pdf_chunker.py-Suite verifiziert): concept_text_windows Fenster-Loop und Seiten-Marker-Logik wurden in _iter_word_windows/_page_at_word_map/_prefix_page_marker extrahiert, damit semantic_concept_window dieselbe Chunking-Infrastruktur nutzt statt ein zweites Schema zu bauen.

Suite: uv run pytest generative lib/decision_engine/tests shared/tests -q6199 passed, 3 skipped, 9 deselected (337s), keine Fehler. uv run ruff check . → All checks passed. uv run ruff format --check . → alle Dateien formatiert. Zusätzlich gezielt uv run pytest generative/tests/test_pdf_chunker.py -m slow -k semantic_window → 1 passed (echtes Embedding-Modell, cross-lingual, 42s).

Scope

Ausschließlich innerhalb run_extractors_per_concept (orchestrator.py) + additive Erweiterung von pdf_chunker.py + Tests. planner.py, db.py, eval_quality_v4.py, eval_dashboard_server.py, extractor.py unangetastet; Lauf-Ende/Summary/DB-Schreibpfade sowie Frühausstiegs-Pfade vor dem Extractor-Dispatch in orchestrator.py unangetastet (parallele #330-Arbeit).

)

concept_text_window() ist rein lexikalisch: ein deutscher Planner-Titel auf
einer englischen Quelle hat 0 Token-Overlap und wird verworfen, obwohl er
planner.filter_hallucinated's #66-Rettungsanker (Stage 4) bereits passiert
hat -- bei fast-Profil und kleiner englischer Quelle (Knowles-PDF) fuehrte
das zu 0 Draft-Notes trotz thematisch passender Konzepte.

Neue Funktion pdf_chunker.semantic_concept_window(): MAX-Cosine zwischen
Titel- und Satz-Embeddings (multilinguales MiniLM, generative/embeddings.py)
gegen dieselbe Schwelle TITLE_PRESENCE_COSINE_THRESHOLD wie #66 -- Fenster-
Geometrie (_iter_word_windows, Seiten-Marker) aus concept_text_window
wiederverwendet statt eines zweiten Chunking-Schemas. Faellt vor dem
endgueltigen [skip] gezielt darauf zurueck, fail-closed bei Modellfehler
oder Score unter Schwelle. run_extractors_per_concept bekommt dafuer den
injizierbaren Parameter semantic_window_fn (Default: pdf_chunker-Funktion
mit config-Schwelle), analog filter_hallucinated's semantic_presence_fn.

Kalibrierung (offline, echte PDF-Volltexte, keine LLM-Calls): die drei
realen Skip-Faelle aus Issue #127 (Andragogik, Paedagogisches Modell,
Selbstkonzept als selbstgesteuert Lernender) liegen bei 0.72-0.82,
themenfremde Kontrollfaelle bei 0.24-0.40 -- Schwelle 0.50 trennt klar.

Suite: 6199 passed, 3 skipped, 9 deselected (generative + lib/decision_engine
+ shared, 337s). ruff check + format sauber.
@TillQuandel
TillQuandel merged commit 6753bb1 into master Jul 17, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Stage-5-Volltext-Check ist sprachblind — deutsche Planner-Titel auf englischer Quelle werden komplett geskippt (0 Notes)

2 participants