fix(analyse): vollstaendigen Bericht nehmen, wenn das Modell sich selbst korrigiert

Der Recherchebericht der EU-Fassung enthielt seit zwei Laeufen nur noch den
Abschnitt ZUSAMMENFASSUNG, 1513 statt der ueblichen mehreren tausend Zeichen.
Die Abschnitte HINTERGRUND, AKTEURE, AKTUELLE LAGE, EINSCHAETZUNG und
QUELLENQUALITAET fehlten vollstaendig.

Ursache. Das Modell lieferte zuerst ein knappes JSON, bemerkte den Fehler
selbst ("Wait, I need to include the full briefing content in the summary
field. Let me redo this properly with all sections") und haengte danach eine
zweite, vollstaendige Fassung an. Unsere Auswertung nahm das erste Objekt und
verwarf alles danach. Der Bericht war also da, wir haben ihn weggeworfen.

Behoben. json_utils bekommt extract_json_objects und extract_json_arrays, die
alle vollstaendigen Bloecke einer Antwort liefern statt nur den ersten. Der
Analyzer waehlt daraus den Bericht mit dem laengsten summary, der Faktencheck
die Liste mit den meisten Fakten. Beide protokollieren, wenn mehrere Fassungen
auftauchen, damit der Fall sichtbar bleibt. Die bisherigen Einzelabfragen
extract_json_object und extract_json_array bleiben unveraendert, damit sich an
den uebrigen Aufrufstellen nichts aendert.

Gegenprobe an der echten Antwort aus Lauf 50. Vorher 1513 Zeichen mit einem
Abschnitt, jetzt 8189 Zeichen mit allen sechs.

Neu sind 16 Pruefungen, insgesamt laufen 96 ohne Netzzugriff und ohne Kosten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Dieser Commit ist enthalten in:
claude-dev
2026-08-01 13:49:15 +00:00
Ursprung 5a08507217
Commit bf967216de
5 geänderte Dateien mit 197 neuen und 11 gelöschten Zeilen

107
tests/test_mehrfachantwort.py Normale Datei
Datei anzeigen

@@ -0,0 +1,107 @@
"""Testet den Umgang mit Antworten, die mehrere Fassungen enthalten.
Modelle korrigieren sich gelegentlich selbst und haengen nach einer ersten,
knappen Antwort eine zweite, vollstaendige an. Wer nur die erste nimmt,
verliert den Grossteil des Berichts. Genau das ist in Lage 49 und 50 passiert,
dort blieb von sechs Abschnitten nur einer uebrig.
Laeuft ohne Netzzugriff und ohne Kosten.
Aufruf aus dem Projektstamm:
venv/bin/python tests/test_mehrfachantwort.py
"""
import os
import re
import sys
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "src"))
from json_utils import ( # noqa: E402
extract_json_object, extract_json_objects, extract_json_array, extract_json_arrays,
)
from agents.analyzer import AnalyzerAgent # noqa: E402
from agents.factchecker import FactCheckerAgent # noqa: E402
ok = 0
fail = 0
def pruefe(name, bedingung, extra=""):
global ok, fail
if bedingung:
ok += 1
print(" OK " + name)
else:
fail += 1
print(" FEHL " + name + " " + str(extra))
KURZ = "## ZUSAMMENFASSUNG\\n\\n- Ein einzelner Punkt."
LANG = ("## ZUSAMMENFASSUNG\\n\\n- Punkt eins.\\n\\n"
"## HINTERGRUND\\n\\nDie Vorgeschichte.\\n\\n"
"## AKTEURE\\n\\nDie Beteiligten.\\n\\n"
"## AKTUELLE LAGE\\n\\nDer Stand.\\n\\n"
"## EINSCHÄTZUNG\\n\\nDie Bewertung.\\n\\n"
"## QUELLENQUALITÄT\\n\\nDie Belege.")
SELBSTKORREKTUR = (
'```json\n{\n "summary": "' + KURZ + '",\n "sources": [{"nr": 1}]\n}\n```\n\n'
'Wait, I need to include the full briefing content in the summary field. '
'Let me redo this properly with all sections.\n\n'
'```json\n{\n "summary": "' + LANG + '",\n "sources": [{"nr": 1}, {"nr": 2}]\n}\n```'
)
print("\nA) Alle Fassungen werden gefunden")
objekte = extract_json_objects(SELBSTKORREKTUR)
pruefe("A1 beide Fassungen erkannt", len(objekte) == 2, len(objekte))
pruefe("A2 erste Fassung ist die knappe",
"HINTERGRUND" not in objekte[0]["summary"])
pruefe("A3 zweite Fassung ist die vollstaendige",
"QUELLENQUALITÄT" in objekte[1]["summary"])
pruefe("A4 Einzelabfrage liefert weiterhin die erste",
"HINTERGRUND" not in extract_json_object(SELBSTKORREKTUR)["summary"])
print("\nB) Der Analyzer waehlt die vollstaendige Fassung")
bericht = AnalyzerAgent()._parse_response(SELBSTKORREKTUR)
pruefe("B1 Objekt erhalten", isinstance(bericht, dict))
abschnitte = re.findall(r"## *([A-ZÄÖÜ ]{4,20})", bericht.get("summary", ""))
pruefe("B2 alle sechs Abschnitte enthalten", len(abschnitte) == 6, abschnitte)
pruefe("B3 Quellen der gewaehlten Fassung", len(bericht.get("sources", [])) == 2,
bericht.get("sources"))
print("\nC) Eine einzelne saubere Antwort bleibt unveraendert")
einfach = '{"summary": "' + LANG + '", "sources": [{"nr": 1}]}'
b2 = AnalyzerAgent()._parse_response(einfach)
pruefe("C1 unveraendert geparst", isinstance(b2, dict) and "QUELLENQUALITÄT" in b2["summary"])
pruefe("C2 nur eine Fassung gefunden", len(extract_json_objects(einfach)) == 1)
print("\nD) Dasselbe fuer Faktenlisten")
FAKTEN_KURZ = '[{"claim": "Nur ein Fakt", "status": "confirmed", "evidence": "e"}]'
FAKTEN_LANG = ('[{"claim": "Erster Fakt", "status": "confirmed", "evidence": "e"},'
' {"claim": "Zweiter Fakt", "status": "confirmed", "evidence": "e"},'
' {"claim": "Dritter Fakt", "status": "confirmed", "evidence": "e"}]')
FC_KORREKTUR = (FAKTEN_KURZ + "\n\nMoment, ich habe Fakten vergessen. Hier vollstaendig:\n\n"
+ FAKTEN_LANG)
listen = extract_json_arrays(FC_KORREKTUR)
pruefe("D1 beide Listen erkannt", len(listen) == 2, len(listen))
pruefe("D2 Einzelabfrage liefert weiterhin die erste",
len(extract_json_array(FC_KORREKTUR)) == 1)
fakten = FactCheckerAgent()._parse_response(FC_KORREKTUR)
pruefe("D3 Faktencheck nimmt die vollstaendige Liste", len(fakten) == 3, len(fakten))
print("\nE) Unbrauchbare Antworten bleiben unbrauchbar")
pruefe("E1 Fliesstext liefert keine Objekte", extract_json_objects("Nur Text") == [])
pruefe("E2 Fliesstext liefert keine Listen", extract_json_arrays("Nur Text") == [])
pruefe("E3 leerer Text ist unkritisch",
extract_json_objects("") == [] and extract_json_arrays("") == [])
print("\nF) Kaputte erste Fassung, brauchbare zweite")
KAPUTT = ('{"summary": "abgeschnitten ' + "\n\n"
+ '{"summary": "' + LANG + '", "sources": []}')
objekte_f = extract_json_objects(KAPUTT)
pruefe("F1 die brauchbare Fassung wird gefunden",
any("QUELLENQUALITÄT" in str(o.get("summary", "")) for o in objekte_f),
len(objekte_f))
print("\nErgebnis: " + str(ok) + " bestanden, " + str(fail) + " fehlgeschlagen")
sys.exit(1 if fail else 0)