Zum Inhalt

0003 — Sub-D Property-Verification: Zitate als Substring der zitierten PDF-Seite

Status accepted
Datum 2026-04-10
Refs Issues #50, #54, #60; tests/integration/test_citations_substring.py

Kontext

Der LLM-Output enthält pro Assessment N Zitate, jedes mit text, quelle (z.B. "GRÜNE NRW Wahlprogramm 2022, S. 58") und url. Wahrscheinlich korrekt — aber wie verifizieren wir das, ohne jedes einzeln händisch nachzuschlagen?

Die naheliegenden Test-Optionen sind alle unbefriedigend:

  • Mock-LLM-Tests: prüfen das Schema, sagen aber nichts über die inhaltliche Korrektheit.
  • Snapshot-Tests der LLM-Outputs: drift mit jedem Modell-Update.
  • Manuelles Stichprobenchecken: skaliert nicht über mehrere BLs.

Optionen

Option A — Schema-only Tests (was wir vorher hatten)

Pydantic validiert dass jedes Zitat die Felder text, quelle, url hat und url mit /static/referenzen/ beginnt. Erkennt syntaktische Korruption, aber keine Halluzinationen.

Option B — Property-Test gegen die echten PDFs

Pro Zitat in der Prod-DB: 1. quelle per Token-Coverage-Match auf den PROGRAMME-Eintrag mappen. 2. Seitennummer aus quelle extrahieren. 3. Per fitz die PDF-Seite lesen, Whitespace + Soft-Hyphen normalisieren. 4. text muss als Substring (oder 5-Wort-Anker) in der Seite vorkommen. 5. Bug-Klasse 17 (Cross-Bundesland-Zitat): das aufgelöste Programm muss zum Bundesland des Antrags passen, oder ein Grundsatzprogramm sein.

Vorteile: prüft die einzige Eigenschaft die wirklich zählt — "war das was zitiert wird auch wirklich da". Findet Halluzinationen direkt.

Nachteile: braucht eine lokale Kopie der gwoe-antraege.db und der Wahlprogramm-PDFs. Test ist Pydantic-Schema-übergreifend (Integration, nicht Unit). Skipped sauber wenn DB nicht gemounted ist.

Option C — Online-Verifikation pro Assessment-Insert

Im analyze_antrag-Flow direkt nach LLM-Call jedes Zitat verifizieren und bei Failure abbrechen oder retry.

Vorteile: kein "stale data in DB"-Risiko.

Nachteile: fügt Latenz und Komplexität in den Hot-Path. Die Verifikation ist O(N×M), wo N=Zitate und M=Wahlprogramm-Pages.

Entscheidung

Option B als pytest-Integration-Testtests/integration/test_citations_substring.py, parametrisiert per _load_recent_assessments(limit_per_bl=5) × _flat_zitate().

Strict substring als Default-Match (Whitespace + Soft-Hyphen normalisiert, LLM-Truncation-Marker ... toleriert), 5-Wort-Anker als Fallback für geringfügige Wort-Drift wie "LLM hat mittendrin gekürzt". Min-Length-Guard von 20 Zeichen verhindert false-positive Matches auf trivialen Snippets.

Marker pytestmark = pytest.mark.integration — der Test läuft nicht in der Default-Suite, sondern explizit per pytest -m integration. Skipped wenn webapp/data/gwoe-antraege.db nicht existiert (Dev-Setup ohne DB-Kopie).

Match-Helpers (_normalize, _is_substring, _resolve_quelle_to_programm_id, _extract_page_number) sind eigene Unit-Tests in TestHelpers — die Match- Logik selbst ist nicht-trivial und braucht ihre Eigenkontrolle.

Konsequenzen

Positiv

  • Findet Halluzinationen direkt: Issue #60 wurde durch den ersten Live-Lauf dieses Tests entdeckt (3 von 36 Citations failed), ohne dass ein Mensch Wahlprogramm-PDFs aufmachen musste.
  • Re-runnable als Regression-Gate: nach jedem Deploy einmal pytest -m integration gegen die DB → 0 Failures = OK.
  • Test-Logik = Production-Logik: ADR 0001 Option B (reconstruct_zitate) nutzt identische Match-Heuristiken (find_chunk_for_text, _normalize_for_match). Damit kann der Test nichts fangen, was die Production nicht auch fangen würde, und umgekehrt — kein Test-/Prod-Drift.

Negativ

  • Lokale DB-Kopie nötig: vor jedem Sub-D-Run muss data/gwoe-antraege.db vom Container gepullt werden. CI-Integration steht aus.
  • Test ist langsam-ish: ~50 Citations × ein PDF-Open pro Programm ist bei den ~30 indexierten Programmen ~250ms im Ganzen, nicht trivial aber nicht prohibitiv.
  • Token-Coverage-Heuristik für Quelle-zu-Programm-Mapping kann false- positive bei sehr ähnlichen Programmen werden (z.B. CDU NRW 2022 vs. CDU Niedersachsen 2022 — würde durch Bundesland-Bonus-Check abgefangen).

Folgen für andere ADRs

  • ADR 0001 ist von ADR 0003 abhängig — wenn dieser Test entfernt würde, hätte der LLM-Citation-Postprocess keinen Backstop und neue Halluzinations- Bug-Klassen würden still durchrutschen.