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-Test — tests/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 integrationgegen 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.dbvom 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.