LLM-Halluzinationen: drei Citation-Cases mit erfundenen Seiten/Snippets #60

Closed
opened 2026-04-09 11:36:43 +02:00 by tobias · 2 comments
Owner

Befund (2026-04-09, autonomer Roadmap-Run #59)

Erster Live-Lauf von Sub-Issue D (Citation Property-Verification, #54) gegen die Prod-DB im Container hat nach den Test-Härtungen aus Commit b76c08d drei Cases identifiziert, in denen das LLM eine Quelle so zitiert, dass die angegebene PDF-Seite den Snippet nicht enthält — auch nicht über Anker-Match (5-Wort-Sequenz wortwörtlich im PDF). Das sind die übrig gebliebenen 3 von ursprünglich 15 Failures, die anderen 12 waren Test-False-Positives.

Cases

1. NRW GRÜNE Drucksache 18/9605, S. 58 von gruene-nrw-2022

zitiert: 'Wir sollen das Wahlalter auf 16 (Landtag) und 14 (Kommune) absenken.'
PDF S.58:  'deordnung stärken und verbindlich regeln. die kommunen unterstützen wir dabei, befragungen von kindern und jugendlichen durchzuführen, um ihre belange vor ort besser wahrzunehmen ...'

Komplett verschiedene Themen — entweder hat das LLM die Seitenzahl erfunden (das Wahlalter steht möglicherweise auf einer anderen Seite des GRÜNE-NRW-Programms) oder den Snippet konfabuliert. "Wir sollen" als Formulierung deutet auch nicht auf einen LLM-eigenen Programm-Text hin, sondern auf eine Paraphrasierung.

2. NRW BÜNDNIS 90/DIE GRÜNEN Drucksache 18/18100, S. 36 von Grundsatzprogramm 2020

Quelle:    'Grundsatzprogramm BÜNDNIS 90/DIE GRÜNEN 2020, S. 36'
zitiert:   'Plattformen müssen umfassend reguliert werden ... werbs- und Arbeitsbedingungen sicherzustellen.'

Das Bundes-Grundsatzprogramm 2020 hat Inhalt zu Plattform-Regulierung, aber die Seitenzahl trifft offenbar nicht. Möglicherweise rephrased oder cross-referenced aus mehreren Stellen.

3. LSA SPD Drucksache 8/6645, S. 37 von spd-lsa-2021

zitiert: 'Wir Sozialdemokratinnen und Sozialdemokraten ächten Rechtsextremismus, Rassismus und Antisemitismus. ...'
PDF S.37: '37 werden. daher setzen wir uns dafür ein, dass zweitstudiengebühren abgeschafft werden ...'

Komplett anderes Thema. Der Snippet klingt sehr nach SPD-Bundestagsprogramm-Boilerplate, möglicherweise hat das LLM aus seinem Trainings-Wissen geschöpft statt aus dem retrieved chunk.

Hypothesen

  1. LLM ignoriert die Seitenzahl im retrieved chunk und konstruiert eine eigene mit best-guess-Wahl. Die ZITATEREGEL im Prompt fordert wörtliche Übernahme der Quelle, aber der Modell-Compliance ist offenbar < 100%.
  2. LLM cross-referenced aus dem Trainings-Wissen wenn der retrieved chunk nicht ganz auf die Antrag-Frage passt. Das ist genau die Bug-Klasse 7, die Sub-D fangen soll.
  3. Embedding-Retrieval liefert die falschen Chunks und das LLM macht aus dem Resultat einen plausibel klingenden Snippet — das wäre ein Indexing-Bug, nicht ein LLM-Bug.

Acceptance Criteria

  • Per Drucksache: das ursprüngliche Assessment-JSON aus gwoe-antraege.db extrahieren und prüfen, welche Chunks dem LLM tatsächlich übergeben wurden (format_quotes_for_prompt-Output reproduzieren)
  • Wenn Chunks korrekt aber LLM hat anderen Snippet erfunden → Prompt-Härtung: ZITATEREGEL noch strenger formulieren, Quelle als ENUM aus den geretrievten Chunks vorgeben
  • Wenn Chunks falsch → Embedding-Retrieval-Bug: Top-K erhöhen, andere Distanz-Metrik, oder Drucksache-spezifischer Re-Index
  • Re-Run der Sub-D-Tests gegen die Prod-DB, Ziel 0 Failures (oder Cases, die echtes Material für Issue/57-Audit-Folge sind)

Umfang

3 von 36 substring-getesteten Citations = ~8% Halluzinations-Rate auf der getesteten Stichprobe. Die Stichprobe war die letzten 5 Assessments pro aktivem BL aus der Prod-DB. Bei einer realistischen jährlichen Run-Rate von ~200 Anträgen wären das ~16 erfundene Citations pro Jahr, was die Glaubwürdigkeit der Reports schwächt.

Refs: #54, #59 (Sub-D Live-Verifikation), #50 (Umbrella E2E)

## Befund (2026-04-09, autonomer Roadmap-Run #59) Erster Live-Lauf von Sub-Issue D (Citation Property-Verification, #54) gegen die Prod-DB im Container hat nach den Test-Härtungen aus Commit `b76c08d` drei Cases identifiziert, in denen das LLM eine Quelle so zitiert, dass die angegebene PDF-Seite den Snippet **nicht** enthält — auch nicht über Anker-Match (5-Wort-Sequenz wortwörtlich im PDF). Das sind die übrig gebliebenen 3 von ursprünglich 15 Failures, die anderen 12 waren Test-False-Positives. ## Cases ### 1. NRW GRÜNE Drucksache 18/9605, S. 58 von gruene-nrw-2022 ``` zitiert: 'Wir sollen das Wahlalter auf 16 (Landtag) und 14 (Kommune) absenken.' PDF S.58: 'deordnung stärken und verbindlich regeln. die kommunen unterstützen wir dabei, befragungen von kindern und jugendlichen durchzuführen, um ihre belange vor ort besser wahrzunehmen ...' ``` Komplett verschiedene Themen — entweder hat das LLM die Seitenzahl erfunden (das Wahlalter steht möglicherweise auf einer anderen Seite des GRÜNE-NRW-Programms) oder den Snippet konfabuliert. "Wir sollen" als Formulierung deutet auch nicht auf einen LLM-eigenen Programm-Text hin, sondern auf eine Paraphrasierung. ### 2. NRW BÜNDNIS 90/DIE GRÜNEN Drucksache 18/18100, S. 36 von Grundsatzprogramm 2020 ``` Quelle: 'Grundsatzprogramm BÜNDNIS 90/DIE GRÜNEN 2020, S. 36' zitiert: 'Plattformen müssen umfassend reguliert werden ... werbs- und Arbeitsbedingungen sicherzustellen.' ``` Das Bundes-Grundsatzprogramm 2020 hat Inhalt zu Plattform-Regulierung, aber die Seitenzahl trifft offenbar nicht. Möglicherweise rephrased oder cross-referenced aus mehreren Stellen. ### 3. LSA SPD Drucksache 8/6645, S. 37 von spd-lsa-2021 ``` zitiert: 'Wir Sozialdemokratinnen und Sozialdemokraten ächten Rechtsextremismus, Rassismus und Antisemitismus. ...' PDF S.37: '37 werden. daher setzen wir uns dafür ein, dass zweitstudiengebühren abgeschafft werden ...' ``` Komplett anderes Thema. Der Snippet klingt sehr nach SPD-Bundestagsprogramm-Boilerplate, möglicherweise hat das LLM aus seinem Trainings-Wissen geschöpft statt aus dem retrieved chunk. ## Hypothesen 1. **LLM ignoriert die Seitenzahl** im retrieved chunk und konstruiert eine eigene mit best-guess-Wahl. Die ZITATEREGEL im Prompt fordert wörtliche Übernahme der Quelle, aber der Modell-Compliance ist offenbar < 100%. 2. **LLM cross-referenced aus dem Trainings-Wissen** wenn der retrieved chunk nicht ganz auf die Antrag-Frage passt. Das ist genau die Bug-Klasse 7, die Sub-D fangen soll. 3. **Embedding-Retrieval liefert die falschen Chunks** und das LLM macht aus dem Resultat einen plausibel klingenden Snippet — das wäre ein Indexing-Bug, nicht ein LLM-Bug. ## Acceptance Criteria - [ ] Per Drucksache: das ursprüngliche Assessment-JSON aus `gwoe-antraege.db` extrahieren und prüfen, welche Chunks dem LLM tatsächlich übergeben wurden (`format_quotes_for_prompt`-Output reproduzieren) - [ ] Wenn Chunks korrekt aber LLM hat anderen Snippet erfunden → **Prompt-Härtung**: ZITATEREGEL noch strenger formulieren, Quelle als ENUM aus den geretrievten Chunks vorgeben - [ ] Wenn Chunks falsch → **Embedding-Retrieval-Bug**: Top-K erhöhen, andere Distanz-Metrik, oder Drucksache-spezifischer Re-Index - [ ] Re-Run der Sub-D-Tests gegen die Prod-DB, Ziel 0 Failures (oder Cases, die echtes Material für Issue/57-Audit-Folge sind) ## Umfang 3 von 36 substring-getesteten Citations = ~8% Halluzinations-Rate auf der getesteten Stichprobe. Die Stichprobe war die letzten 5 Assessments pro aktivem BL aus der Prod-DB. Bei einer realistischen jährlichen Run-Rate von ~200 Anträgen wären das ~16 erfundene Citations pro Jahr, was die Glaubwürdigkeit der Reports schwächt. Refs: #54, #59 (Sub-D Live-Verifikation), #50 (Umbrella E2E)
Author
Owner

Resolved (2026-04-09 Fortsetzung)

Fix in db3ada9 (vorbereitende Regression-Fix in ed64399).

Diagnose

In-Process-Reproduktion mit get_relevant_quotes_for_antrag() zeigte: die Halluzinationen waren kein Embedding-Bug, sondern reines LLM-Compliance-Problem.

Case Retrieved Top-3 Halluziniertes Zitat Befund
1 NRW 18/9605 GRÜNE S.88, S.58, S.96 Wahlalter 16/14 zu S.58 S.58 retrievt — Chunk-Text handelt von Befragungen, nicht Wahlalter. LLM hat den Snippet aus Trainingswissen über Grüne-Programme rekonstruiert; Seite zufällig korrekt.
2 NRW 18/18100 grüne-grundsatz S.60, S.48, S.94 Plattform-Regulierung zu S.36 S.36 nie retrievt → Seite + Snippet komplett halluziniert.
3 LSA 8/6645 SPD S.30, S.39, S.43 Sozialdemokraten-Boilerplate zu S.37 S.37 nie retrievt → klassisches Pretraining-Boilerplate.

Fix (Variante A + C)

A — ENUM-Anker in format_quotes_for_prompt:

  • Jeder retrievte Chunk bekommt eine [Q1]/[Q2]/...-ID im Prompt.
  • Neue ZITATEREGEL erzwingt: jedes Zitat MUSS auf einen [Qn] verweisen, der text-String MUSS eine wörtliche, zusammenhängende Passage von ≥5 Wörtern aus genau diesem Chunk sein, quelle MUSS exakt das Source-Label des Chunks sein. Wenn kein Chunk passt → leeres zitate-Array.
  • analyzer.get_system_prompt zieht die Regel im System-Prompt nach.

C — Recall-Boost top_k_per_partei 2 → 5. Bringt in Case 2 die zuvor fehlende S.36 ins Window.

Verifikation

Die 3 Drucksachen wurden aus der Prod-DB gelöscht und über die normale /api/analyze-drucksache-Pipeline neu analysiert. Anker-Match-Verifikation (Substring oder 5-Wort-Sequenz vs. tatsächlich retrievte Chunks):

NRW 18/9605:  4 ok, 0 fail
NRW 18/18100: 3 ok, 0 fail
LSA 8/6645:   6 ok, 0 fail
→ 13/13, 0 Halluzinationen

Alle vorher halluzinierten Snippets sind durch verifizierbare Originalzitate ersetzt:

  • Case 1: Wahlalter-Snippet → "Unsere Demokratie hat ein erhebliches Repräsentationsdefizit" (grüne-grundsatz S.75)
  • Case 2: "Plattformen müssen umfassend reguliert werden, um Grundrechte zu schützen" (grüne-grundsatz S.36) — diesmal real-retrievt durch Top-K=5
  • Case 3: "Stärkung einer geschlechtersensiblen Berufsorientierung" (spd-lsa-2021 S.41)

Side-Befund — separater Critical Bug ed64399

Während der Untersuchung gefunden: embeddings.py:528 referenzierte nach dem #55-Refactor noch eine undefinierte partei_upper-Variable. NameError wurde im breiten except Exception in analyzer.py:249 verschluckt → seit eb045d0 (heute 11:22) wären alle Analysen still über den Keyword-Fallback statt Embeddings gelaufen. Im Prod waren noch keine neuen Assessments betroffen (0 neue seit 11:22), aber der Bug hätte alle künftigen Analysen entwertet. Fix:

  • partei_upperpartei_lookup
  • analyzer.py:249 fängt nicht mehr NameError/AttributeError/TypeError/KeyError — Programmierfehler im Embedding-Pfad sollen hart fehlschlagen statt still degradieren
  • Regression-Test in tests/test_embeddings.py

Tests

179/179 grün (177 + 2 neue: ENUM-ID-Format, Regression für partei_upper).

Closing.

## Resolved (2026-04-09 Fortsetzung) Fix in db3ada9 (vorbereitende Regression-Fix in ed64399). ### Diagnose In-Process-Reproduktion mit `get_relevant_quotes_for_antrag()` zeigte: die Halluzinationen waren **kein Embedding-Bug**, sondern reines LLM-Compliance-Problem. | Case | Retrieved Top-3 | Halluziniertes Zitat | Befund | |---|---|---|---| | 1 NRW 18/9605 GRÜNE | S.88, **S.58**, S.96 | Wahlalter 16/14 zu S.58 | S.58 retrievt — Chunk-Text handelt von Befragungen, nicht Wahlalter. LLM hat den Snippet aus Trainingswissen über Grüne-Programme rekonstruiert; Seite zufällig korrekt. | | 2 NRW 18/18100 grüne-grundsatz | S.60, S.48, S.94 | Plattform-Regulierung zu S.36 | S.36 nie retrievt → Seite + Snippet komplett halluziniert. | | 3 LSA 8/6645 SPD | S.30, S.39, S.43 | Sozialdemokraten-Boilerplate zu S.37 | S.37 nie retrievt → klassisches Pretraining-Boilerplate. | ### Fix (Variante A + C) **A — ENUM-Anker** in `format_quotes_for_prompt`: - Jeder retrievte Chunk bekommt eine `[Q1]`/`[Q2]`/...-ID im Prompt. - Neue ZITATEREGEL erzwingt: jedes Zitat MUSS auf einen `[Qn]` verweisen, der `text`-String MUSS eine wörtliche, zusammenhängende Passage von ≥5 Wörtern aus genau diesem Chunk sein, `quelle` MUSS exakt das Source-Label des Chunks sein. Wenn kein Chunk passt → leeres `zitate`-Array. - `analyzer.get_system_prompt` zieht die Regel im System-Prompt nach. **C — Recall-Boost** `top_k_per_partei` 2 → 5. Bringt in Case 2 die zuvor fehlende S.36 ins Window. ### Verifikation Die 3 Drucksachen wurden aus der Prod-DB gelöscht und über die normale `/api/analyze-drucksache`-Pipeline neu analysiert. Anker-Match-Verifikation (Substring oder 5-Wort-Sequenz vs. tatsächlich retrievte Chunks): ``` NRW 18/9605: 4 ok, 0 fail NRW 18/18100: 3 ok, 0 fail LSA 8/6645: 6 ok, 0 fail → 13/13, 0 Halluzinationen ``` Alle vorher halluzinierten Snippets sind durch verifizierbare Originalzitate ersetzt: - Case 1: Wahlalter-Snippet → "Unsere Demokratie hat ein erhebliches Repräsentationsdefizit" (grüne-grundsatz S.75) - Case 2: "Plattformen müssen umfassend reguliert werden, um Grundrechte zu schützen" (grüne-grundsatz S.36) — diesmal real-retrievt durch Top-K=5 - Case 3: "Stärkung einer geschlechtersensiblen Berufsorientierung" (spd-lsa-2021 S.41) ### Side-Befund — separater Critical Bug ed64399 Während der Untersuchung gefunden: `embeddings.py:528` referenzierte nach dem #55-Refactor noch eine undefinierte `partei_upper`-Variable. NameError wurde im breiten `except Exception` in `analyzer.py:249` verschluckt → seit eb045d0 (heute 11:22) wären alle Analysen still über den Keyword-Fallback statt Embeddings gelaufen. Im Prod waren noch keine neuen Assessments betroffen (0 neue seit 11:22), aber der Bug hätte alle künftigen Analysen entwertet. Fix: - `partei_upper` → `partei_lookup` - `analyzer.py:249` fängt nicht mehr `NameError/AttributeError/TypeError/KeyError` — Programmierfehler im Embedding-Pfad sollen hart fehlschlagen statt still degradieren - Regression-Test in `tests/test_embeddings.py` ### Tests 179/179 grün (177 + 2 neue: ENUM-ID-Format, Regression für `partei_upper`). Closing.
tobias reopened this issue 2026-04-09 23:00:47 +02:00
Author
Owner

Reopened — A+C war insuffizient (Sub-D Live-Run)

Direkt nach dem db3ada9-Deploy habe ich Sub-D () gegen die frische Prod-DB-Kopie laufen lassen: 45 passed, 1 failed. Der Fail war kein Pre-Fix-Stale, sondern ein Post-Deploy-Case des A+C-Pfades:

BB 8/673 BSW zitiert "Wertschätzung für Lehrerinnen und Lehrer, Abbau von Arbeitsüberlastung..." als . PDF S.4 ist die Cover-/Adressseite. Volltext-Suche im PDF: der Snippet steht real auf S.27. Reproduktion der Embedding-Retrieval (top_k=5):

→ Das LLM hat Text aus dem S.27-Chunk kopiert, in aber die Seitenzahl des Top-2-Chunks (S.4) geschrieben. Klassischer Cross-Mix zwischen Q-IDs. A+C kann das nicht verhindern, weil das -Tag nur ein weicher Anker im Prompt ist — der LLM darf den Snippet aus Qn nehmen und in was anderes schreiben.

Fix Option B — server-seitige Quellen-Rekonstruktion ()

Neue Funktion läuft im Analyzer nach aber vor Pydantic-Validation:

  1. Flacht alle retrievten Chunks aus zu einer Liste.
  2. Pro Zitat: via Substring oder 5-Wort-Anker gegen alle Chunks matchen (Helpers + , identische Logik wie Sub-D).
  3. Match → und werden server-seitig durch und des matchenden Chunks ÜBERSCHREIBEN. LLM-Output wird ignoriert.
  4. Kein Match → Zitat verworfen.

Damit kann der LLM nur noch sauber zitieren oder gar nicht — es gibt keinen Pfad mehr zu "echter Text, falsche quelle".

Verifikation

Nach Deploy: BB 8/673 + BB 8/310 (gleiches Window, gleicher Bug) + BUND 21/4275/4940/3660 (drei zwischenzeitlich vom User erzeugte Bundestags-Assessments aus dem A+C-only-Window, davon zwei mit identischem Cross-Mix) gelöscht und über die normale -Pipeline neu erzeugt. Sub-D-Re-Run gegen die frische Prod-DB-Kopie:

0 Halluzinationen über alle aktiven Bundesländer (NRW, LSA, BE, MV, BB, BUND, …).

Tests

185/185 grün (179 + 6 neu in ):

  • BB-8/673-Re-Mapping (text-aus-S.27 → quelle wird S.27 statt LLM-emittiertes S.4)
  • Drop bei "Snippet in keinem Chunk auffindbar" (klassische Halluzination)
  • No-op bei leerem
  • 5-Wort-Anker-Fallback bei LLM-Truncation
  • Short-needle Edge-Case (zu schwach für aussagekräftiges Binding)
  • Soft-Hyphen Bridging (PyMuPDF ­-Linebreak-Artefakte)

Closing again.

## Reopened — A+C war insuffizient (Sub-D Live-Run) Direkt nach dem db3ada9-Deploy habe ich Sub-D () gegen die frische Prod-DB-Kopie laufen lassen: **45 passed, 1 failed**. Der Fail war kein Pre-Fix-Stale, sondern ein **Post-Deploy-Case** des A+C-Pfades: **BB 8/673 BSW** zitiert "Wertschätzung für Lehrerinnen und Lehrer, Abbau von Arbeitsüberlastung..." als . PDF S.4 ist die Cover-/Adressseite. Volltext-Suche im PDF: der Snippet steht real auf **S.27**. Reproduktion der Embedding-Retrieval (top_k=5): → Das LLM hat **Text aus dem S.27-Chunk** kopiert, in aber **die Seitenzahl des Top-2-Chunks (S.4)** geschrieben. Klassischer Cross-Mix zwischen Q-IDs. A+C kann das nicht verhindern, weil das -Tag nur ein weicher Anker im Prompt ist — der LLM darf den Snippet aus Qn nehmen und in was anderes schreiben. ## Fix Option B — server-seitige Quellen-Rekonstruktion () Neue Funktion läuft im Analyzer **nach** aber **vor** Pydantic-Validation: 1. Flacht alle retrievten Chunks aus zu einer Liste. 2. Pro Zitat: via Substring oder 5-Wort-Anker gegen alle Chunks matchen (Helpers + , identische Logik wie Sub-D). 3. Match → und werden **server-seitig** durch und des matchenden Chunks ÜBERSCHREIBEN. LLM-Output wird ignoriert. 4. Kein Match → Zitat verworfen. Damit kann der LLM nur noch sauber zitieren oder gar nicht — es gibt keinen Pfad mehr zu "echter Text, falsche quelle". ## Verifikation Nach Deploy: BB 8/673 + BB 8/310 (gleiches Window, gleicher Bug) + BUND 21/4275/4940/3660 (drei zwischenzeitlich vom User erzeugte Bundestags-Assessments aus dem A+C-only-Window, davon zwei mit identischem Cross-Mix) gelöscht und über die normale -Pipeline neu erzeugt. Sub-D-Re-Run gegen die frische Prod-DB-Kopie: → **0 Halluzinationen über alle aktiven Bundesländer** (NRW, LSA, BE, MV, BB, BUND, …). ## Tests 185/185 grün (179 + 6 neu in ): - BB-8/673-Re-Mapping (text-aus-S.27 → quelle wird S.27 statt LLM-emittiertes S.4) - Drop bei "Snippet in keinem Chunk auffindbar" (klassische Halluzination) - No-op bei leerem - 5-Wort-Anker-Fallback bei LLM-Truncation - Short-needle Edge-Case (zu schwach für aussagekräftiges Binding) - Soft-Hyphen Bridging (PyMuPDF ­-Linebreak-Artefakte) Closing again.
Sign in to join this conversation.
No description provided.