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

@@ -20,6 +20,7 @@ oder URL ableitbar ist, staan selbst liefert keine Datumsangaben.
import asyncio
import json
import logging
from dataclasses import dataclass
from datetime import datetime
from config import (
@@ -305,6 +306,17 @@ async def run_eu_research(
if not action["queries"]:
break
# Ist die Treffer-Obergrenze erreicht, bringen weitere Runden nichts
# mehr. Ohne diesen Abbruch liefe die Schleife bis zur letzten Runde
# weiter und befragte jedes Mal das teuerste Modell, ohne dass danach
# noch eine einzige Suche ausgefuehrt wuerde.
if len(collected) >= MAX_TOTAL_RESULTS:
logger.info(
"EU-Recherche: Suchphase nach Runde %d beendet, Treffer-Obergrenze %d erreicht",
round_no, MAX_TOTAL_RESULTS,
)
break
total.cost_usd += searches_done * STAAN_COST_PER_QUERY_USD
if not collected:
@@ -385,6 +397,9 @@ _SEARCH_NOTE_WITH_RESULTS = (
" Nutze stattdessen die unten angehängten ERGEBNISSE DER EUROPÄISCHEN WEBSUCHE "
"als unabhängige Zweitquellen und zitiere sie als Evidenz, wenn sie eine "
"Behauptung stützen oder widerlegen. Zusätzlich zählen die übergebenen Meldungen."
" WICHTIG: Nenne bei jedem Beleg, auf den du dich stützt, immer die vollständige "
"Adresse (die URL hinter der [S..]-Nummer), nicht nur die Nummer. Ohne Adresse "
"gilt ein Beleg als nicht nachprüfbar."
)
_SEARCH_NOTE_NO_RESULTS = (
" Stütze dich ausschließlich auf die übergebenen Meldungen und Quellen."
@@ -432,6 +447,21 @@ def _add_usage(total: ClaudeUsage, u: ClaudeUsage | None) -> None:
total.duration_ms += u.duration_ms
@dataclass
class Belegsuche:
"""Ergebnis einer gezielten Belegsuche.
block = fertiger Kontextblock fuer den Auftrag, leer wenn nichts gefunden
usage = Verbrauch aus Planung und Suche
queries = tatsaechlich ausgefuehrte Suchanfragen, fuer die Nachrunde
quellen = gefundene Quellen einzeln, fuer die Quellenzuordnung im Faktencheck
"""
block: str
usage: ClaudeUsage
queries: list[str]
quellen: list[dict]
async def plan_verification_searches(
*,
title: str,
@@ -440,22 +470,23 @@ async def plan_verification_searches(
max_queries: int = EU_VERIFY_MAX_QUERIES,
avoid_queries: list[str] | None = None,
label: str = "Stützsuche",
) -> tuple[str, ClaudeUsage, list[str]]:
) -> Belegsuche:
"""Plant gezielte Belegsuchen zu den uebergebenen Punkten und fuehrt sie aus.
Die Planung erfolgt punktbezogen, das planende Modell ordnet jeder Anfrage
zu, welche Punkte sie belegen soll. avoid_queries verhindert, dass eine
Nachrunde dieselben Anfragen noch einmal stellt.
Gibt (kontextblock, verbrauch, ausgefuehrte_anfragen) zurueck. Der
Kontextblock ist leer, wenn nichts gefunden wurde.
Gibt eine Belegsuche zurueck. Der Kontextblock ist leer, wenn nichts
gefunden wurde. Die gefundenen Quellen werden zusaetzlich einzeln
zurueckgegeben, damit die Quellenzuordnung im Faktencheck sie kennt.
"""
from config import CLAUDE_MODEL_FAST
total = ClaudeUsage()
items = [str(s).strip() for s in (search_items or []) if str(s).strip()][:EU_VERIFY_MAX_ITEMS]
if not items:
return "", total, []
return Belegsuche("", total, [], [])
default_market = market_for_language(_iso_from_display(output_language))
avoid_block = ""
@@ -479,7 +510,7 @@ async def plan_verification_searches(
_add_usage(total, plan_usage)
except Exception as e:
logger.warning("EU-%s: Planung der Anfragen fehlgeschlagen (%s), fahre ohne Suche fort", label, e)
return "", total, []
return Belegsuche("", total, [], [])
from agents.researcher import _extract_json_object
obj = _extract_json_object(plan_text or "")
@@ -498,10 +529,11 @@ async def plan_verification_searches(
if not queries:
logger.warning("EU-%s: Planung lieferte keine brauchbaren Anfragen", label)
return "", total, []
return Belegsuche("", total, [], [])
lines: list[str] = []
ausgefuehrt: list[str] = []
quellen: list[dict] = []
gesehene_urls: set[str] = set()
treffer = 0
for q in queries:
@@ -524,6 +556,15 @@ async def plan_verification_searches(
if url:
gesehene_urls.add(url)
treffer += 1
# Die Quelle wird zusaetzlich einzeln gefuehrt, damit die
# Quellenzuordnung im Faktencheck sie wie eine Meldung behandeln
# kann. Ohne das gelten Fakten als unbelegt, die sich allein auf
# diese Belege stuetzen.
quellen.append({
"headline": r["title"] or "",
"source": r["hostname"] or "",
"source_url": url,
})
lines.append(f"[S{treffer}] {r['title'] or '(ohne Titel)'} | {r['hostname']} | {r['url']}")
if r.get("snippet"):
lines.append(f" Auszug: {r['snippet'][:300]}")
@@ -534,14 +575,14 @@ async def plan_verification_searches(
logger.info("EU-%s: %d Suchen, %d Treffer im Kontextblock", label, len(ausgefuehrt), treffer)
if not lines:
return "", total, ausgefuehrt
return Belegsuche("", total, ausgefuehrt, [])
block = (
"\n\nERGEBNISSE DER EUROPÄISCHEN WEBSUCHE "
"(von unserem System ausgeführt, als unabhängige Zweitquellen nutzbar):\n"
+ "\n".join(lines)
)
return block, total, ausgefuehrt
return Belegsuche(block, total, ausgefuehrt, quellen)
def build_eu_prompt(prompt: str, results_block: str = "") -> str:
@@ -571,11 +612,12 @@ async def eu_call_with_search(
total = ClaudeUsage()
results_block = ""
if search_items:
results_block, such_usage, _ = await plan_verification_searches(
suche = await plan_verification_searches(
title=title, search_items=search_items,
output_language=output_language, max_queries=max_queries,
)
_add_usage(total, such_usage)
results_block = suche.block
_add_usage(total, suche.usage)
result, usage = await call_claude(build_eu_prompt(prompt, results_block), tools=None)
_add_usage(total, usage)