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:
@@ -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)
|
||||
|
||||
@@ -551,15 +551,18 @@ class FactCheckerAgent:
|
||||
anzahl_anfragen = EU_VERIFY_MAX_QUERIES if max_queries is None else max_queries
|
||||
anzahl_nachhak = EU_VERIFY_FOLLOWUP_QUERIES if followup_queries is None else followup_queries
|
||||
|
||||
block, such_usage, gestellte_anfragen = await plan_verification_searches(
|
||||
suche = await plan_verification_searches(
|
||||
title=title, search_items=search_items,
|
||||
output_language=output_language, max_queries=anzahl_anfragen,
|
||||
)
|
||||
_add_usage(gesamt, such_usage)
|
||||
_add_usage(gesamt, suche.usage)
|
||||
|
||||
result, usage = await call_claude(build_eu_prompt(prompt, block), tools=None)
|
||||
result, usage = await call_claude(build_eu_prompt(prompt, suche.block), tools=None)
|
||||
_add_usage(gesamt, usage)
|
||||
facts = self._parse_response(result, articles=articles)
|
||||
# Die Suchbelege zaehlen bei der Quellenzuordnung wie Meldungen. Ohne
|
||||
# sie gelten Fakten als unbelegt, die sich allein auf die europaeische
|
||||
# Suche stuetzen, und werden faelschlich herabgestuft.
|
||||
facts = self._parse_response(result, articles=(articles or []) + suche.quellen)
|
||||
|
||||
offene = [f for f in facts if f.get("status") in OFFENE_STATUS]
|
||||
if anzahl_nachhak <= 0 or len(offene) < EU_VERIFY_FOLLOWUP_MIN_OPEN:
|
||||
@@ -569,7 +572,7 @@ class FactCheckerAgent:
|
||||
facts, nachhak_usage = await self._eu_nachhaken(
|
||||
facts=facts, offene=offene, title=title,
|
||||
output_language=output_language, incident_type=incident_type,
|
||||
avoid_queries=gestellte_anfragen, followup_queries=anzahl_nachhak,
|
||||
avoid_queries=suche.queries, followup_queries=anzahl_nachhak,
|
||||
)
|
||||
_add_usage(gesamt, nachhak_usage)
|
||||
return facts, gesamt
|
||||
@@ -600,12 +603,13 @@ class FactCheckerAgent:
|
||||
if not claims:
|
||||
return facts, gesamt
|
||||
|
||||
block, such_usage, _ = await plan_verification_searches(
|
||||
suche = await plan_verification_searches(
|
||||
title=title, search_items=claims, output_language=output_language,
|
||||
max_queries=(EU_VERIFY_FOLLOWUP_QUERIES if followup_queries is None else followup_queries),
|
||||
avoid_queries=avoid_queries, label="Nachhak-Suche",
|
||||
)
|
||||
_add_usage(gesamt, such_usage)
|
||||
block = suche.block
|
||||
_add_usage(gesamt, suche.usage)
|
||||
if not block:
|
||||
logger.info("EU-Nachhak-Runde: keine zusaetzlichen Belege gefunden, Fakten bleiben unveraendert")
|
||||
return facts, gesamt
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren