fix(eu): drei Fehler beheben, die den Ertrag des Faktenchecks aufzehrten

Der Vergleichslauf zur Innenministerkonferenz (Lage 49) zeigte, dass der
verbesserte Faktencheck zwar zehn Fakten lieferte, davon aber nur drei
uebrig blieben. Die Ursachen lagen in drei nachgelagerten Stellen.

1. Quellenzuordnung stufte alle zehn Fakten herab.
   Durch den groesseren Belegblock stuetzt das Modell seine Fakten jetzt
   staerker auf die europaeische Suche als auf die gespeicherten Meldungen.
   Es zitiert dabei die Belegnummern statt vollstaendiger Adressen. Die
   Quellenzuordnung suchte daraufhin vergeblich nach passenden Meldungen und
   stufte jeden Fakt herab. Im Vergleich: Lauf 45 hatte null Herabstufungen,
   Lauf 44 eine, Lauf 49 zehn von zehn.
   Behoben an zwei Stellen. Die Belegsuche gibt die gefundenen Quellen jetzt
   einzeln zurueck, der Faktencheck reicht sie an die Zuordnung durch, und
   der Auftrag verlangt ausdruecklich die vollstaendige Adresse je Beleg.

2. Duplikatpruefung loeschte sieben von zehn Fakten.
   Das Clustering warf inhaltlich verschiedene Beschluesse in eine Gruppe,
   weil viele Behauptungen gleich beginnen. Es gab keinerlei Absicherung, bei
   kleinen Faktenmengen entschied das Modell sogar voellig allein.
   Zwei Sicherungen greifen jetzt. Eine Gruppe, die mehr als 40 Prozent aller
   Fakten umfasst, wird verworfen. Und jeder Loeschkandidat muss dem
   behaltenen Fakt auch rechnerisch aehnlich sein, Grenze 0,55 im selben Mass
   wie im Vorfilter. Beide Werte sind ueber Umgebungsvariablen einstellbar.

3. Leerrunden in der Recherche.
   War die Treffer-Obergrenze erreicht, brach nur die innere Schleife ab. Die
   Rundenschleife lief bis zum Ende weiter und befragte jedes Mal das teuerste
   Modell, ohne noch eine einzige Suche auszufuehren. Das kostete rund 0,17
   USD je Durchgang, bei drei Durchgaengen etwa 0,50 USD pro Aktualisierung.

Der Weg ueber die Anthropic-CLI bleibt unveraendert. Neu sind 16 Pruefungen,
darunter der nachgebildete Fall aus Lage 49, insgesamt laufen jetzt 80
Pruefungen 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:26:32 +00:00
Ursprung c4e19777f9
Commit 0f39154e39
6 geänderte Dateien mit 413 neuen und 30 gelöschten Zeilen

Datei anzeigen

@@ -80,10 +80,11 @@ plan_antwort["queries"] = [
treffer_je_anfrage["Ceuta Opferzahl offiziell"] = treffer(2, "a")
treffer_je_anfrage["Ceuta border deaths toll"] = treffer(2, "b")
treffer_je_anfrage["Sanchez Ceuta declaracion"] = treffer(2, "c")
block, usage, ausgefuehrt = asyncio.run(eur.plan_verification_searches(
_r = asyncio.run(eur.plan_verification_searches(
title="Ceuta", search_items=["57 Tote gemeldet", "Sanchez sprach von Angriff"],
output_language="Deutsch", max_queries=7,
))
block, usage, ausgefuehrt = _r.block, _r.usage, _r.queries
pruefe("A1 alle geplanten Anfragen ausgefuehrt", len(zustand["suchen"]) == 3, zustand["suchen"])
pruefe("A2 ausgefuehrte Anfragen zurueckgemeldet", len(ausgefuehrt) == 3, ausgefuehrt)
pruefe("A3 jeder Punkt steht im Planungsauftrag",
@@ -106,9 +107,10 @@ plan_antwort["queries"] = [
]
treffer_je_anfrage["anfrage eins"] = treffer(3, "x")
treffer_je_anfrage["anfrage zwei"] = treffer(3, "x") # identische URLs
block_b, _, _ = asyncio.run(eur.plan_verification_searches(
_rb = asyncio.run(eur.plan_verification_searches(
title="Ceuta", search_items=["Punkt"], max_queries=7,
))
block_b = _rb.block
pruefe("B1 Dubletten entfernt", block_b.count("[S") == 3, block_b.count("[S"))
pruefe("B2 Nummerierung bleibt luecklos",
all(("[S" + str(i) + "]") in block_b for i in (1, 2, 3)))
@@ -120,10 +122,11 @@ plan_antwort["queries"] = [
{"q": "neue anfrage", "market": "de-de"},
]
treffer_je_anfrage["neue anfrage"] = treffer(2, "n")
block_c, _, ausgefuehrt_c = asyncio.run(eur.plan_verification_searches(
_rc = asyncio.run(eur.plan_verification_searches(
title="Ceuta", search_items=["Punkt"], max_queries=5,
avoid_queries=["alte anfrage"], label="Nachhak-Suche",
))
block_c, ausgefuehrt_c = _rc.block, _rc.queries
pruefe("C1 bereits gestellte Anfrage wird verworfen",
[s["q"] for s in zustand["suchen"]] == ["neue anfrage"], zustand["suchen"])
pruefe("C2 Sperrliste steht im Planungsauftrag", "alte anfrage" in zustand["plan_prompt"])
@@ -134,9 +137,10 @@ neu()
plan_antwort["queries"] = [{"q": "q" + str(i), "market": "de-de"} for i in range(1, 11)]
for i in range(1, 11):
treffer_je_anfrage["q" + str(i)] = treffer(20, "y" + str(i))
block_d, _, ausgefuehrt_d = asyncio.run(eur.plan_verification_searches(
_rd = asyncio.run(eur.plan_verification_searches(
title="Ceuta", search_items=["Punkt"], max_queries=4,
))
block_d, ausgefuehrt_d = _rd.block, _rd.queries
pruefe("D1 hoechstens max_queries Anfragen", len(ausgefuehrt_d) == 4, ausgefuehrt_d)
pruefe("D2 Treffer je Anfrage begrenzt",
block_d.count("[S") == 4 * EU_VERIFY_HITS_PER_QUERY, block_d.count("[S"))
@@ -163,10 +167,11 @@ async def staan_mit_fehler(q, **kw):
eur.staan_search = staan_mit_fehler
block_e, usage_e, ausgefuehrt_e = asyncio.run(eur.plan_verification_searches(
_re = asyncio.run(eur.plan_verification_searches(
title="Ceuta", search_items=["Punkt"], max_queries=5,
))
eur.staan_search = fake_staan
block_e, usage_e, ausgefuehrt_e = _re.block, _re.usage, _re.queries
pruefe("E1 gescheiterte Suche uebersprungen", ausgefuehrt_e == ["heil"], ausgefuehrt_e)
pruefe("E2 gefundene Belege bleiben erhalten", block_e.count("[S") == 2, block_e.count("[S"))
pruefe("E3 nur bezahlte Suchen berechnet",
@@ -174,15 +179,48 @@ pruefe("E3 nur bezahlte Suchen berechnet",
neu()
plan_antwort["queries"] = []
block_f, _, ausgefuehrt_f = asyncio.run(eur.plan_verification_searches(
_rf = asyncio.run(eur.plan_verification_searches(
title="Ceuta", search_items=["Punkt"], max_queries=5,
))
block_f, ausgefuehrt_f = _rf.block, _rf.queries
pruefe("E4 leere Planung liefert leeren Block", block_f == "" and ausgefuehrt_f == [])
block_g, _, _ = asyncio.run(eur.plan_verification_searches(
_rg = asyncio.run(eur.plan_verification_searches(
title="Ceuta", search_items=[], max_queries=5,
))
block_g = _rg.block
pruefe("E5 ohne Pruefpunkte keine Suche", block_g == "")
print("\nF) Gefundene Quellen werden einzeln zurueckgegeben")
neu()
plan_antwort["queries"] = [{"q": "eine anfrage", "market": "de-de"}]
treffer_je_anfrage["eine anfrage"] = treffer(3, "q", host="tagesschau.de")
_rh = asyncio.run(eur.plan_verification_searches(
title="Ceuta", search_items=["Punkt"], max_queries=5,
))
pruefe("F1 jede Quelle einzeln gemeldet", len(_rh.quellen) == 3, len(_rh.quellen))
pruefe("F2 Quelle traegt Titel, Herkunft und Adresse",
all(q.get("headline") and q.get("source") and q.get("source_url") for q in _rh.quellen),
_rh.quellen[:1])
pruefe("F3 Adressen stimmen mit dem Block ueberein",
all(q["source_url"] in _rh.block for q in _rh.quellen))
neu()
plan_antwort["queries"] = [{"q": "a", "market": "de-de"}, {"q": "b", "market": "de-de"}]
treffer_je_anfrage["a"] = treffer(2, "dup")
treffer_je_anfrage["b"] = treffer(2, "dup")
_ri = asyncio.run(eur.plan_verification_searches(
title="Ceuta", search_items=["Punkt"], max_queries=5,
))
pruefe("F4 doppelte Quellen nur einmal gemeldet", len(_ri.quellen) == 2, len(_ri.quellen))
print("\nG) Der Auftrag verlangt vollstaendige Quellenadressen")
from agents.eu_researcher import build_eu_prompt # noqa: E402
mit = build_eu_prompt("Auftrag", "\n\nBELEGE\n[S1] Titel | host | https://x.y/z")
ohne = build_eu_prompt("Auftrag", "")
pruefe("G1 Hinweis auf die Adresspflicht vorhanden",
"vollständige" in mit and "URL" in mit)
pruefe("G2 ohne Belege kein Adresshinweis", "Adresse (die URL" not in ohne)
print("\nErgebnis: " + str(ok) + " bestanden, " + str(fail) + " fehlgeschlagen")
sys.exit(1 if fail else 0)