breed_evaluator: LLM-as-Judge Qualitätsbewertung via Claude Haiku
This commit is contained in:
parent
1c80481f42
commit
d80abf07e5
2 changed files with 152 additions and 0 deletions
142
backend/scraper/breed_evaluator.py
Normal file
142
backend/scraper/breed_evaluator.py
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
"""
|
||||
Qualitätsbewertung der KI-Rassen-Anreicherung via Claude (LLM-as-Judge).
|
||||
|
||||
Bewertet eine Zufallsstichprobe angereicherter Rassen nach:
|
||||
- Vollständigkeit (alle Felder befüllt?)
|
||||
- Korrektheit (plausible Fakten?)
|
||||
- Sprachqualität (natürliches Deutsch?)
|
||||
- Konsistenz (Felder stimmen untereinander überein?)
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import asyncio
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_EVAL_PROMPT = '''\
|
||||
Du bist ein Qualitätsprüfer für Hunderassen-Daten. Bewerte den folgenden \
|
||||
Datensatz für die Rasse "{name}" auf einer Skala von 1-5.
|
||||
|
||||
Datensatz:
|
||||
{data}
|
||||
|
||||
Antworte NUR als JSON:
|
||||
{{
|
||||
"vollstaendigkeit": <1-5>,
|
||||
"korrektheit": <1-5>,
|
||||
"sprachqualitaet": <1-5>,
|
||||
"konsistenz": <1-5>,
|
||||
"gesamt": <1-5>,
|
||||
"hinweis": "Kurze Begründung oder auffällige Mängel (max 1 Satz)"
|
||||
}}
|
||||
|
||||
Bewertungskriterien:
|
||||
- Vollständigkeit: Sind beschreibung, vorkommen_de, groesse, gewicht, \
|
||||
lebensdauer, aktivitaet, erfahrung, kinder_geeignet, wohnung_geeignet, \
|
||||
temperament alle befüllt?
|
||||
- Korrektheit: Stimmen die Angaben mit bekannten Fakten überein?
|
||||
- Sprachqualität: Ist der deutsche Text natürlich, fehlerfrei und informativ?
|
||||
- Konsistenz: Passen die Felder zueinander (z.B. Gewicht zur Größe, \
|
||||
Aktivität zur Erfahrung)?
|
||||
'''
|
||||
|
||||
|
||||
async def evaluate_enrichment(sample_size: int = 20) -> dict:
|
||||
"""
|
||||
Bewertet `sample_size` zufällig gewählte angereicherte Rassen via Claude.
|
||||
|
||||
Returns dict mit aggregierten Scores und Einzelergebnissen.
|
||||
"""
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
from database import db
|
||||
|
||||
ANTHROPIC_KEY = os.getenv("ANTHROPIC_API_KEY", "")
|
||||
if not ANTHROPIC_KEY:
|
||||
raise RuntimeError("ANTHROPIC_API_KEY nicht gesetzt — Evaluierung benötigt Cloud.")
|
||||
|
||||
with db() as conn:
|
||||
rassen = conn.execute(
|
||||
"""SELECT name, beschreibung, vorkommen_de, groesse,
|
||||
gewicht_min_kg, gewicht_max_kg, lebensdauer,
|
||||
aktivitaet, erfahrung, kinder_geeignet,
|
||||
wohnung_geeignet, temperament
|
||||
FROM wiki_rassen
|
||||
WHERE ki_enriched = 1
|
||||
ORDER BY RANDOM()
|
||||
LIMIT ?""",
|
||||
(sample_size,),
|
||||
).fetchall()
|
||||
|
||||
if not rassen:
|
||||
return {"error": "Keine angereicherten Rassen gefunden."}
|
||||
|
||||
import anthropic
|
||||
client = anthropic.Anthropic(api_key=ANTHROPIC_KEY)
|
||||
|
||||
results = []
|
||||
totals = {"vollstaendigkeit": 0, "korrektheit": 0,
|
||||
"sprachqualitaet": 0, "konsistenz": 0, "gesamt": 0}
|
||||
|
||||
for rasse in rassen:
|
||||
name = rasse["name"]
|
||||
data = {
|
||||
"beschreibung": rasse["beschreibung"],
|
||||
"vorkommen_de": rasse["vorkommen_de"],
|
||||
"groesse": rasse["groesse"],
|
||||
"gewicht_min_kg": rasse["gewicht_min_kg"],
|
||||
"gewicht_max_kg": rasse["gewicht_max_kg"],
|
||||
"lebensdauer": rasse["lebensdauer"],
|
||||
"aktivitaet": rasse["aktivitaet"],
|
||||
"erfahrung": rasse["erfahrung"],
|
||||
"kinder_geeignet": rasse["kinder_geeignet"],
|
||||
"wohnung_geeignet": rasse["wohnung_geeignet"],
|
||||
"temperament": rasse["temperament"],
|
||||
}
|
||||
prompt = _EVAL_PROMPT.format(
|
||||
name=name,
|
||||
data=json.dumps(data, ensure_ascii=False, indent=2),
|
||||
)
|
||||
try:
|
||||
def _call():
|
||||
return client.messages.create(
|
||||
model="claude-haiku-4-5-20251001",
|
||||
max_tokens=256,
|
||||
system=[{
|
||||
"type": "text",
|
||||
"text": "Du bist ein präziser Qualitätsprüfer. Antworte ausschließlich als JSON.",
|
||||
"cache_control": {"type": "ephemeral"},
|
||||
}],
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
)
|
||||
loop = asyncio.get_event_loop()
|
||||
resp = await loop.run_in_executor(None, _call)
|
||||
raw = resp.content[0].text.strip()
|
||||
|
||||
# JSON extrahieren
|
||||
import re
|
||||
match = re.search(r"\{[\s\S]+\}", raw)
|
||||
scores = json.loads(match.group(0)) if match else {}
|
||||
|
||||
entry = {"name": name, **scores}
|
||||
results.append(entry)
|
||||
for key in totals:
|
||||
totals[key] += scores.get(key, 0)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Evaluierung fehlgeschlagen für %s: %s", name, e)
|
||||
results.append({"name": name, "error": str(e)})
|
||||
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
count = len([r for r in results if "error" not in r])
|
||||
averages = {k: round(v / count, 2) for k, v in totals.items()} if count else {}
|
||||
|
||||
return {
|
||||
"sample_size": len(rassen),
|
||||
"evaluated": count,
|
||||
"averages": averages,
|
||||
"results": results,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue