0001 — LLM-Citations server-seitig binden statt prompt-seitig¶
| Status | accepted |
| Datum | 2026-04-10 |
| Refs | Issue #60, Commits eb045d0/ed64399/db3ada9/6ced7ae, Sub-D Tests |
Kontext¶
Der GWÖ-Antragsprüfer lässt ein LLM (Qwen Plus via DashScope) parlamentarische
Anträge bewerten. Pro Antrag werden via Embedding-Retrieval relevante Chunks
aus den Wahl- und Grundsatzprogrammen der involvierten Parteien zugespielt.
Das LLM gibt im JSON-Output wahlprogramm_scores[*].wahlprogramm.zitate[*]
mit jeweils text, quelle (Source-Label, z.B. "GRÜNE NRW Wahlprogramm
2022, S. 58") und url zurück.
Beim ersten Live-Lauf des Sub-D Citation-Property-Tests (siehe ADR 0003) gegen die Prod-DB wurden drei Halluzinations-Cases gefunden: das LLM hatte Snippets erfunden und unter realen Source-Labels zitiert. Issue #60 wurde geöffnet.
Optionen¶
Drei strukturell unterschiedliche Wege, das LLM zu binden:
Option A — Schärferer Prompt¶
Die ZITATEREGEL im User-Prompt wird verschärft. Konkret:
- Jeder retrievte Chunk wird mit einer ENUM-ID
[Q1],[Q2], … getaggt. - Das LLM wird angewiesen, jedes Zitat per
[Qn]-Tag an einen Chunk zu binden, dentextwörtlich aus diesem Chunk zu kopieren und das Source-Label exakt zu übernehmen. - Top-K wird von 2 → 5 erhöht, damit die "richtige" Seite häufiger im Retrieval-Window landet.
Vorteile: prompt-only, kleiner Diff, keine Server-Pipeline-Änderung.
Nachteile: ENUM-Anker im Prompt ist ein weicher Hint. Das LLM darf
zwar den Snippet aus Chunk Qn nehmen, aber es gibt nichts, was es daran
hindert, die Seite aus Chunk Qm in quelle zu schreiben. Cross-Mix bleibt
möglich.
Option B — Server-seitige Quellen-Rekonstruktion¶
Nach dem LLM-Call werden die emittierten Zitate Server-seitig nachgearbeitet:
- Für jedes Zitat im Output-JSON wird der
textper Substring oder 5-Wort-Anker gegen alle retrievten Chunks gematcht. - Bei Match:
quelleundurlwerden aus dem matchenden Chunk konstruiert und der LLM-Output für diese Felder verworfen. - Bei kein-Match: das ganze Zitat wird verworfen.
Vorteile: strukturell — der LLM hat keine Möglichkeit mehr, eine falsche Quelle anzugeben. Die einzigen zwei Outcomes sind "korrekt zitiert" oder "verworfen".
Nachteile: zusätzlicher Server-seitiger Schritt nach json.loads und
vor Pydantic-Validation. Match-Logik muss konsistent mit Sub-D bleiben.
Option C — Schema-Änderung mit quote_id-Feld¶
Statt quelle direkt zu emittieren, soll das LLM ein neues Feld quote_id
(z.B. "Q3") liefern, aus dem der Server quelle/url rekonstruiert.
Vorteile: explizit schema-modelliert, keine Heuristik.
Nachteile: invasiv (Pydantic-Modell, JSON-Schema, Frontend, DB-Migration); verlässt sich darauf, dass das LLM die ID korrekt setzt — wenn es lügt, ist die Bindung futsch.
Entscheidung¶
A + B kombiniert. Option A liefert die Vorab-Härtung im Prompt — sie fängt den Großteil der Cases (etwa 80%). Option B ist die strukturelle Backstop, die die restlichen 20% (echter Snippet aus Qn, falsche Seite aus Qm) verlässlich abdeckt. Option C wurde verworfen, weil der Schema- Eingriff den Aufwand nicht rechtfertigt, solange B die LLM-Eingabe ohnehin ignoriert.
Konkret in dieser Reihenfolge implementiert:
db3ada9— Option A: ENUM-Anker[Q1]/[Q2]/…im Prompt + Top-K 2→5. Live-Verifikation der drei Original-Cases aus #60: 13/13 ok.- Sub-D Live-Run gegen Prod-DB: 45/46 grün, ein neuer Case (BB 8/673) gefunden mit "echter Snippet, falsche Seite". Beweis dass A allein nicht reicht.
6ced7ae— Option B:embeddings.reconstruct_zitate(data, semantic_quotes)nachjson.loadsund vor Pydantic-Validation. Helpersfind_chunk_for_textund_normalize_for_matchmit identischer Logik wie Sub-D. Bei Match wird_chunk_source_labelund_chunk_pdf_urlangewendet, bei No-Match wird das Zitat gedroppt.- Sub-D Re-Run nach B: 52/52 grün über NRW/LSA/BE/MV/BB/BUND.
Konsequenzen¶
Positiv¶
- Strukturelle Garantie: nach
reconstruct_zitatekann ein im DB gespeichertes Zitat nur noch eines von zwei Dingen sein: korrekt zitiert aus einem real retrievten Chunk, oder gar nicht da. - Sub-D als CI-Gate: das Property-Test-Pattern fängt jede künftige Regression dieser Bug-Klasse.
- Server vertraut LLM nicht mehr für
quelle/url. Künftige Modell- Wechsel oder Prompt-Drift können die Quellen-Korrektheit nicht mehr brechen.
Negativ¶
- Fehlende Match-Heuristik = stilles Verschwinden: Wenn der LLM einen
echten Chunk leicht paraphrasiert (z.B. Komma anders gesetzt), verwirft
Option B das Zitat. Die 5-Wort-Anker-Fallback fängt das in den meisten
Fällen, aber Edge-Cases bleiben. Mitigation: doctest-style Sub-D-Run pro
Deploy + Monitor der
wahlprogramm_scores[*].zitate-Counts pro Assessment. - Code-Pfad-Komplexität: drei Schichten (Prompt-ENUM, Server-Postprocess, Sub-D-Test) statt einer. Dafür ist jede Schicht isoliert testbar.
embeddings.pyhat jetzt mehr Verantwortung als nur Embedding-Retrieval — auch Match-Helpers und Source-Label-Konstruktion. Akzeptabel solange das Modul thematisch geschlossen bleibt; bei weiterem Wachstum alscitations.pyextrahieren.
Folgen für andere ADRs¶
- ADR 0003 (Sub-D Citation-Property-Tests) ist die Test-Säule dieser Entscheidung — wenn 0003 jemals supersedet wird, bricht 0001 ohne den Test-Backstop möglicherweise still.
- Neue Adapter (zukünftige Bundesländer) müssen sich nicht extra um Citation-Korrektheit kümmern; der Server-Postprocess ist adapter-agnostisch.