#63 B+C: Force-Honesty + UI-Warning bei Score ohne Zitate
Problem: BUND 21/3660 zeigt Score 10/10 für Linke und Grüne, aber null Zitate — der Report sieht aus als sei die Bewertung fundiert, obwohl das LLM mangels indexierter Quellen (linke-grundsatz fehlt) aus Trainingswissen geraten hat. User-Feedback: "Da muss stehen warum." Fix C — Force-Honesty im Prompt: - format_quotes_for_prompt akzeptiert neuen Parameter searched_parties. Parteien, für die kein Chunk retrievt wurde, werden explizit als "KEINE QUELLEN VORHANDEN" markiert, mit der Anweisung "score: 0, zitate: [], Begründung: keine Quellen im Index". - Neue ZITATEREGEL Punkt 5: "Wenn KEINE QUELLEN VORHANDEN → score 0." Das ist die strukturelle Lösung — das LLM darf nicht mehr raten. - analyzer.py: fraktionen-Liste wird an format_quotes_for_prompt als searched_parties durchgereicht. Fix B — UI-Transparenz: - index.html: gelbe Warn-Box (amber, border-left #ffc107) wenn wp.wahlprogramm.score > 0 aber wp.wahlprogramm.zitate.length === 0: "Keine belegbaren Quellen im Index gefunden — Score basiert auf LLM-Einschätzung, nicht auf verifizierten Programm-Stellen." - Wird für bestehende Assessments sofort sichtbar (JS-seitig berechnet), keine DB-Migration nötig. Neue Assessments nach Force-Honesty sollten idealerweise Score=0 haben, aber die Warning ist ein Fallback für den Fall dass das LLM die Prompt-Regel nicht immer 100% befolgt. Fix A (Linke/AfD-Grundsatzprogramme) folgt als separater Commit — sind öffentlich downloadbar, brauchen manuellen Sichtbarkeitscheck. Tests: 194/194 grün (keine Schema-Änderung, nur Prompt + Template). Refs: #63, ADR 0001
This commit is contained in:
parent
45379a2639
commit
92dcd25f73
@ -251,7 +251,9 @@ async def analyze_antrag(text: str, bundesland: str = "NRW", model: str = "qwen-
|
|||||||
semantic_quotes = get_relevant_quotes_for_antrag(
|
semantic_quotes = get_relevant_quotes_for_antrag(
|
||||||
text, fraktionen, bundesland=bundesland, top_k_per_partei=5,
|
text, fraktionen, bundesland=bundesland, top_k_per_partei=5,
|
||||||
)
|
)
|
||||||
quotes_context = format_quotes_for_prompt(semantic_quotes)
|
quotes_context = format_quotes_for_prompt(
|
||||||
|
semantic_quotes, searched_parties=fraktionen,
|
||||||
|
)
|
||||||
except (NameError, AttributeError, TypeError, KeyError):
|
except (NameError, AttributeError, TypeError, KeyError):
|
||||||
# Programmierfehler (z.B. der partei_upper-Refactor-Rest aus
|
# Programmierfehler (z.B. der partei_upper-Refactor-Rest aus
|
||||||
# #55/eb045d0, der zu Issue #60 führte) sollen hart fehlschlagen
|
# #55/eb045d0, der zu Issue #60 führte) sollen hart fehlschlagen
|
||||||
|
|||||||
@ -769,7 +769,10 @@ def reconstruct_zitate(data: dict, semantic_quotes: dict) -> dict:
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def format_quotes_for_prompt(quotes: dict) -> str:
|
def format_quotes_for_prompt(
|
||||||
|
quotes: dict,
|
||||||
|
searched_parties: Optional[list[str]] = None,
|
||||||
|
) -> str:
|
||||||
"""Format quotes for inclusion in LLM prompt.
|
"""Format quotes for inclusion in LLM prompt.
|
||||||
|
|
||||||
Each chunk gets a stable ENUM-ID ([Q1], [Q2], …) and the prompt
|
Each chunk gets a stable ENUM-ID ([Q1], [Q2], …) and the prompt
|
||||||
@ -782,8 +785,14 @@ def format_quotes_for_prompt(quotes: dict) -> str:
|
|||||||
Each quote is annotated with the fully-qualified source (programme
|
Each quote is annotated with the fully-qualified source (programme
|
||||||
name + page) so the LLM cannot fall back on training-set defaults
|
name + page) so the LLM cannot fall back on training-set defaults
|
||||||
when constructing its citations.
|
when constructing its citations.
|
||||||
|
|
||||||
|
Issue #63 erweitert: wenn ``searched_parties`` übergeben wird, werden
|
||||||
|
Parteien, für die **kein** Chunk retrievt wurde, im Prompt explizit
|
||||||
|
als "keine Quellen im Index" markiert. Das LLM wird angewiesen, für
|
||||||
|
diese Parteien ``score: null`` zu setzen statt aus dem Trainingswissen
|
||||||
|
zu raten.
|
||||||
"""
|
"""
|
||||||
if not quotes:
|
if not quotes and not searched_parties:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
lines = ["\n## Relevante Passagen aus Wahl- und Parteiprogrammen\n"]
|
lines = ["\n## Relevante Passagen aus Wahl- und Parteiprogrammen\n"]
|
||||||
@ -801,6 +810,11 @@ def format_quotes_for_prompt(quotes: dict) -> str:
|
|||||||
"ausgeschrieben).\n"
|
"ausgeschrieben).\n"
|
||||||
"4. Wenn kein Chunk wirklich passt: lass das Zitat-Array leer. "
|
"4. Wenn kein Chunk wirklich passt: lass das Zitat-Array leer. "
|
||||||
"Lieber 0 Zitate als ein erfundenes Zitat.\n"
|
"Lieber 0 Zitate als ein erfundenes Zitat.\n"
|
||||||
|
"5. **Wenn für eine Fraktion unten KEINE QUELLEN VORHANDEN "
|
||||||
|
"steht**: setze `score: 0` für `wahlprogramm` UND "
|
||||||
|
"`parteiprogramm` dieser Fraktion und schreibe in die "
|
||||||
|
"`begründung`: 'Keine Quellen im Index — Bewertung nicht "
|
||||||
|
"möglich.' Erfinde KEINEN Score aus dem Trainingswissen.\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
counter = 0
|
counter = 0
|
||||||
@ -821,6 +835,21 @@ def format_quotes_for_prompt(quotes: dict) -> str:
|
|||||||
text = chunk["text"][:500] + "..." if len(chunk["text"]) > 500 else chunk["text"]
|
text = chunk["text"][:500] + "..." if len(chunk["text"]) > 500 else chunk["text"]
|
||||||
lines.append(f'- [Q{counter}] {_chunk_source_label(chunk)}: "{text}"')
|
lines.append(f'- [Q{counter}] {_chunk_source_label(chunk)}: "{text}"')
|
||||||
|
|
||||||
|
# Issue #63: Parteien ohne jegliche retrievte Chunks explizit markieren,
|
||||||
|
# damit das LLM nicht aus Trainingswissen halluziniert.
|
||||||
|
if searched_parties:
|
||||||
|
parties_with_chunks = set(quotes.keys())
|
||||||
|
missing = [p for p in searched_parties if p not in parties_with_chunks]
|
||||||
|
if missing:
|
||||||
|
lines.append("\n### KEINE QUELLEN VORHANDEN\n")
|
||||||
|
lines.append(
|
||||||
|
"Für folgende Fraktionen sind weder Wahl- noch "
|
||||||
|
"Grundsatzprogramm-Passagen im Index vorhanden. "
|
||||||
|
"Bewerte sie mit `score: 0` und `zitate: []`:\n"
|
||||||
|
)
|
||||||
|
for p in missing:
|
||||||
|
lines.append(f"- **{p}**: KEINE QUELLEN — score 0, keine Zitate.")
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1509,6 +1509,20 @@
|
|||||||
</div>
|
</div>
|
||||||
`).join('');
|
`).join('');
|
||||||
|
|
||||||
|
// Issue #63: Transparenz-Warnung bei Score > 0 ohne Zitate.
|
||||||
|
// Differenziert zwischen "Score 0 = keine Quellen" (LLM hat
|
||||||
|
// Force-Honesty befolgt) und "Score > 0 aber 0 Zitate" (LLM
|
||||||
|
// hat trotzdem geratet oder Zitate wurden von reconstruct_zitate
|
||||||
|
// verworfen). Nur bei Scores > 0 warnen, weil Score 0 schon
|
||||||
|
// selbsterklärend ist.
|
||||||
|
const wpScore = wp.wahlprogramm?.score ?? 0;
|
||||||
|
const wpZitateCount = (wp.wahlprogramm?.zitate || []).length;
|
||||||
|
const noQuotesWarning = (wpScore > 0 && wpZitateCount === 0) ? `
|
||||||
|
<div style="margin: 0.5rem 0; padding: 0.5rem; background: #fff3cd; border-left: 3px solid #ffc107; font-size: 0.8rem; color: #856404;">
|
||||||
|
⚠ Keine belegbaren Quellen im Index gefunden — Score basiert auf LLM-Einschätzung, nicht auf verifizierten Programm-Stellen.
|
||||||
|
</div>
|
||||||
|
` : '';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div style="margin: 0.75rem 0; padding: 0.75rem; background: var(--color-bg); border-radius: 4px;">
|
<div style="margin: 0.75rem 0; padding: 0.75rem; background: var(--color-bg); border-radius: 4px;">
|
||||||
<strong>${wp.fraktion}</strong>
|
<strong>${wp.fraktion}</strong>
|
||||||
@ -1519,6 +1533,7 @@
|
|||||||
Parteiprogramm: <strong>${wp.parteiprogramm?.score || '-'}/10</strong>
|
Parteiprogramm: <strong>${wp.parteiprogramm?.score || '-'}/10</strong>
|
||||||
</div>
|
</div>
|
||||||
${wp.wahlprogramm?.begründung ? `<div style="font-size: 0.9rem; color: #555; margin-bottom: 0.5rem;">${wp.wahlprogramm.begründung}</div>` : ''}
|
${wp.wahlprogramm?.begründung ? `<div style="font-size: 0.9rem; color: #555; margin-bottom: 0.5rem;">${wp.wahlprogramm.begründung}</div>` : ''}
|
||||||
|
${noQuotesWarning}
|
||||||
${zitateHtml}
|
${zitateHtml}
|
||||||
</div>
|
</div>
|
||||||
`}).join('');
|
`}).join('');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user