107 lines
4.5 KiB
Markdown
107 lines
4.5 KiB
Markdown
|
|
# 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
|
|||
|
|
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.
|