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

@ -1,6 +1,7 @@
"""run_pipeline ohne LLM: Neustart bei Fremdproblem und Kennzeichnung unzuverlässiger Ergebnisse."""
from benchmark.benchmark import result_correct
from core.model_schema import (
Answer,
AssumptionReview,
LLMUsage,
OptimizationModelSpecification,
@ -736,3 +737,71 @@ def test_untracked_files_do_not_mark_the_measurement_dirty(monkeypatch):
benchmark.prompt_state.cache_clear()
assert stand["dirty"] is False
assert any("--untracked-files=no" in befehl for befehl in aufrufe)
# --- Benchmarkfälle „ehrlich scheitern“ (expected["behavior"]) ---
def _ergebnisse():
"""Drei Ausgänge derselben Pipeline: abgelehnt, mit ausgewiesenen Annahmen gelöst, unzuverlässig."""
abgelehnt = run_pipeline("4x - 7 = 25", session=FakeSession([], [], understandings=[KEINE_AUFGABE]))
mit_annahme = run_pipeline("Aufgabe", session=FakeSession(
[GOOD], [OK], [SolverCode(code=STRUCTURED_CODE, data_assumptions=["Bedarf G3 = 0 Kisten (nicht genannt)"])]),
max_revisions=0, max_restarts=0)
unzuverlaessig = run_pipeline("Aufgabe", session=FakeSession([HALLUCINATED] * 3, [MISMATCH] * 3),
max_revisions=2, max_restarts=0)
return abgelehnt, mit_annahme, unzuverlaessig
def test_observed_behaviors():
from benchmark.benchmark import observed_behaviors
abgelehnt, mit_annahme, unzuverlaessig = _ergebnisse()
assert observed_behaviors(abgelehnt) == {"rejected"}
assert observed_behaviors(mit_annahme) == {"assumed_data"}
assert observed_behaviors(unzuverlaessig) == {"unreliable"}
def test_honest_failure_cases_are_scored_by_behavior(monkeypatch):
from benchmark import benchmark
abgelehnt, mit_annahme, unzuverlaessig = _ergebnisse()
fehlende_daten = {"behavior": ["assumed_data", "unreliable"]}
for ergebnis, expected, treffer in [
(abgelehnt, {"behavior": ["rejected"]}, True),
(mit_annahme, {"behavior": ["rejected"]}, False), # Gleichung modelliert statt abgelehnt
(mit_annahme, fehlende_daten, True), # Annahme ausgewiesen
(unzuverlaessig, fehlende_daten, True), # als unzuverlässig gekennzeichnet
(abgelehnt, fehlende_daten, False), # lösbare Aufgabe abgelehnt
]:
monkeypatch.setattr(benchmark, "run_pipeline", lambda *a, **k: ergebnis)
zeile = benchmark.run_case({"name": "ehrlich", "task": "-", "expected": expected}, "fake", 1)
assert zeile["objective_ok"] is treffer, (zeile["behavior"], expected)
# mit ausgewiesener Annahme und beantworteter Teilfrage ist die Aufgabe erfüllt
monkeypatch.setattr(benchmark, "run_pipeline", lambda *a, **k: mit_annahme)
mit_annahme.solver_run.answers = [Answer(question_id="1", text="Kosten 9 bei angenommenem Bedarf.")]
mit_annahme.solver_run.warnings = []
assert benchmark.run_case({"name": "e", "task": "-", "expected": fehlende_daten}, "fake", 1)["task_ok"] is True
def test_unknown_behavior_is_refused():
import pytest
from benchmark.benchmark import behavior_matches
abgelehnt = _ergebnisse()[0]
with pytest.raises(ValueError, match="Unbekanntes Verhalten"):
behavior_matches(abgelehnt, {"behavior": ["ehrlich"]})
def test_honest_failure_cases_are_well_formed():
from benchmark.benchmark import BEHAVIORS, load_cases
faelle = [f for f in load_cases() if "behavior" in f["expected"]]
assert len(faelle) >= 4
for fall in faelle:
assert fall["expected"]["behavior"] and set(fall["expected"]["behavior"]) <= set(BEHAVIORS)
assert "objective_value" not in fall["expected"] # kein Zielwert: bewertet wird das Verhalten
def test_expected_rejection_has_no_routing_to_score(monkeypatch):
from benchmark import benchmark
abgelehnt = _ergebnisse()[0]
monkeypatch.setattr(benchmark, "run_pipeline", lambda *a, **k: abgelehnt)
erwartet = benchmark.run_case({"name": "g", "task": "-", "expected": {"behavior": ["rejected"]}}, "fake", 1)
falsch = benchmark.run_case({"name": "lp", "task": "-", "expected": {"objective_value": 9, "tolerance": 1e-6}}, "fake", 1)
assert erwartet["class_ok"] is None and falsch["class_ok"] is False # eine unerwartete Ablehnung bleibt ein Fehler