Skip to content

fix(eval): Satz-Level-Retrieval-Rescue + Titel-Chunk-Deprioritisierung gegen Retrieval-Miss (#232) - #265

Merged
TillQuandel merged 2 commits into
masterfrom
fix/232-retrieval-rescue
Jul 14, 2026
Merged

fix(eval): Satz-Level-Retrieval-Rescue + Titel-Chunk-Deprioritisierung gegen Retrieval-Miss (#232)#265
TillQuandel merged 2 commits into
masterfrom
fix/232-retrieval-rescue

Conversation

@TillQuandel

Copy link
Copy Markdown
Owner

Root Cause (#232): Granularitaets-Mismatch Chunk vs. Satz

Das Eval-Retrieval (_retrieve_claim_contexts) rankt Claims gegen CHUNK-Embeddings (100-180 Tok). Der tatsaechlich stuetzende SATZ hat hohe Claim-Cosine, sein Home-Chunk landet aber weit hinten in der Rangliste, weil das Beleg-Signal ueber den Chunk gemittelt wird -> er faellt aus adaptive_k (oft k=2) -> der Judge sieht den Beleg nie -> falsch-positives not_in_context -> aufgeblaehte Halluzinationsrate (5/6 handverifizierte FP-Anker im Ausloesefall, faktische Fehlerquote ~7 % statt 42,9 %).

Fix

F1 -- Satz-Level-Retrieval-Rescue VOR dem Judge

_rescue_chunk_indices + _sentence_chunk_map in _retrieve_claim_contexts: pro Claim werden die Home-Chunks des/der best-belegenden Satzes/Chunks additiv in den Kontext-Pool injiziert, bevor der Judge urteilt. Kein Auto-Relabel -- der Judge entscheidet weiter selbst, der Rescue stellt nur sicher, dass der Beleg im Kontext ist. Zwei komplementaere Pfade (Union):

  1. Semantisch: Chunks nach max. Satz-Cosine zum Claim (lokalisiertes Signal statt Chunk-Mittel; Floor RESCUE_SENTENCE_MIN_COSINE=0.45 gegen Injektion fuer echte Halluzinationen).
  2. Lexikalisch: Chunks nach Content-Token-Overlap (>=4 Buchstaben). Faengt starke Paraphrasen gleicher Sprache, wo die Satz-Cosine niedrig ist (Goldset suehl idx4: Satz-Rang 19, aber Lexik-Rang 1).

F2 -- Titel-/Front-Matter-Chunk-Deprioritisierung

Der erste Chunk (Titelseite/Running-Header/Abstract, z.B. „& Asynchronous Synchronous E-Learning A study of…") dominiert Top-1 fuer ~1/3 der Claims und kollabiert adaptive_k auf k=2. Er wird -- nur wenn front-matter-typisch (Title-Case-Dichte / sehr kurz) -- im Ranking mild gedaempft (RESCUE_TITLE_CHUNK_PENALTY=0.08), nicht entfernt (Abstracts enthalten echte Belege; F1 holt den Chunk bei Bedarf zurueck).

Cross-Lingual-Behandlung

Der Presence-Scorer hat False-Negatives bei DE-Claim <-> EN-Quelle (Cosine ~0,64-0,67 < 0,75). F1 faengt diese doppelt ab: (a) der semantische Pfad nutzt max. Satz-Cosine (kein 0,75-Cut, Floor nur 0,45) und (b) der lexikalische Zweitpfad ueber geteilte Content-Tokens/Eigennamen/Zahlen ist das explizite Cross-Lingual-Netz. Beide Goldset-Cross-Lingual-Anker (idx4 ~0,67, idx13 ~0,64) werden gefangen.

Zitat-Marker-robuste Evidence-Normalisierung

_normalize_for_evidence: PyMuPDF interleaviert hochgestellte Zitat-/Fussnoten-Marker als inline Ziffern-Tokens („… in the social world 5 which implies …", „… science 42 and business 136 …"), die den woertlichen Beleg-Substring-Abgleich brechen (idx13, bates idx6). Kurze freistehende Ziffern-Tokens (1-3 Stellen) werden symmetrisch auf Beleg UND Pool entfernt -> match-neutral fuer echte Zahlen (z.B. „355 Studien"), 4+-stellige Zahlen (Jahre) bleiben erhalten. Produktions-_verify_evidence nutzt ohnehin fuzz.token_set_ratio (robust) -- minimaler Blast-Radius.

Goldset-Recall (#254, deterministisch, kein LLM)

generative/tests/test_retrieval_goldset.py (Evidence-in-Pool-Recall ueber die false_positive_retrieval_miss-Anker):

Recall
VORHER (master, Referenz-Umgebung) 2/10 = 0,200
NACHHER (F1+F2) 10/10 = 1,000

Das @pytest.mark.xfail(strict=True) wurde entfernt, die Assertion recall >= 0.90 ist jetzt hart. Negativ-Kontrolle idx8 (contradicted/true_hallucination) bleibt gruen -- F1 relabelt nicht, der Rescue „repariert" sie nicht weg. Absolute Zahl ist umgebungsabhaengig (README-Caveat); die Abnahme prueft die Verbesserung in DERSELBEN Umgebung.

EVAL_VERSION-Bump + Skew

EVAL_VERSION 4.1 -> 4.2 (Retrieval-Methodik geaendert -> der Kontext-Pool, den der Judge sieht, ist ein anderer). Der Bump invalidiert den content-adressierten Eval-Cache automatisch (EVAL_CACHE_NAMESPACE = eval-v4.2). quality_history.jsonl / bestehende Runs bleiben unveraendert (read-only); der eval_version-Skew ist gewollt sichtbar. Ein fairer Vor/Nach-Vergleich der Halluzinationsrate braucht einen Re-Eval-Sweep unter 4.2, nicht den direkten Vergleich gegen alte 4.1-Zeilen (dokumentiert in calibration/retrieval-goldset/README.md).

Abnahme

  • Goldset-Recall 10/10 = 1,000 (xfail entfernt, harte Assertion gruen), Negativ-Kontrolle gruen.
  • Voll-Suite pytest generative lib/decision_engine/tests shared/tests -q: 5833 passed, 3 skipped (0 Failures). Einzige Test-Anpassung: test_audit_double_call.py-Snapshot eval_version 4.1 -> 4.2 (Retrieval dort gemockt, einzige Diff ist der Versions-String -- keine aufgeweichte Korrektheitspruefung).
  • ruff check + ruff format --check sauber.

Alle Rescue-Schwellen sind ENV-tunbar (am Goldset kalibriert, umgebungsabhaengig).

Closes #232

🤖 Generated with Claude Code

Tilltime added 2 commits July 14, 2026 09:54
…g gegen Retrieval-Miss (#232)

Root Cause (#232): Granularitaets-Mismatch. Das Eval-Retrieval
(_retrieve_claim_contexts) rankt Claims gegen CHUNK-Embeddings (100-180 Tok);
der stuetzende SATZ hat hohe Claim-Cosine, sein Home-Chunk landet aber weit
hinten, weil das Beleg-Signal ueber den Chunk gemittelt wird -> faellt aus
adaptive_k -> der Judge sieht den Beleg nie -> falsch-positives not_in_context
-> aufgeblaehte Halluzinationsrate.

F1 -- Satz-Level-Retrieval-Rescue VOR dem Judge (_rescue_chunk_indices,
_sentence_chunk_map): pro Claim werden die Home-Chunks des/der best-belegenden
Satzes/Chunks ADDITIV in den Kontext-Pool injiziert. KEIN Relabel -- der Judge
entscheidet weiter selbst. Zwei Pfade (Union): (1) semantisch nach max.
Satz-Cosine je Chunk (faengt Cross-Lingual DE<->EN, wo der Chunk-Mittelwert
verwaessert), (2) lexikalisch nach Content-Token-Overlap (faengt starke
Paraphrasen gleicher Sprache, wo die Satz-Cosine niedrig ist -- Cross-Lingual-
Netz gegen die Presence-Scorer-False-Negatives).

F2 -- Titel-/Front-Matter-Chunk-Deprioritisierung: der erste Chunk wird, wenn
front-matter-typisch (Title-Case-Dichte / sehr kurz), im adaptive_k-Ranking
mild gedaempft (nicht entfernt -- Abstracts enthalten echte Belege; F1 holt den
Chunk bei Bedarf zurueck).

Zitat-Marker-robuste Evidence-Normalisierung (_normalize_for_evidence): PyMuPDF
interleaviert hochgestellte Zitat-/Fussnoten-Marker als inline Ziffern-Tokens,
die den woertlichen Beleg-Substring-Abgleich brechen. Kurze freistehende
Ziffern-Tokens (1-3 Stellen) werden symmetrisch entfernt -- match-neutral fuer
echte Zahlen, Jahre bleiben erhalten.

Goldset #254 (test_retrieval_goldset.py): Evidence-in-Pool-Recall
2/10=0.200 (VORHER) -> 10/10=1.000 (NACHHER) in der Referenz-Umgebung.
xfail(strict=True) entfernt, Assertion recall>=0.90 jetzt hart. Negativ-
Kontrolle idx8 (true_hallucination) bleibt gruen.

EVAL_VERSION 4.1 -> 4.2 (Methodik-Aenderung invalidiert den Eval-Cache-
Namespace). quality_history.jsonl unveraendert (read-only); der eval_version-
Skew ist gewollt sichtbar, fairer Vor/Nach-Vergleich braucht Re-Eval-Sweep
(dokumentiert in calibration/retrieval-goldset/README.md). Snapshot in
test_audit_double_call.py auf 4.2 nachgezogen.

Suite: 5833 passed, 3 skipped (pytest -m "not slow"). ruff check + format sauber.
…n/testen (#232)

Nachbesserung nach Kontrolle (NEEDS-FIX): F1 loest die Ueber-Zaehl-Seite, trug
aber ein unbeschriftetes Masking-Risiko auf der Unter-Zaehl-Seite.

1. Ehrliche Doku: Die Floor-/Safety-Behauptung war empirisch falsch. Der
   RESCUE_SENTENCE_MIN_COSINE-Floor (0.45) ist ein OFF-TOPIC-Filter, KEIN
   Halluzinations-Filter -- eine themen-nahe, aber unbelegte Halluzination (max
   Satz-Cosine ~0.64) liegt in derselben Cosine-Range wie echte Cross-Lingual-
   Belege (0.64-0.67) und passiert ihn. Kommentare in eval_quality_v4.py +
   README korrigiert: F1 injiziert (anders als das flag-only Fix B) tatsaechlich
   on-topic Kontext in den Judge-Pool und traegt damit ein Masking-Risiko.

2. Budget-Cap: RESCUE_MAX_CHUNKS=6, an die Dokumentgroesse gekoppelt
   (RESCUE_MAX_DOC_FRACTION=0.30) -> min(6, round(0.30*n_chunks)). Begrenzt die
   injizierte Kontextmenge (Masking-Flaeche). Semantischer + lexikalischer Pfad
   werden vor dem Cap fair verschraenkt (Round-Robin), damit das Cap keinen Pfad
   aushungert. Goldset-Recall mit gebuendeltem Budget neu gemessen: bleibt 10/10.

3. Masking-Richtung testbar: zwei neue Goldset-Anker (Hrastinski) -- eine
   topical_hallucination_probe (fabrizierte, themen-nahe Aussage) und eine
   off_topic_control. Neue Tests belegen ohne Judge: (a) fuer die Probe wird
   on-topic Kontext injiziert, der fabrizierte Beleg landet aber NICHT im Pool,
   und das Budget-Cap greift; (b) Off-Topic bekommt 0 Rescue-Chunks. Beide sind
   aus der FP-Recall-Population ausgeschlossen (Metadaten-Test).

4. Nicht ueberclaimen: klar dokumentiert (Code + README + Test-Docstring), dass
   die volle Masking-Validierung (bleibt hallucination_rate ehrlich?) einen
   Re-Eval-Sweep mit echtem Judge braucht -- Recall-vs-Masking ist ein realer,
   offen benannter Trade-off.

Minor: _normalize_for_evidence-Ziffern-Kommentar praezisiert (match-neutral nur
bei Zahlgleichheit; bei Zahl-Differenz geht Diskriminanz verloren, akzeptabel da
Produktions-Verifikation fuzzy).

Suite: 5834 passed, 3 skipped (pytest -m "not slow"); Goldset 6/6 (-m slow),
Recall 10/10. ruff check + format sauber.
@TillQuandel
TillQuandel merged commit 70fee82 into master Jul 14, 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.

Eval-Verifier: possible_retrieval_miss blaeht Halluzinationsrate auf — 5/6 FP-Anker handverifiziert (Hrastinski, v0.3.143)

2 participants