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

Datei anzeigen

@@ -1112,10 +1112,23 @@ Antworte AUSSCHLIESSLICH mit dem Markdown-Text der Bullets, ohne Einleitung, ohn
# Haeufigster Bruch (deutsche Zitate mit geradem Schlusszeichen); ohne
# diesen Schritt rettete der Regex-Fallback unten nur ein Bruchstueck
# und das Lagebild brach still mitten im Satz ab.
from json_utils import extract_json_object as _forgiving_object
repaired_obj = _forgiving_object(response)
if isinstance(repaired_obj, dict) and "summary" in repaired_obj:
return repaired_obj
# Enthaelt die Antwort mehrere Objekte, weil das Modell sich selbst
# korrigiert hat ("Let me redo this properly with all sections"), zaehlt
# das ausfuehrlichste. Sonst landete nur die erste, kurze Fassung in der
# Datenbank und der Bericht verlor fuenf von sechs Abschnitten.
from json_utils import extract_json_objects as _forgiving_objects
kandidaten = [o for o in _forgiving_objects(response)
if isinstance(o, dict) and "summary" in o]
if kandidaten:
bester = max(kandidaten, key=lambda o: len(str(o.get("summary") or "")))
if len(kandidaten) > 1:
logger.warning(
"Analyse enthielt %d Berichtsfassungen, nehme die ausfuehrlichste "
"(%d statt %d Zeichen)",
len(kandidaten), len(str(bester.get("summary") or "")),
len(str(kandidaten[0].get("summary") or "")),
)
return bester
# Versuch 3: Abgeschnittenes JSON reparieren (haeufig bei langen Antworten)
candidate = match.group() if match else cleaned

Datei anzeigen

@@ -595,7 +595,7 @@ class FactCheckerAgent:
plan_verification_searches, build_eu_prompt, _add_usage,
)
from config import EU_VERIFY_FOLLOWUP_QUERIES
from json_utils import extract_json_array
from json_utils import extract_json_arrays
gesamt = ClaudeUsage()
kandidaten = offene[:MAX_FACTS_PER_VERIFY_GROUP]
@@ -634,10 +634,13 @@ class FactCheckerAgent:
logger.warning("EU-Nachhak-Runde fehlgeschlagen (%s), Fakten bleiben unveraendert", e)
return facts, gesamt
aktualisierungen = extract_json_array(result or "")
if not isinstance(aktualisierungen, list):
# Korrigiert sich das Modell selbst und haengt eine zweite Fassung an,
# zaehlt die inhaltsreichste, nicht die erste.
fassungen = extract_json_arrays(result or "")
if not fassungen:
logger.warning("EU-Nachhak-Runde: Antwort nicht auswertbar, Fakten bleiben unveraendert")
return facts, gesamt
aktualisierungen = max(fassungen, key=len)
hochgestuft = 0
for eintrag in aktualisierungen:
@@ -1060,10 +1063,19 @@ class FactCheckerAgent:
# Nicht maskierte Anfuehrungszeichen im Text reparieren (deutsche Zitate
# in claim/evidence). Ohne diesen Schritt lieferte der Faktencheck bei
# einem einzigen Stoerzeichen still eine leere Liste.
from json_utils import extract_json_array as _forgiving_array
repaired = _forgiving_array(response)
if isinstance(repaired, list):
return self._validate_facts(repaired, articles=articles)
# Liefert das Modell mehrere Fassungen, weil es sich selbst korrigiert
# hat, zaehlt die mit den meisten Fakten.
from json_utils import extract_json_arrays as _forgiving_arrays
fassungen = _forgiving_arrays(response)
if fassungen:
beste = max(fassungen, key=len)
if len(fassungen) > 1:
logger.warning(
"Faktencheck-Antwort enthielt %d Fassungen, nehme die mit den "
"meisten Fakten (%d statt %d)",
len(fassungen), len(beste), len(fassungen[0]),
)
return self._validate_facts(beste, articles=articles)
logger.warning(
"Konnte Faktencheck-Antwort nicht als JSON parsen (Laenge: %d, Anfang: %r)",

Datei anzeigen

@@ -170,6 +170,59 @@ def _extract_block(text: str, opener: str, closer: str, want: type) -> Any | Non
return None
def _extract_all_blocks(text: str, opener: str, closer: str, want: type) -> list:
"""Alle vollstaendigen JSON-Bloecke der gewuenschten Art, in Reihenfolge.
Modelle korrigieren sich gelegentlich selbst und haengen nach einer ersten
Antwort eine zweite, vollstaendigere an, etwa mit einem Hinweis wie "Let me
redo this properly". Wer nur den ersten Block nimmt, verliert die bessere
Fassung.
"""
if not text:
return []
for candidate in (text, repair_json_text(text)):
decoder = json.JSONDecoder()
gefunden = []
idx = 0
while True:
start = candidate.find(opener, idx)
if start == -1:
break
try:
obj, ende = decoder.raw_decode(candidate, start)
except (json.JSONDecodeError, ValueError):
idx = start + 1
continue
if isinstance(obj, want):
gefunden.append(obj)
idx = ende
else:
idx = start + 1
if gefunden:
return gefunden
if candidate is not text:
break
return []
def extract_json_objects(text: str) -> list[dict]:
"""Alle vollstaendigen JSON-Objekte im Text, mit Reparatur-Rueckfall."""
cleaned = _strip_fences(text)
direct = loads_forgiving(cleaned)
if isinstance(direct, dict):
return [direct]
return _extract_all_blocks(text, "{", "}", dict)
def extract_json_arrays(text: str) -> list[list]:
"""Alle vollstaendigen JSON-Arrays im Text, mit Reparatur-Rueckfall."""
cleaned = _strip_fences(text)
direct = loads_forgiving(cleaned)
if isinstance(direct, list):
return [direct]
return _extract_all_blocks(text, "[", "]", list)
def extract_json_object(text: str) -> dict | None:
"""Erstes vollstaendiges JSON-Objekt im Text, mit Reparatur-Rueckfall."""
cleaned = _strip_fences(text)