Skip to content

fix(extractor): Fensterexpansions-Rescue bei leerem Erst+Retry (#308)#335

Merged
TillQuandel merged 1 commit into
masterfrom
fix/308-extractor-window-rescue
Jul 17, 2026
Merged

fix(extractor): Fensterexpansions-Rescue bei leerem Erst+Retry (#308)#335
TillQuandel merged 1 commit into
masterfrom
fix/308-extractor-window-rescue

Conversation

@TillQuandel

Copy link
Copy Markdown
Owner

Closes #308.

Problem

PR #297 hat für den stummen <!--END-->-Drop einen Retry auf demselben Textfenster eingebaut (Symptom-Milderung). Die in #280 benannte Wurzel blieb: der Planner weist Konzepten Chunks zu, die die Belegstelle nicht enthalten. Bleibt ein Konzept nach Erst-Call UND #280-Retry (beide auf dem 400-Wort-Fenster von concept_text_window) leer, fiel es endgültig als dropped/empty_extraction weg — der #297-Retry verdoppelte in diesem Fall nur die Token-Kosten des Fehlversuchs, ohne die Chance auf Rettung zu erhöhen.

Vorher-Messung

Coverage-Serie 2 (2026-07-16): 2 [extractor-empty]-Fälle, #297-Retry rettete 1 von 2 (Sandmeier „Psychometrische Validierung" gerettet → Inbox; Kok Lauf 5 „Informierte Einwilligung" trotz Retry Totalverlust, trace drop_reason: empty_extraction). Ältere Belege: Schlebbe 2026-07-12 „Forschungstrends zu Information Needs" (1/3 verloren), Schüller 2026-07-14 „Kompetenzniveaus am Beispiel Datenanalyse" (<!--END-->-only), alte A/B-Logs cell4_Porst mit 21 [extractor-empty]-Zeilen. Testlauf-Serie 2026-07-14: Sonnet 4.6 traf Kernkonzepte in 3 von 6 Läufen (u.a. „Amotivation", [high]-priorisiert).

Design-Entscheidung

Fensterexpansion statt Neu-Ranking der Chunk-Zuordnung (die im Auftrag genannte Alternative): schlichter, ändert nichts am Planner/Chunking-Vertrag, und die Belegstelle ist ja durch concept_text_window bereits korrekt lokalisiert (Titel-Match, Score +100) — das Problem ist die Fenstergröße um den Treffer, nicht die Zuordnung selbst.

  • Neue Konstante orchestrator._RESCUE_WINDOW_WORDS = 1200 (3x window_words=400).
  • Bleibt extractor.run_per_concept nach Erst-Call+[MITTEL] Stiller Konzeptverlust via <!--END--> ohne Retry — traf Kernkonzepte in 3 von 6 Läufen #280-Retry bei None, folgt auf Orchestrator-Ebene (run_extractors_per_concept) genau EIN Rescue-Versuch mit dem expandierten Fenster.
  • Neuer Parameter retry_empty: bool = True an run_per_concept: der Rescue-Call setzt retry_empty=False, damit der interne [MITTEL] Stiller Konzeptverlust via <!--END--> ohne Retry — traf Kernkonzepte in 3 von 6 Läufen #280-Retry (auf demselben, bereits expandierten Fenster) nicht nochmal feuert — Kostendeckelung auf max. 1 Zusatz-Call pro Konzept (statt bis zu 2).
  • Kein Rescue-Call, wenn das expandierte Fenster identisch zum ursprünglichen wäre (z.B. sehr kurze Dokumente, bei denen 400 und 1200 Wörter beide den ganzen Text abdecken) — ein dritter Call wäre dann garantiert derselbe Fehlschlag.
  • Funnel-Events konsistent: Rescue-Erfolg → kein dropped-Event, Draft zählt normal zu drafts/concept_map. Endgültiger Verlust → weiterhin dropped/empty_extraction.
  • Log-Signaturen: [extractor-window-rescue] (Erfolg) / [extractor-window-rescue-failed].
  • Rückgabewerte/Semantik von run_extractors_per_concept (drafts, concept_map, dropped, failures) unverändert — nur drafts/dropped erweitern sich natürlich um die geretteten Fälle.

RED-Nachweis (vor Implementierung)

generative/tests/test_extractor_window_rescue.py, 6 Tests, 3 schlugen vor der Implementierung fehl:

FAILED test_window_rescue_recovers_after_double_empty
  AssertionError: Erwartet genau 3 Calls (Erst+#280-Retry+Rescue), waren 2
FAILED test_window_rescue_still_dropped_if_also_empty
  AssertionError: Erwartet genau 3 Calls total, waren 2
FAILED test_retry_empty_false_skips_internal_retry
  TypeError: run_per_concept() got an unexpected keyword argument 'retry_empty'
3 failed, 3 passed in 4.41s

Nach Implementierung: alle 6 grün (plus 34 bestehende Extractor-/Stage-Outcome-/Citation-Tests weiterhin grün, keine Regression).

Suite-Ergebnis

  • uv run pytest generative lib/decision_engine/tests shared/tests -q: 6162 passed, 3 skipped, 8 deselected, keine Fehler.
  • uv run ruff check .: All checks passed.
  • uv run ruff format --check .: 302 files already formatted.

Geänderte Dateien

  • generative/agents/extractor.py — Parameter retry_empty: bool = True an run_per_concept.
  • generative/orchestrator.py — Fenster-Rescue in run_extractors_per_concept (nur die Extractor-Dispatch-Region).
  • generative/tests/test_extractor_window_rescue.py (neu) — 6 Tests (Orchestrator-Rescue-Erfolg/-Fehlschlag/-Skip-bei-identischem-Fenster/-nur-für-None-nicht-Exceptions, Extractor-retry_empty-Kontrakt).

Offene Punkte

  • Der expanded_ctext == old_ctext-Skip vergleicht auf Textgleichheit; bei sehr großen Dokumenten mit vielen gleich hoch bewerteten Fenstern ist ein (harmloser) dritter Call mit im Ergebnis nahezu identischem, aber nicht bytegleichem Fenster nicht ausgeschlossen — kein Korrektheitsproblem, nur ein theoretisch möglicher Extra-Call.
  • Keine Änderungen an generative/config.py, pdf_enrich.py, docs/evaluation.md, agents/base.py, eval_quality_v4.py (Sperrzonen).

Root-Cause (#280/#297): der Planner weist Konzepten Chunks zu, die die
Belegstelle nicht ausreichend enthalten; das 400-Wort-Fenster
(concept_text_window) traf sie dann auch nach dem #280-Retry nicht --
der #297-Retry lief auf demselben Fenster und verdoppelte nur die
Token-Kosten des Fehlversuchs, ohne die Ursache zu beheben.

Fix: bleibt ein Konzept nach Erst-Call UND #280-Retry (extractor.
run_per_concept) weiterhin leer, folgt auf Orchestrator-Ebene genau EIN
Rescue-Versuch mit deutlich groesserem Fenster (400 -> 1200 Woerter,
_RESCUE_WINDOW_WORDS). Neuer Parameter retry_empty=False an
run_per_concept unterdrueckt dabei den internen #280-Retry, damit der
Rescue max. 1 statt bis zu 2 Zusatz-Calls kostet. Erfolg -> Draft zaehlt
normal, kein dropped-Event; Fehlschlag -> weiterhin dropped/
empty_extraction. Kein Rescue-Call, wenn das expandierte Fenster
identisch zum urspruenglichen waere (z.B. sehr kurze Dokumente).

Log-Signaturen: [extractor-window-rescue] / [extractor-window-rescue-failed].
@TillQuandel
TillQuandel merged commit 51e4140 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.

[MITTEL] Planner-Konzept-zu-Chunk-Zuordnung: Root-Cause des extractor-empty (#280-Wurzel)

2 participants