From e440ef93d1c6362202a7e57035b9dde3e0cf4620 Mon Sep 17 00:00:00 2001 From: claude-dev Date: Sun, 2 Aug 2026 11:47:56 +0000 Subject: [PATCH] fix(faktencheck): Zahl der Faktenaussagen richtet sich nach dem Material In allen Auftraegen stand fest "5-10 wichtigste Faktenaussagen", bei der Aktualisierung "3-5 neue". Die Zahl hatte damit nichts mit der Menge des Materials zu tun. Aus 16 Meldungen wurden genauso viele Fakten wie aus 91. Beide Wege lasen dieselbe Vorgabe verschieden. Der Weg ueber die Anthropic-CLI behandelte sie als Richtwert und lag zwischen 9 und 21 Fakten, ueberschritt die Zehn also regelmaessig. Das Modell auf dem EU-Weg las sie als Zielvorgabe und traf sie in neun von siebzehn Laeufen exakt. Daher wirkten die EU-Berichte so gleichfoermig, nicht die Lage bestimmte den Umfang, sondern die Zahl im Text. Bei der Ceuta-Lage bedeutete das, dass zehn Punkte eine europaweite Krise abbilden mussten, in der Opferzahlen, die schwimmende Barriere, das Sondertreffen der Innenminister, Italiens Schengen-Aussetzung, der offene Brief von 22 Staaten, die Rueckfuehrungen und mehrere Regierungsaeusserungen vorkamen. faktenspanne() berechnet den Richtwert jetzt aus der Zahl der Meldungen, grob eine Aussage je fuenf Meldungen, begrenzt auf 5 bis 20 bei der Erstpruefung und 3 bis 10 bei der Aktualisierung. 16 Meldungen ergeben weiterhin 5 bis 10, 88 Meldungen ergeben 9 bis 18. Alle Werte sind ueber Umgebungsvariablen einstellbar. Dazu die Formulierung. Die Spanne ist jetzt ausdruecklich als Richtwert und nicht als Zielvorgabe bezeichnet, mit der Erlaubnis, weniger zu nennen wenn das Material nicht mehr hergibt, und mehr wenn sonst wichtige Punkte fehlen wuerden. Betroffen sind die vier Faktencheck-Vorlagen und die Triage. Neu sind 25 Pruefungen, insgesamt laufen 279 ohne Netzzugriff und ohne Kosten. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 1 + src/agents/factchecker.py | 62 ++++++++++++++++-- src/config.py | 13 ++++ tests/test_faktenspanne.py | 125 +++++++++++++++++++++++++++++++++++++ 4 files changed, 196 insertions(+), 5 deletions(-) create mode 100644 tests/test_faktenspanne.py diff --git a/CLAUDE.md b/CLAUDE.md index 9e75b32..b3ec33c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -162,6 +162,7 @@ tests/: hinweis: "Laufen ohne Netzzugriff und ohne Kosten, Modell, Suche und Datenbank sind durch Testdoubles ersetzt. Aufruf aus dem Projektstamm, zum Beispiel venv/bin/python tests/test_eu_factcheck.py" test_eu_factcheck.py: "EU-Faktencheck, Nachhak-Runde, Quellenzuordnung, plus Regressionsschutz fuer den Anthropic-Weg" test_eu_belegsuche.py: "Gezielte Belegsuche ueber staan, Planung, Grenzen, Entdopplung, Ausfallverhalten" + test_faktenspanne.py: "Richtwert fuer die Zahl der Faktenaussagen, waechst mit der Menge der Meldungen" test_qc_und_runden.py: "Absicherung der Duplikatpruefung und Abbruch der Suchrunden bei erreichter Treffergrenze" test_mehrfachantwort.py: "Antworten, in denen sich das Modell selbst korrigiert und mehrere Fassungen liefert, die ausfuehrlichste muss gewinnen" test_faktencheck_belegtiefe.py: "Belegzaehlung nach Medienhaus, Nachbedingung fuer bestaetigte Fakten, Sperre gegen das Hochstufen von developing, getrennte Bezeichnungen fuer Widerspruch und Widerlegung" diff --git a/src/agents/factchecker.py b/src/agents/factchecker.py index 0d05b0f..8835bb3 100644 --- a/src/agents/factchecker.py +++ b/src/agents/factchecker.py @@ -29,7 +29,10 @@ STRENGE REGELN - KEINE HALLUZINATIONEN: - Lieber "unconfirmed" als falsch bestätigt AUFTRAG: -1. Identifiziere die 5-10 wichtigsten Faktenaussagen aus den Meldungen +1. Identifiziere die wichtigsten Faktenaussagen aus den Meldungen. Als Richtwert + sind das etwa {fakten_min} bis {fakten_max} Aussagen. Das ist ein Richtwert und keine + Zielvorgabe. Nenne weniger, wenn das Material nicht mehr hergibt, und mehr, wenn + sonst wichtige Punkte der Lage fehlen wuerden. 2. Prüfe jeden Claim aktiv per WebSearch gegen mindestens eine weitere unabhängige Quelle 3. Kategorisiere jede Aussage: - "confirmed": Mindestens 2 unabhängige Medienhäuser mit überprüfbarer URL bestätigen die Aussage @@ -71,7 +74,10 @@ STRENGE REGELN - KEINE HALLUZINATIONEN: AUFTRAG: Fokus: "Was sind die gesicherten Fakten zu diesem Thema?" -1. Identifiziere die 5-10 wichtigsten Faktenaussagen aus den Quellen +1. Identifiziere die wichtigsten Faktenaussagen aus den Quellen. Als Richtwert + sind das etwa {fakten_min} bis {fakten_max} Aussagen. Das ist ein Richtwert und keine + Zielvorgabe. Nenne weniger, wenn das Material nicht mehr hergibt, und mehr, wenn + sonst wichtige Punkte der Lage fehlen wuerden. 2. Prüfe jeden Claim aktiv per WebSearch gegen weitere unabhängige Quellen 3. Kategorisiere jede Aussage: - "established": Breit dokumentierter Fakt, 3+ unabhängige Medienhäuser mit URL @@ -115,7 +121,8 @@ STRENGE REGELN - KEINE HALLUZINATIONEN: AUFTRAG: 1. Prüfe ob die neuen Meldungen bereits geprüfte Fakten BESTÄTIGEN, WIDERLEGEN oder ERGÄNZEN 2. Aktualisiere den Status bestehender Fakten wenn nötig (z.B. "unconfirmed" → "confirmed") -3. Identifiziere 3-5 NEUE Faktenaussagen aus den neuen Meldungen +3. Identifiziere NEUE Faktenaussagen aus den neuen Meldungen, als Richtwert etwa + {fakten_min} bis {fakten_max}. Auch das ist ein Richtwert, keine Zielvorgabe. 4. Prüfe neue Claims per WebSearch gegen unabhängige Quellen 5. Markiere wichtige Statusänderungen und neue Entwicklungen mit is_notification: true @@ -159,7 +166,8 @@ STRENGE REGELN - KEINE HALLUZINATIONEN: AUFTRAG: 1. Prüfe ob die neuen Quellen bereits geprüfte Fakten bestätigen, widerlegen oder ergänzen 2. Aktualisiere den Status bestehender Fakten wenn nötig -3. Identifiziere 3-5 NEUE Faktenaussagen aus den neuen Quellen +3. Identifiziere NEUE Faktenaussagen aus den neuen Quellen, als Richtwert etwa + {fakten_min} bis {fakten_max}. Auch das ist ein Richtwert, keine Zielvorgabe. 4. Prüfe neue Claims per WebSearch Status-Kategorien: @@ -198,7 +206,8 @@ Analysiere die neuen Artikel und identifiziere: betroffen sein? (neue Bestätigung, Widerlegung, neue Evidenz, Status-Update nötig) 2. NEUE FAKTENAUSSAGEN: Welche neuen prüfbaren Faktenaussagen enthalten die Artikel, - die noch nicht in den bestehenden Fakten erfasst sind? (3-5 neue Claims) + die noch nicht in den bestehenden Fakten erfasst sind? Als Richtwert etwa + {fakten_min} bis {fakten_max} neue Claims, je nachdem was die Artikel hergeben. 3. GRUPPIERUNG: Gruppiere verwandte Fakten thematisch für die parallele Batch-Verarbeitung. Verwandte Fakten MÜSSEN in derselben Gruppe sein. Max {max_per_group} Fakten pro Gruppe. @@ -398,6 +407,34 @@ BEWERTUNGSREGELN (verbindlich, sie gehen allen Angaben oben vor): keine Pflicht zur Erfindung.""" +def faktenspanne(anzahl_meldungen: int, neu: bool = False) -> tuple[int, int]: + """Richtwert fuer die Zahl der Faktenaussagen, gemessen am Material. + + Frueher stand in allen Auftraegen fest "5-10 wichtigste Faktenaussagen". + Das fuehrte dazu, dass aus 16 Meldungen genauso viele Fakten wurden wie aus + 91, obwohl die groessere Lage sichtbar mehr pruefbare Punkte hergab. Das + Modell auf dem EU-Weg traf die Zehn in neun von siebzehn Laeufen exakt, es + las die Spanne als Zielvorgabe. Der Weg ueber die Anthropic-CLI behandelte + sie als Richtwert und lag zwischen neun und einundzwanzig. + + neu=True gilt fuer die Aktualisierung, dort zaehlen nur die neu + hinzugekommenen Meldungen und der Korridor ist enger. + """ + from config import ( + FAKTEN_JE_ARTIKEL, FAKTEN_MAX, FAKTEN_MIN, + FAKTEN_NEU_JE_ARTIKEL, FAKTEN_NEU_MAX, FAKTEN_NEU_MIN, + ) + + je_artikel = FAKTEN_NEU_JE_ARTIKEL if neu else FAKTEN_JE_ARTIKEL + kleinstwert = FAKTEN_NEU_MIN if neu else FAKTEN_MIN + groesstwert = FAKTEN_NEU_MAX if neu else FAKTEN_MAX + + aus_material = round(max(0, anzahl_meldungen) / max(1, je_artikel)) + obergrenze = max(kleinstwert * 2, min(groesstwert, aus_material)) + untergrenze = max(kleinstwert, obergrenze // 2) + return untergrenze, obergrenze + + def bewertungsregeln() -> str: """Verbindlicher Regelblock, wird an jeden Faktencheck-Auftrag angehaengt. @@ -1055,10 +1092,16 @@ class FactCheckerAgent: articles_text = self._format_articles_text(articles) template = RESEARCH_FACTCHECK_PROMPT_TEMPLATE if incident_type == "research" else FACTCHECK_PROMPT_TEMPLATE + fakten_min, fakten_max = faktenspanne(len(articles)) + logger.info( + "Faktencheck: %d Meldungen, Richtwert %d bis %d Faktenaussagen", + len(articles), fakten_min, fakten_max) prompt = template.format( title=title, articles_text=articles_text, output_language=output_language, + fakten_min=fakten_min, + fakten_max=fakten_max, ) + bewertungsregeln() try: @@ -1105,11 +1148,17 @@ class FactCheckerAgent: else: template = INCREMENTAL_FACTCHECK_PROMPT_TEMPLATE + fakten_min, fakten_max = faktenspanne(len(new_articles), neu=True) + logger.info( + "Inkrementeller Faktencheck: %d neue Meldungen, Richtwert %d bis %d neue Aussagen", + len(new_articles), fakten_min, fakten_max) prompt = template.format( title=title, articles_text=articles_text, existing_facts_text=existing_facts_text, output_language=output_language, + fakten_min=fakten_min, + fakten_max=fakten_max, ) + bewertungsregeln() try: @@ -1164,6 +1213,7 @@ class FactCheckerAgent: articles_text = self._format_articles_text(new_articles, max_articles=15) from config import CLAUDE_MODEL_FAST + triage_min, triage_max = faktenspanne(len(new_articles), neu=True) triage_prompt = TRIAGE_PROMPT_TEMPLATE.format( output_language=output_language, fact_count=len(existing_facts), @@ -1171,6 +1221,8 @@ class FactCheckerAgent: article_count=len(new_articles), articles_text=articles_text, max_per_group=MAX_FACTS_PER_VERIFY_GROUP, + fakten_min=triage_min, + fakten_max=triage_max, ) try: diff --git a/src/config.py b/src/config.py index 95f642a..7f4632f 100644 --- a/src/config.py +++ b/src/config.py @@ -140,6 +140,19 @@ EU_VERIFY_COUNTER_SHARE = float(os.environ.get("EU_VERIFY_COUNTER_SHARE", "0.3") EU_VERIFY_FULLTEXT_QUERIES = int(os.environ.get("EU_VERIFY_FULLTEXT_QUERIES", "3")) EU_VERIFY_FULLTEXT_CHARS = int(os.environ.get("EU_VERIFY_FULLTEXT_CHARS", "1800")) +# Wie viele Faktenaussagen ein Faktencheck aufstellen soll. Der Richtwert waechst +# mit der Menge des Materials. Eine feste Obergrenze von zehn fuehrte dazu, dass +# aus 16 Meldungen genauso viele Fakten wurden wie aus 91, obwohl die Lage im +# zweiten Fall deutlich mehr pruefbare Punkte hergibt. +FAKTEN_JE_ARTIKEL = int(os.environ.get("FAKTEN_JE_ARTIKEL", "5")) +FAKTEN_MIN = int(os.environ.get("FAKTEN_MIN", "5")) +FAKTEN_MAX = int(os.environ.get("FAKTEN_MAX", "20")) +# Dasselbe fuer die Aktualisierung, dort zaehlen nur die neu hinzugekommenen +# Meldungen. +FAKTEN_NEU_JE_ARTIKEL = int(os.environ.get("FAKTEN_NEU_JE_ARTIKEL", "5")) +FAKTEN_NEU_MIN = int(os.environ.get("FAKTEN_NEU_MIN", "3")) +FAKTEN_NEU_MAX = int(os.environ.get("FAKTEN_NEU_MAX", "10")) + # --- Verbrauchssaetze (Credits je Aktion) ------------------------------------- # Beschlossen 2026-07-23, der Verbrauchsrechner im Verwaltungsportal nutzt # dieselben Werte: Live-Lauf 45, Recherche je Durchlauf 40 (das Anlegen faehrt diff --git a/tests/test_faktenspanne.py b/tests/test_faktenspanne.py new file mode 100644 index 0000000..1e42113 --- /dev/null +++ b/tests/test_faktenspanne.py @@ -0,0 +1,125 @@ +"""Testet, dass die Zahl der Faktenaussagen mit dem Material waechst. + +Laeuft ohne Netzzugriff und ohne Kosten, der Modellaufruf ist ersetzt. + +Aufruf aus dem Projektstamm: + venv/bin/python tests/test_faktenspanne.py +""" +import asyncio +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "src")) + +import agents.factchecker as fcmod # noqa: E402 +import agents.claude_client as cc # noqa: E402 +from agents.factchecker import ( # noqa: E402 + FactCheckerAgent, faktenspanne, FACTCHECK_PROMPT_TEMPLATE, + RESEARCH_FACTCHECK_PROMPT_TEMPLATE, INCREMENTAL_FACTCHECK_PROMPT_TEMPLATE, + INCREMENTAL_RESEARCH_FACTCHECK_PROMPT_TEMPLATE, TRIAGE_PROMPT_TEMPLATE, +) +from agents.claude_client import _ai_backend_var, ClaudeUsage # noqa: E402 +from config import FAKTEN_MAX, FAKTEN_MIN, FAKTEN_NEU_MAX, FAKTEN_NEU_MIN # noqa: E402 + +ok = 0 +fail = 0 + + +def pruefe(name, bedingung, extra=""): + global ok, fail + if bedingung: + ok += 1 + print(" OK " + name) + else: + fail += 1 + print(" FEHL " + name + " " + str(extra)) + + +print("\nA) Die Spanne waechst mit der Zahl der Meldungen") +klein = faktenspanne(16) +mittel = faktenspanne(53) +gross = faktenspanne(88) +sehr_gross = faktenspanne(300) +pruefe("A1 wenige Meldungen ergeben die alte Spanne", klein == (5, 10), klein) +pruefe("A2 mehr Meldungen ergeben mehr Fakten", gross[1] > klein[1], (klein, gross)) +pruefe("A3 die Reihenfolge stimmt", + klein[1] <= mittel[1] <= gross[1] <= sehr_gross[1], + (klein, mittel, gross, sehr_gross)) +pruefe("A4 Obergrenze wird eingehalten", sehr_gross[1] == FAKTEN_MAX, sehr_gross) +pruefe("A5 Untergrenze wird eingehalten", faktenspanne(0)[0] == FAKTEN_MIN, faktenspanne(0)) +pruefe("A6 Untergrenze liegt nie ueber der Obergrenze", + all(faktenspanne(n)[0] <= faktenspanne(n)[1] for n in range(0, 400, 7))) +pruefe("A7 negative Eingabe bricht nicht", faktenspanne(-5) == (5, 10), faktenspanne(-5)) + +print("\nB) Fuer die Aktualisierung gilt ein engerer Korridor") +neu_klein = faktenspanne(8, neu=True) +neu_gross = faktenspanne(200, neu=True) +pruefe("B1 wenige neue Meldungen ergeben die alte Spanne", + neu_klein == (FAKTEN_NEU_MIN, FAKTEN_NEU_MIN * 2), neu_klein) +pruefe("B2 Obergrenze der Aktualisierung greift", neu_gross[1] == FAKTEN_NEU_MAX, neu_gross) +pruefe("B3 Aktualisierung bleibt unter der Erstpruefung", + faktenspanne(200, neu=True)[1] < faktenspanne(200)[1]) + +print("\nC) Die Auftraege enthalten Platzhalter statt fester Zahlen") +for name, vorlage in ( + ("Erstpruefung adhoc", FACTCHECK_PROMPT_TEMPLATE), + ("Erstpruefung Recherche", RESEARCH_FACTCHECK_PROMPT_TEMPLATE), + ("Aktualisierung adhoc", INCREMENTAL_FACTCHECK_PROMPT_TEMPLATE), + ("Aktualisierung Recherche", INCREMENTAL_RESEARCH_FACTCHECK_PROMPT_TEMPLATE), + ("Triage", TRIAGE_PROMPT_TEMPLATE)): + pruefe(f"C {name} nutzt den Richtwert", + "{fakten_min}" in vorlage and "{fakten_max}" in vorlage) + pruefe(f"C {name} ohne feste Vorgabe", + "5-10" not in vorlage and "3-5 " not in vorlage and "(3-5" not in vorlage) + +pruefe("C Richtwert ist ausdruecklich keine Zielvorgabe", + "Richtwert und keine" in FACTCHECK_PROMPT_TEMPLATE + and "Zielvorgabe" in FACTCHECK_PROMPT_TEMPLATE) +pruefe("C Abweichen nach oben und unten ist erlaubt", + "weniger" in FACTCHECK_PROMPT_TEMPLATE and "mehr" in FACTCHECK_PROMPT_TEMPLATE) + +print("\nD) Der berechnete Wert landet im Auftrag") +letzter = {"prompt": ""} + + +async def fake_call_claude(prompt, tools="WebSearch,WebFetch", model=None, + raw_text=False, timeout=None): + letzter["prompt"] = prompt + return "[]", ClaudeUsage(input_tokens=10, output_tokens=5, cost_usd=0.01) + + +fcmod.call_claude = fake_call_claude +cc.call_claude = fake_call_claude +_ai_backend_var.set("cli") +fc = FactCheckerAgent() + + +def meldungen(n): + return [{"headline": f"Meldung {i}", "source": "tagesschau", + "source_url": f"https://tagesschau.de/{i}", "content_original": "t"} + for i in range(n)] + + +asyncio.run(fc.check("Lage", meldungen(16), "adhoc", "Deutsch")) +u, o = faktenspanne(16) +pruefe("D1 kleine Lage traegt ihren Richtwert", + f"etwa {u} bis {o} Aussagen" in letzter["prompt"], + [z for z in letzter["prompt"].splitlines() if "Richtwert" in z][:1]) + +asyncio.run(fc.check("Lage", meldungen(88), "adhoc", "Deutsch")) +u2, o2 = faktenspanne(88) +pruefe("D2 grosse Lage traegt einen groesseren Richtwert", + f"etwa {u2} bis {o2} Aussagen" in letzter["prompt"] and o2 > o, + [z for z in letzter["prompt"].splitlines() if "Richtwert" in z][:1]) + +asyncio.run(fc.check_incremental( + "Lage", meldungen(30), + [{"claim": "Ein bestehender Punkt", "status": "confirmed", "evidence": "e"}], + "adhoc", "Deutsch")) +u3, o3 = faktenspanne(30, neu=True) +pruefe("D3 Aktualisierung nutzt den engeren Korridor", + f"etwa\n {u3} bis {o3}" in letzter["prompt"] or f"{u3} bis {o3}" in letzter["prompt"], + [z for z in letzter["prompt"].splitlines() if "Richtwert" in z][:1]) + +print("\nErgebnis: " + str(ok) + " bestanden, " + str(fail) + " fehlgeschlagen") +sys.exit(1 if fail else 0)