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:
@@ -25,6 +25,34 @@ STATUS_PRIORITY = {
|
||||
"developing": 1,
|
||||
}
|
||||
|
||||
# Schutz gegen Fehlurteile beim Zusammenfassen von Duplikaten. Das Clustering
|
||||
# entscheidet ein schnelles Modell, das gelegentlich inhaltlich verschiedene
|
||||
# Fakten in eine Gruppe wirft, etwa wenn viele Behauptungen gleich beginnen.
|
||||
# Zwei Sicherungen greifen. Eine Gruppe, die einen zu grossen Anteil aller
|
||||
# Fakten umfasst, wird komplett verworfen. Und jeder einzelne Loeschkandidat
|
||||
# muss dem behaltenen Fakt auch rechnerisch aehnlich genug sein.
|
||||
DEDUP_MAX_CLUSTER_ANTEIL = float(os.environ.get("QC_DEDUP_MAX_CLUSTER_ANTEIL", "0.4"))
|
||||
DEDUP_MIN_AEHNLICHKEIT = float(os.environ.get("QC_DEDUP_MIN_AEHNLICHKEIT", "0.55"))
|
||||
|
||||
|
||||
def _aehnlichkeit(claim_a: str, claim_b: str) -> float:
|
||||
"""Rechnerische Aehnlichkeit zweier Behauptungen zwischen 0 und 1.
|
||||
|
||||
Gleiche Gewichtung wie im Vorfilter, damit beide Stufen dasselbe Mass
|
||||
verwenden.
|
||||
"""
|
||||
from agents.factchecker import normalize_claim, _keyword_set
|
||||
|
||||
norm_a = normalize_claim(claim_a or "")
|
||||
norm_b = normalize_claim(claim_b or "")
|
||||
if not norm_a or not norm_b:
|
||||
return 0.0
|
||||
kw_a = _keyword_set(claim_a or "")
|
||||
kw_b = _keyword_set(claim_b or "")
|
||||
kw_union = kw_a | kw_b
|
||||
jaccard = len(kw_a & kw_b) / len(kw_union) if kw_union else 0.0
|
||||
return 0.7 * SequenceMatcher(None, norm_a, norm_b).ratio() + 0.3 * jaccard
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. Faktencheck-Duplikate
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -177,11 +205,23 @@ async def check_fact_duplicates(db, incident_id: int, incident_title: str) -> in
|
||||
facts_by_id = {f["id"]: f for f in all_facts}
|
||||
ids_to_delete = set()
|
||||
|
||||
max_cluster = max(2, int(len(all_facts) * DEDUP_MAX_CLUSTER_ANTEIL))
|
||||
|
||||
for cluster_ids in all_clusters:
|
||||
valid_ids = [cid for cid in cluster_ids if cid in facts_by_id]
|
||||
if len(valid_ids) <= 1:
|
||||
continue
|
||||
|
||||
# Sicherung 1: Eine Gruppe, die einen zu grossen Teil aller Fakten
|
||||
# umfasst, ist fast immer ein Fehlurteil des Clusterings.
|
||||
if len(valid_ids) > max_cluster:
|
||||
logger.warning(
|
||||
"QC Duplikat: Gruppe mit %d von %d Fakten verworfen, das ist "
|
||||
"unplausibel (Grenze %d)",
|
||||
len(valid_ids), len(all_facts), max_cluster,
|
||||
)
|
||||
continue
|
||||
|
||||
cluster_facts = [facts_by_id[cid] for cid in valid_ids]
|
||||
best = max(cluster_facts, key=lambda f: (
|
||||
STATUS_PRIORITY.get(f["status"], 0),
|
||||
@@ -190,12 +230,23 @@ async def check_fact_duplicates(db, incident_id: int, incident_title: str) -> in
|
||||
))
|
||||
|
||||
for fact in cluster_facts:
|
||||
if fact["id"] != best["id"]:
|
||||
ids_to_delete.add(fact["id"])
|
||||
logger.info(
|
||||
"QC Duplikat: ID %d entfernt, behalte ID %d ('%s')",
|
||||
fact["id"], best["id"], best["claim"][:60],
|
||||
if fact["id"] == best["id"]:
|
||||
continue
|
||||
# Sicherung 2: Nur loeschen, wenn die beiden Behauptungen auch
|
||||
# rechnerisch nah beieinander liegen.
|
||||
naehe = _aehnlichkeit(fact["claim"], best["claim"])
|
||||
if naehe < DEDUP_MIN_AEHNLICHKEIT:
|
||||
logger.warning(
|
||||
"QC Duplikat: ID %d behalten, Aehnlichkeit zu ID %d nur %.2f "
|
||||
"('%s')",
|
||||
fact["id"], best["id"], naehe, fact["claim"][:60],
|
||||
)
|
||||
continue
|
||||
ids_to_delete.add(fact["id"])
|
||||
logger.info(
|
||||
"QC Duplikat: ID %d entfernt (Aehnlichkeit %.2f), behalte ID %d ('%s')",
|
||||
fact["id"], naehe, best["id"], best["claim"][:60],
|
||||
)
|
||||
|
||||
if ids_to_delete:
|
||||
placeholders = ",".join("?" * len(ids_to_delete))
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren