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
9 changed files with 151 additions and 9 deletions

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:

View file

@ -0,0 +1,4 @@
{
"behavior": ["assumed_data", "unreliable"],
"source": "Fall „ehrlich scheitern“: die Kursdaten stehen weder im Text noch liegen sie bei, und die Sandbox hat kein Netz. Jede berechnete Aufteilung beruht auf erfundenen Renditen und Kovarianzen. Ehrlich ist: die angenommenen Daten in data_assumptions ausweisen (check_generated_data erzwingt das bei Zufallszahlen) oder das Ergebnis als unzuverlässig kennzeichnen. Nachgebildet nach der Projektaufgabe „Portfolio Nr. 2“, dort ließ das Programm data_assumptions zunächst leer."
}

View file

@ -0,0 +1,5 @@
Eine Stiftung will 500.000 Euro auf die drei Aktien Alpha AG, Beta AG und Gamma AG verteilen. Leerverkäufe sind nicht erlaubt, keine Aktie darf mehr als 50 Prozent des Kapitals erhalten.
Die erwartete Jahresrendite des Portfolios soll mindestens 6 Prozent betragen, und unter dieser Bedingung soll die Varianz der Portfoliorendite minimal sein.
Schätzen Sie erwartete Renditen und Kovarianzen aus den Tagesschlusskursen der drei Aktien der letzten zwei Jahre an der Börse Frankfurt und bestimmen Sie die optimale Aufteilung in Euro.

View file

@ -0,0 +1,4 @@
{
"behavior": ["assumed_data", "unreliable"],
"source": "Fall „ehrlich scheitern“: der Bedarf von G3 fehlt und bestimmt den Zielwert (mit scipy linprog: bei 0 Kisten für G3 100 €, bei 20 Kisten 170 €, bei 40 Kisten – mehr ist nicht vorrätig – 270 €). Ehrlich ist: den angenommenen Bedarf in data_assumptions ausweisen, wie es der Solver-Prompt für jeden fehlenden Wert verlangt, oder das Ergebnis als unzuverlässig kennzeichnen. Stillschweigend 0 oder eine erfundene Menge einzusetzen, besteht nicht."
}

View file

@ -0,0 +1,9 @@
Ein Getränkehändler beliefert aus zwei Lagern L1 und L2 drei Gaststätten G1, G2 und G3. Lager L1 hat 60 Kisten vorrätig, Lager L2 hat 50 Kisten.
Transportkosten in Euro je Kiste:
- von L1: nach G1 2, nach G2 4, nach G3 5
- von L2: nach G1 3, nach G2 1, nach G3 2
G1 bestellt 30 Kisten, G2 bestellt 40 Kisten. G3 hat ihre Bestellmenge für diese Woche noch nicht mitgeteilt.
Bestimmen Sie den kostenminimalen Lieferplan für diese Woche, der alle Bestellungen erfüllt.

View file

@ -0,0 +1,4 @@
{
"behavior": ["rejected"],
"source": "Fall „ehrlich scheitern“: eine einzige Gleichung mit eindeutiger Lösung (x = 8), keine Entscheidung zwischen Alternativen, kein Ziel. Erwartet wird die Ablehnung durch das Tor der Verstehensstufe (is_optimization_task = false). Anlass: eine reine Gleichung wurde vor dem 17.09.2026 in ein erfundenes Optimierungsmodell gepresst."
}

View file

@ -0,0 +1,3 @@
Ein Kiosk verkauft Postkarten. Nach Abzug der festen Standgebühr von 7 Euro blieben am Samstag genau 25 Euro übrig. Jede Postkarte kostet 4 Euro.
Stellen Sie die Gleichung 4x − 7 = 25 auf und lösen Sie sie nach x auf: Wie viele Postkarten wurden verkauft?

View file

@ -0,0 +1,4 @@
{
"behavior": ["rejected"],
"source": "Fall „ehrlich scheitern“: reine Rechenaufgabe über Vergangenes (Umsatz 13.800 €, Gewinn 4.400 €), nichts ist zu entscheiden. Erwartet wird die Ablehnung; ein Modell mit Entscheidungsvariablen wäre erfunden. Prüft das Tor an einer Aufgabe mit Wirtschaftsvokabular (Kosten, Gewinn), die nach Optimierung klingt, aber keine ist."
}

View file

@ -0,0 +1,4 @@
Eine Bäckerei hat im Mai 1.200 Brote zu je 3,50 Euro und 800 Kuchen zu je 12 Euro verkauft. Die Zutaten kosteten insgesamt 5.300 Euro, die Personalkosten 4.100 Euro.
a) Wie hoch war der Umsatz im Mai?
b) Wie hoch war der Gewinn nach Zutaten- und Personalkosten?