fix: abgeschnittene Antworten meiden den Anbieter; LLM-Ausfall macht unzuverlässig statt abzustürzen

Grundlinie 20260918-191400: 9 von 22 Läufen hatten Aufrufe, die an LLM_MAX_TOKENS abgeschnitten
wurden (Venice 4/28, Makora 2/43, übrige 0/54); hard_portfolio_kardinalitaet brach nach vier
Abschneidungen ab, Kosten gingen verloren.

- Abschneiden: sofort neu versuchen, Anbieter per provider.ignore meiden (_avoid_provider,
  gemeinsam mit „zu langsam“)
- nach allen Wiederholungen LLMUnavailable (Stufe + Ursache); fatale Fehler bleiben unverpackt
- Solver-Stufe behält Modell und Prüfbericht (Solver-Fehler), Gegenprobe behält erstes Ergebnis,
  davor Ergebnis ohne Spezifikation mit Kosten („LLM-Fehler“, Report „Lauf abgebrochen“)
- Benchmark: Status „LLM-Fehler“ statt ABBRUCH, zählt nicht als ehrliches Scheitern
- TextToModel.py: Shebang

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-09-18 19:24:23 +02:00
commit 6b2b2c6466

View file

@ -55,7 +55,10 @@ class FakeSession:
warteschlange = self.queues[response_model]
if response_model is TaskUnderstanding and not warteschlange:
return EINFACHES_VERSTAENDNIS # Standard, solange ein Test nichts anderes vorgibt
return warteschlange.pop(0)
antwort = warteschlange.pop(0)
if isinstance(antwort, Exception): # gescheiterter Aufruf, z. B. LLMUnavailable
raise antwort
return antwort
HALLUCINATED = make_spec(executive_summary="Produktionsplanung mit Drehen und Fräsen") # Fremdproblem
@ -853,3 +856,50 @@ def test_second_repair_knows_the_first_error():
reparaturen = [inhalt for stufe, inhalt in session.contents if stufe == "Solver-Reparatur"]
assert "FRÜHERE FEHLVERSUCHE" not in reparaturen[0] # beim ersten gibt es noch keinen Verlauf
assert "Fehler B: Typ" in reparaturen[1] and "Versuch 1: " in reparaturen[1] and "Fehler A: Index" in reparaturen[1]
# --- LLM nach allen Wiederholungen gescheitert: unzuverlässiges Ergebnis statt Absturz ---
def _llm_weg(step):
from core.llm import LLMUnavailable
return LLMUnavailable(step, RuntimeError("The output is incomplete due to a max_tokens length limit."))
def test_llm_failure_in_solver_stage_keeps_the_model():
# beobachtet: hard_portfolio_kardinalitaet, viermal abgeschnitten ⇒ Absturz nach 1162 s, Kosten verloren
session = FakeSession([GOOD], [OK], [_llm_weg("Solver-Code")])
result = run_pipeline("Aufgabe", session=session, max_revisions=0, max_restarts=0)
assert not result.reliable and result.specification is not None and result.review is not None
assert result.solver_run.status == SolverStatus.ERROR and result.solver_code is None
assert result.reliability_issues[0].startswith("Solver-Fehler: LLM-Aufruf 'Solver-Code'")
assert result.usage is not None
def test_llm_failure_before_the_model_is_a_paid_non_result(tmp_path):
from benchmark.benchmark import observed_behaviors
from reporting.report import write_report
session = FakeSession([_llm_weg("Extraktion")], [])
result = run_pipeline("Aufgabe", session=session)
assert not result.reliable and not result.rejected and result.specification is None
assert result.reliability_issues[0].startswith("LLM-Fehler: ") and result.usage.calls == 2
assert observed_behaviors(result) == set() # ein Absturz ist kein ehrliches Scheitern
write_report(result, str(tmp_path / "r.md"))
text = (tmp_path / "r.md").read_text(encoding="utf-8")
assert "Lauf abgebrochen" in text and "abgelehnt" not in text
def test_benchmark_counts_an_llm_failure_as_failure_with_costs(monkeypatch):
from benchmark import benchmark
session = FakeSession([_llm_weg("Extraktion")], [])
monkeypatch.setattr(benchmark, "run_pipeline", lambda *a, **k: run_pipeline("Aufgabe", session=session))
zeile = benchmark.run_case({"name": "t", "task": "Aufgabe", "expected": {"objective_value": 9, "tolerance": 1e-6}},
"fake", 1)
assert zeile["solver_status"] == "LLM-Fehler" and zeile["llm_calls"] == 2 and "error" not in zeile
assert zeile["objective_ok"] is False and zeile["task_ok"] is False
def test_llm_failure_in_the_infeasible_recheck_keeps_the_first_result():
session = FakeSession(specs=[GOOD, _llm_weg("Extraktion")], reviews=[OK], codes=[SolverCode(code=INFEASIBLE_CODE)])
result = run_pipeline("Aufgabe", session=session, max_revisions=0, max_restarts=0)
assert result.solver_run.status == SolverStatus.INFEASIBLE and not result.infeasible_confirmed
assert not result.reliable and "Gegenprobe scheiterte am LLM" in result.reliability_issues[-1]