feat(eu): Faktencheck weist Unsicherheit aus statt alles zu bestaetigen
Der EU-Faktencheck bestaetigte praktisch jede Behauptung. Ueber alle Laeufe seit Lage 42 lag der Anteil offener Punkte bei 6 Prozent, beim Weg ueber die Anthropic-CLI bei 31 Prozent. In acht aufeinanderfolgenden Laeufen zur Ceuta-Lage war er exakt null, achtmal zehn Behauptungen, zehnmal bestaetigt. Bei derselben Lage bewerteten beide Wege dieselben Aussagen verschieden. Der Anthropic-Weg stufte "22 EU-Laender kritisieren Spanien" mit sechs Quellen als unbestaetigt ein, der EU-Weg dieselbe Aussage mit vier Quellen als bestaetigt. Mehr Quellen und trotzdem vorsichtiger, das ist kein Zufall, sondern Bauart. Drei Ursachen, drei Aenderungen. 1. Die Belegsuche kannte nur Bestaetigung. Alle Anfragen waren stuetzend formuliert, wer so sucht findet auch nur Stuetzendes. Ein Anteil der Anfragen (EU_VERIFY_COUNTER_SHARE, Vorgabe 0.3) ist jetzt eine Gegenprobe und sucht gezielt nach Dementi, Richtigstellung, abweichenden Angaben und der Darstellung der Gegenseite. Die Treffer sind im Kontext als [GEGENPROBE] gekennzeichnet. 2. Das Modell konnte Belege nicht wirklich pruefen. Die Belegsuche lief mit abgeschaltetem Volltext, das Modell sah je Treffer nur Ueberschrift und Auszug. Ein Artikel zum Thema wurde damit zum Beleg fuer eine konkrete Zahl. Bis zu EU_VERIFY_FULLTEXT_QUERIES Anfragen holen jetzt den Artikeltext. Dazu die Ansage, dass ein Treffer, der nur das Thema erwaehnt, kein Beleg ist, und dass eine Angabe, die praeziser ist als ihre Quelle, nicht traegt. 3. Die zweite Runde lief nur in eine Richtung. Sie belegte offene Punkte nach, und wenn nichts offen war, lief sie gar nicht. Ausgerechnet der Fall "alles bestaetigt" blieb ungeprueft. Jetzt folgt dort eine Belastungsprobe. Sie sucht ausschliesslich Gegenproben zu den staerksten Behauptungen und darf Bestaetigungen zuruecknehmen, aber niemals vergeben. Zurueckgestuft wird bei Widerspruch, bei Dementi, wenn die Belege die konkrete Angabe gar nicht nennen, wenn die Behauptung praeziser ist als ihre Quellen, oder wenn nur eine einzige Stelle berichtet. Ausserdem darf die Nachhak-Runde einen Status jetzt auch absenken, wenn die Nachrecherche der Behauptung widerspricht. Dabei fiel ein Robustheitsmangel auf. Scheiterte die zweite Runde, etwa weil die Suche nicht erreichbar war, ging der gesamte Faktencheck verloren statt nur die Zusatzpruefung. Die zweite Runde ist jetzt gekapselt, das Ergebnis des ersten Durchgangs bleibt in jedem Fall erhalten. Der Weg ueber die Anthropic-CLI bleibt unveraendert. Neu sind 26 Pruefungen, insgesamt laufen 250 ohne Netzzugriff und ohne Kosten. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
@@ -28,6 +28,9 @@ from config import (
|
||||
EU_RESEARCH_MAX_NEW_PER_ROUND,
|
||||
EU_RESEARCH_MAX_ROUNDS_ADHOC,
|
||||
EU_RESEARCH_MAX_ROUNDS_RESEARCH,
|
||||
EU_VERIFY_COUNTER_SHARE,
|
||||
EU_VERIFY_FULLTEXT_CHARS,
|
||||
EU_VERIFY_FULLTEXT_QUERIES,
|
||||
EU_VERIFY_HITS_PER_QUERY,
|
||||
EU_VERIFY_MAX_ITEMS,
|
||||
EU_VERIFY_MAX_QUERIES,
|
||||
@@ -448,6 +451,13 @@ _SEARCH_NOTE_WITH_RESULTS = (
|
||||
" 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."
|
||||
" EBENSO WICHTIG, so prüfst du einen Beleg: Ein Treffer, der nur das Thema "
|
||||
"erwähnt, ist KEIN Beleg. Die konkrete Aussage mit ihrer Zahl, ihrem Namen oder "
|
||||
"ihrem Datum muss im Titel, im Auszug oder im Artikeltext tatsächlich vorkommen. "
|
||||
"Steht dort nur, dass es die Lage gibt, oder ist die Angabe vager als deine "
|
||||
"Behauptung, dann trägt dieser Treffer sie nicht. Zähle ihn dann nicht mit und "
|
||||
"sage das in der Begründung ehrlich. Lieber ein unbestätigter Punkt mit klarer "
|
||||
"Begründung als eine Bestätigung, die einer Nachprüfung nicht standhält."
|
||||
)
|
||||
_SEARCH_NOTE_NO_RESULTS = (
|
||||
" Stütze dich ausschließlich auf die übergebenen Meldungen und Quellen."
|
||||
@@ -464,16 +474,30 @@ Erzeuge bis zu {max_queries} Websuche-Anfragen. Plane sie GEZIELT je Punkt, nich
|
||||
pauschal für alle zusammen. Eine gute Anfrage macht genau einen dieser Punkte
|
||||
überprüfbar, mit seinen konkreten Zahlen, Namen und Orten.
|
||||
|
||||
Davon sind {counter_queries} Anfragen ausdrücklich GEGENPROBEN. Eine Gegenprobe sucht
|
||||
nicht nach Bestätigung, sondern nach allem, was der Behauptung widerspricht. Also nach
|
||||
Dementis, Richtigstellungen, abweichenden Zahlen anderer Stellen, nach der Darstellung
|
||||
der Gegenseite und nach Berichten, die den Vorgang anders einordnen. Wer nur nach
|
||||
Bestätigung sucht, findet auch nur Bestätigung, und der Faktencheck wird wertlos.
|
||||
Beispiele für Gegenproben: "Behörde dementiert Angaben zu X", "X Zahlen umstritten
|
||||
Kritik", "X widerspricht Darstellung". Markiere sie mit "gegenprobe": true.
|
||||
|
||||
REGELN:
|
||||
- Ordne jeder Anfrage über "for_items" zu, welche Punkte sie belegen soll.
|
||||
- Ordne jeder Anfrage über "for_items" zu, welche Punkte sie prüfen soll.
|
||||
- Decke möglichst viele verschiedene Punkte ab, statt denselben mehrfach zu suchen.
|
||||
- Priorisiere Punkte mit harten, überprüfbaren Angaben (Zahlen, Daten, Zitate, Beschlüsse).
|
||||
- Wähle für die Gegenproben die Punkte aus, bei denen ein Irrtum am schwersten wiegt,
|
||||
also Opferzahlen, Zuschreibungen von Verantwortung, Beschlüsse und Zitate.
|
||||
- Setze "full_content": true bei den Anfragen, deren Punkt sich nur im Artikeltext
|
||||
wirklich prüfen lässt, höchstens bei {fulltext_queries} Anfragen. Bei diesen bekommst
|
||||
du den Artikeltext statt nur der Überschrift.
|
||||
- Nutze die Sprache, in der die Berichterstattung zu erwarten ist. Für Ereignisse in
|
||||
einem Land sind Quellen in dessen Landessprache oft ergiebiger.
|
||||
- Keine Jahreszahlen anhängen, keine Wiederholung derselben Anfrage.
|
||||
{avoid_block}
|
||||
Antworte NUR mit JSON:
|
||||
{{"queries": [{{"q": "suchbegriffe", "market": "{default_market}", "for_items": [1, 2]}}]}}
|
||||
{{"queries": [{{"q": "suchbegriffe", "market": "{default_market}", "for_items": [1, 2],
|
||||
"gegenprobe": false, "full_content": false}}]}}
|
||||
"market" ist "de-de", "en-us" oder "fr-fr". Fremdsprachige Anfragen funktionieren mit "en-us"."""
|
||||
|
||||
|
||||
@@ -518,6 +542,7 @@ async def plan_verification_searches(
|
||||
max_queries: int = EU_VERIFY_MAX_QUERIES,
|
||||
avoid_queries: list[str] | None = None,
|
||||
label: str = "Stützsuche",
|
||||
nur_gegenproben: bool = False,
|
||||
) -> Belegsuche:
|
||||
"""Plant gezielte Belegsuchen zu den uebergebenen Punkten und fuehrt sie aus.
|
||||
|
||||
@@ -544,11 +569,22 @@ async def plan_verification_searches(
|
||||
+ "\n".join(f" {q[:120]}" for q in avoid_queries[:10]) + "\n"
|
||||
)
|
||||
|
||||
# Ein fester Anteil der Anfragen sucht nach Widerspruch statt nach
|
||||
# Bestaetigung. Ohne das sieht der Faktencheck nur stuetzendes Material und
|
||||
# bestaetigt praktisch alles.
|
||||
if nur_gegenproben:
|
||||
anzahl_gegenproben = max_queries
|
||||
else:
|
||||
anzahl_gegenproben = max(1, round(max_queries * EU_VERIFY_COUNTER_SHARE)) if max_queries >= 3 else 0
|
||||
anzahl_volltexte = min(EU_VERIFY_FULLTEXT_QUERIES, max_queries)
|
||||
|
||||
plan_prompt = _VERIFY_QUERY_PROMPT.format(
|
||||
today=datetime.now(TIMEZONE).strftime("%d.%m.%Y"),
|
||||
title=title or "(ohne Titel)",
|
||||
items_block="\n".join(f"{i}. {s[:220]}" for i, s in enumerate(items, 1)),
|
||||
max_queries=max_queries,
|
||||
counter_queries=anzahl_gegenproben,
|
||||
fulltext_queries=anzahl_volltexte,
|
||||
default_market=default_market,
|
||||
avoid_block=avoid_block,
|
||||
)
|
||||
@@ -571,7 +607,12 @@ async def plan_verification_searches(
|
||||
if not text or text.lower() in gesehen:
|
||||
continue
|
||||
gesehen.add(text.lower())
|
||||
queries.append({"q": text, "market": str(q.get("market", "")).strip().lower()})
|
||||
queries.append({
|
||||
"q": text,
|
||||
"market": str(q.get("market", "")).strip().lower(),
|
||||
"gegenprobe": nur_gegenproben or bool(q.get("gegenprobe")),
|
||||
"full_content": bool(q.get("full_content")),
|
||||
})
|
||||
if len(queries) >= max_queries:
|
||||
break
|
||||
|
||||
@@ -584,14 +625,21 @@ async def plan_verification_searches(
|
||||
quellen: list[dict] = []
|
||||
gesehene_urls: set[str] = set()
|
||||
treffer = 0
|
||||
volltexte_genutzt = 0
|
||||
gegenproben_gelaufen = 0
|
||||
for q in queries:
|
||||
will_volltext = q["full_content"] and volltexte_genutzt < anzahl_volltexte
|
||||
try:
|
||||
res = await staan_search(
|
||||
q["q"],
|
||||
market=q["market"] if q["market"] in ("de-de", "en-us", "fr-fr") else default_market,
|
||||
extra_snippets=True, max_snippets=3, full_content=False,
|
||||
extra_snippets=True, max_snippets=3, full_content=will_volltext,
|
||||
)
|
||||
ausgefuehrt.append(q["q"])
|
||||
if will_volltext:
|
||||
volltexte_genutzt += 1
|
||||
if q["gegenprobe"]:
|
||||
gegenproben_gelaufen += 1
|
||||
except StaanError as e:
|
||||
logger.warning("EU-%s: Suche fehlgeschlagen (%s)", label, e)
|
||||
continue
|
||||
@@ -613,23 +661,40 @@ async def plan_verification_searches(
|
||||
"source": r["hostname"] or "",
|
||||
"source_url": url,
|
||||
})
|
||||
lines.append(f"[S{treffer}] {r['title'] or '(ohne Titel)'} | {r['hostname']} | {r['url']}")
|
||||
marke = " [GEGENPROBE]" if q["gegenprobe"] else ""
|
||||
lines.append(
|
||||
f"[S{treffer}]{marke} {r['title'] or '(ohne Titel)'} | {r['hostname']} | {r['url']}")
|
||||
if r.get("snippet"):
|
||||
lines.append(f" Auszug: {r['snippet'][:300]}")
|
||||
for chunk in (r.get("extra_snippets") or [])[:1]:
|
||||
lines.append(f" Auszug: {chunk[:300]}")
|
||||
# Volltext, damit das Modell pruefen kann, ob die Quelle die
|
||||
# Behauptung wirklich traegt, statt aus der Ueberschrift zu schliessen.
|
||||
volltext = (r.get("full_text") or "").strip()
|
||||
if volltext:
|
||||
lines.append(f" Artikeltext: {volltext[:EU_VERIFY_FULLTEXT_CHARS]}")
|
||||
|
||||
total.cost_usd += len(ausgefuehrt) * STAAN_COST_PER_QUERY_USD
|
||||
logger.info("EU-%s: %d Suchen, %d Treffer im Kontextblock", label, len(ausgefuehrt), treffer)
|
||||
logger.info(
|
||||
"EU-%s: %d Suchen (davon %d Gegenproben, %d mit Artikeltext), %d Treffer im Kontextblock",
|
||||
label, len(ausgefuehrt), gegenproben_gelaufen, volltexte_genutzt, treffer)
|
||||
|
||||
if not lines:
|
||||
return Belegsuche("", total, ausgefuehrt, [])
|
||||
|
||||
block = (
|
||||
kopf = (
|
||||
"\n\nERGEBNISSE DER EUROPÄISCHEN WEBSUCHE "
|
||||
"(von unserem System ausgeführt, als unabhängige Zweitquellen nutzbar):\n"
|
||||
+ "\n".join(lines)
|
||||
"(von unserem System ausgeführt, als unabhängige Zweitquellen nutzbar).\n"
|
||||
)
|
||||
if gegenproben_gelaufen:
|
||||
kopf += (
|
||||
"Ein Teil dieser Suchen war eine GEGENPROBE, sie suchte gezielt nach "
|
||||
"Widerspruch, Dementi oder abweichenden Angaben. Diese Treffer sind mit "
|
||||
"[GEGENPROBE] gekennzeichnet. Findet sich dort nichts Widersprechendes, "
|
||||
"stützt das die Behauptung zusätzlich. Findet sich etwas, muss es in die "
|
||||
"Bewertung einfließen.\n"
|
||||
)
|
||||
block = kopf + "\n".join(lines)
|
||||
return Belegsuche(block, total, ausgefuehrt, quellen)
|
||||
|
||||
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren