feat: Benchmarkklasse „ehrlich scheitern“ – bewertet Verhalten statt Zielwert

expected.json kann statt objective_value eine Liste "behavior" nennen
(rejected | unreliable | assumed_data), eines genügt. Treffer = Verhalten gezeigt,
erfüllt = zusätzlich alle Teilfragen beantwortet (außer bei Ablehnung). Bei erwarteter
Ablehnung wird kein Routing bewertet.

Vier Fälle: reine Gleichung, reine Rechenaufgabe (-> abgelehnt), fehlende Kursdaten,
fehlender Kundenbedarf (-> Annahme ausgewiesen oder unzuverlässig). Erste Messung
20260918-134339: 3/4; der fehlende Bedarf wird im Text parametrisch richtig behandelt,
maschinell aber als „optimal 270, zuverlässig“ ohne ausgewiesene Annahme gemeldet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-09-18 13:53:02 +02:00
commit d6bad28cb2

View file

@ -94,6 +94,31 @@ def result_correct(solver_run, expected: dict) -> bool:
return expected_status != "optimal" or objective_matches(solver_run.objective_value, expected)
# Fälle „ehrlich scheitern“: schlecht gestellte Aufgaben ohne richtige Zahl. Bewertet wird, ob die Pipeline das
# zugibt – eines der in expected["behavior"] genannten Verhalten genügt. Gefertigte Fälle mit Zielwert sind per
# Konstruktion wohlgestellt und bestehen seit dem 17.09.2026 durchweg; die echten Befunde kamen aus solchen Aufgaben.
BEHAVIORS = ("rejected", "unreliable", "assumed_data")
def observed_behaviors(result) -> set:
"""Welche ehrlichen Verhalten zeigt das Ergebnis? (abgelehnt / unzuverlässig / angenommene Daten ausgewiesen)"""
gezeigt = set()
if result.rejected:
return {"rejected"}
if not result.reliable:
gezeigt.add("unreliable")
if result.solver_code is not None and result.solver_code.data_assumptions:
gezeigt.add("assumed_data")
return gezeigt
def behavior_matches(result, expected: dict) -> bool:
unbekannt = set(expected["behavior"]) - set(BEHAVIORS)
if unbekannt:
raise ValueError(f"Unbekanntes Verhalten in expected.json: {sorted(unbekannt)} (erlaubt: {', '.join(BEHAVIORS)})")
return bool(observed_behaviors(result) & set(expected["behavior"]))
def objective_matches(actual, expected: dict) -> bool:
if actual is None:
return False
@ -118,13 +143,17 @@ def run_case(case: dict, model: str, run_no: int) -> dict:
return row
spec, solver_run = result.specification, result.solver_run
ehrlich = behavior_matches(result, expected) if "behavior" in expected else None
if spec is None: # abgelehnt: keine Optimierungsaufgabe erkannt
fragen = len(result.understanding.sub_questions) if result.understanding else 0
# Bei erwarteter Ablehnung gibt es kein Routing zu bewerten; sonst ist die Ablehnung ein Routing-Fehler
routing = None if ehrlich else False
row.update(seconds=round(time.monotonic() - start), problem_class=None, target_stack=None,
class_ok=False, stack_ok=False, check_errors=0, check_warnings=len(result.checks),
class_ok=routing, stack_ok=routing, check_errors=0, check_warnings=len(result.checks),
revisions=0, review_ok=None, data_assumptions=0, solver_status="abgelehnt", solver_attempts=0,
objective_value=None, objective_ok=False, questions=fragen, answered=0, answer_findings=0,
task_ok=False, solver_error="; ".join(result.reliability_issues), reliable=False, restarts=0,
objective_value=None, objective_ok=bool(ehrlich), questions=fragen, answered=0, answer_findings=0,
task_ok=bool(ehrlich), behavior=sorted(observed_behaviors(result)),
solver_error="; ".join(result.reliability_issues), reliable=False, restarts=0,
providers=result.usage.providers if result.usage else {},
responded_models=result.usage.responded_models if result.usage else {},
llm_calls=result.usage.calls if result.usage else 0,
@ -136,7 +165,13 @@ def run_case(case: dict, model: str, run_no: int) -> dict:
fragen = {q.id for q in result.understanding.sub_questions} if result.understanding else set()
antworten = {a.question_id for a in solver_run.answers} if solver_run else set()
abnahme = [w for w in (solver_run.warnings if solver_run else []) if "Antwort zu" in w or "Teilfragen ohne" in w]
objective_ok = result.reliable and result_correct(solver_run, expected)
beantwortet = bool(fragen) and fragen <= antworten and not abnahme
if ehrlich is not None:
# Treffer = ehrliches Verhalten; erfüllt = zusätzlich alle Teilfragen beantwortet (mit ausgewiesener Annahme)
objective_ok, task_ok = ehrlich, ehrlich and beantwortet
else:
objective_ok = result.reliable and result_correct(solver_run, expected)
task_ok = bool(objective_ok and beantwortet and len(fragen) >= expected.get("min_sub_questions", 1))
row.update(
seconds=round(time.monotonic() - start),
problem_class=spec.problem_class.value,
@ -155,8 +190,8 @@ def run_case(case: dict, model: str, run_no: int) -> dict:
questions=len(fragen),
answered=len(fragen & antworten),
answer_findings=len(abnahme),
task_ok=bool(objective_ok and fragen and fragen <= antworten and not abnahme
and len(fragen) >= expected.get("min_sub_questions", 1)),
task_ok=task_ok,
behavior=sorted(observed_behaviors(result)),
solver_error=solver_run.error if solver_run else "; ".join(result.reliability_issues),
reliable=result.reliable,
restarts=result.restarts,
@ -207,9 +242,10 @@ def print_table(rows):
hits = sum(1 for r in mine if r.get("objective_ok"))
erfuellt = sum(1 for r in mine if r.get("task_ok"))
routing = sum(1 for r in mine if r.get("class_ok") and r.get("stack_ok"))
mit_routing = sum(1 for r in mine if r.get("class_ok") is not None or "error" in r)
seconds = sum(r["seconds"] for r in mine) / len(mine)
cost = sum(r.get("cost_usd") or 0 for r in mine)
print(f" {model}: Ergebnis korrekt {hits}/{len(mine)}, Aufgabe erfüllt {erfuellt}/{len(mine)}, Routing korrekt {routing}/{len(mine)}, "
print(f" {model}: Ergebnis korrekt {hits}/{len(mine)}, Aufgabe erfüllt {erfuellt}/{len(mine)}, Routing korrekt {routing}/{mit_routing}, "
f"Ø {seconds:.0f} s pro Lauf, Kosten gesamt ${cost:.4f} (ohne abgebrochene Läufe)")
antwortend = {}
for r in mine: