feat: Erfundene Einheiten in Antworten prüfen; data_assumptions-Kanal klargestellt
check_answer_units meldet Einheiten hinter Zahlen, die weder Aufgabentext noch Datenprofil hergeben (nach Größenart, GE/ME neutral bei Kosten-/Mengenwörtern); Befund löst Solver-Reparatur aus, bleibt er, Warnung. Reparaturmeldung und Solver-Prompt nennen data_assumptions jetzt als Antwortfeld, nicht report()-Argument. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
7aac3ff3ec
commit
c68a293755
2 changed files with 107 additions and 5 deletions
|
|
@ -11,7 +11,7 @@ Regeln für das Modell:
|
|||
- Implementiere Variablen, Zielfunktion und Restriktionen exakt wie in der Spezifikation. Lasse keine Restriktion weg, lockere keine und füge keine hinzu.
|
||||
- Ist das Modell unlösbar oder unbeschränkt, melde genau das (Status infeasible bzw. unbounded). Verändere das Modell nicht, um eine Lösung zu erzwingen.
|
||||
- Daten: Verwende die Werte aus extracted_values bzw. dem Aufgabentext. Fehlen Werte, wähle kleine, plausible Beispieldaten und führe JEDEN angenommenen Wert in data_assumptions auf (Symbol, Wert, Begründung). Stehen alle Werte im Text, bleibt data_assumptions leer.
|
||||
- Erzeugst du Eingabedaten selbst – Zufallszahlen, ein synthetischer Markt, ein verkleinertes Universum, weil die echten Daten nicht erreichbar sind –, gehört das IMMER nach data_assumptions, nicht nur in eine Tabelle oder einen Antwortsatz. Sonst ist dem Ergebnis maschinell nicht anzusehen, dass es nicht auf den Daten der Aufgabe beruht. [→ check_generated_data]
|
||||
- Erzeugst du Eingabedaten selbst – Zufallszahlen, ein synthetischer Markt, ein verkleinertes Universum, weil die echten Daten nicht erreichbar sind –, gehört das IMMER nach data_assumptions (Feld deiner Antwort neben code, kein Argument von report()), nicht nur in eine Tabelle oder einen Antwortsatz. Sonst ist dem Ergebnis maschinell nicht anzusehen, dass es nicht auf den Daten der Aufgabe beruht. [→ check_generated_data]
|
||||
- Die offenen Annahmen (open_assumptions) der Spezifikation gelten und sind umzusetzen.
|
||||
|
||||
Erlaubte Imports (andere sind verboten und führen zum Abbruch): json, math, itertools, functools, collections, numpy, scipy, highspy, ortools, cvxpy, pyscipopt, cyipopt, networkx, pandas.
|
||||
|
|
@ -85,6 +85,7 @@ Ergebnisausgabe:
|
|||
- Auch bei Status infeasible oder unbounded wird JEDE Teilfrage beantwortet – dann lautet die Antwort, dass und warum die Aufgabe keine zulässige Lösung hat (welche Bedingungen sich widersprechen). Eine unbeantwortete Frage ist schlechter als die Antwort „unlösbar“.
|
||||
- Teilfragen ohne Rechnung (formulieren, zeichnen, erklaeren) werden ebenfalls beantwortet – dort beschreibt der Text das aufgestellte Modell bzw. die Struktur der Zeichnung in Worten.
|
||||
- Verwende NUR Zahlen, die dieses Programm berechnet hat oder die im Aufgabentext stehen. [→ check_answers]
|
||||
- Ebenso bei Einheiten: nur solche, die im Aufgabentext oder in beigelegten Daten stehen. Nennt der Text keine, steht die Zahl ohne Einheit („Der kleinste Zielwert ist 459.“); GE bzw. ME nur, wenn der Text von Kosten, Preisen bzw. Mengen spricht. [→ check_answer_units]
|
||||
- WICHTIG, häufigster Fehler: Sobald eine Antwort einen Vergleichs- oder Zwischenwert nennt, muss dieser Wert auch in einer Ergebnistabelle stehen. Beobachtet: „der beste Weg kostet 29, die Alternative 30“ – die 30 stand nirgends, der Leser konnte sie nicht prüfen. Richtig ist eine zusätzliche Tabelle („Alle Wege“, „Vergleich der Varianten“, „Kostenbestandteile“) mit genau diesen Zahlen. Ebenso für Summen wie „insgesamt 2,32 % Gebühren“: entweder eine Summenzeile in der Tabelle oder die Zahl weglassen.
|
||||
- Fehlt eine Teilfrage, ist die Aufgabe nicht erfüllt – auch wenn der Zielwert stimmt. [→ check_answers]
|
||||
- scenarios: NUR wenn die Aufgabe mehrere Konfigurationen vergleicht (z. B. drei Gewichtungen), sonst leer. Ein Szenario ist eine DATENVARIANTE desselben Modells, kein zweites Modell:
|
||||
|
|
|
|||
|
|
@ -743,7 +743,8 @@ def check_generated_data(code: str, solver_code, spec=None) -> list:
|
|||
and all(numbers_in(p.extracted_values or "") for p in spec.parameters)):
|
||||
return []
|
||||
return ["Das Programm erzeugt Zahlen mit einem Zufallsgenerator, weist aber keine angenommenen "
|
||||
"Daten aus. Selbst erzeugte Eingabedaten gehören in data_assumptions – sonst ist dem "
|
||||
"Daten aus. Selbst erzeugte Eingabedaten gehören in data_assumptions – das Feld der Antwort "
|
||||
"neben code, kein Argument von report() –, sonst ist dem "
|
||||
"Ergebnis nicht anzusehen, dass es nicht auf den Daten der Aufgabe beruht."]
|
||||
|
||||
|
||||
|
|
@ -802,6 +803,57 @@ def check_answers(run, understanding, task: str = "", data_summary: str = "") ->
|
|||
return befunde
|
||||
|
||||
|
||||
# Einheiten nach Größenart: gedeckt ist eine Einheit, wenn der Text irgendeine derselben Art nennt
|
||||
# ("Tausend Euro" deckt "€", "Stunden" deckt "min"). Prozent fehlt bewusst – es ist keine Einheit, die
|
||||
# man erfinden kann, sondern eine Schreibweise für Anteile.
|
||||
UNIT_FAMILIES = {
|
||||
"Geld": ("€", "EUR", "Euro", "$", "USD", "Dollar", "CHF", "Cent", "ct", "GE", "Geldeinheit", "Geldeinheiten"),
|
||||
"Menge": ("Stück", "Stk", "ME", "Mengeneinheit", "Mengeneinheiten"),
|
||||
"Zeit": ("h", "Std", "Stunde", "Stunden", "min", "Minute", "Minuten", "s", "Sekunden", "Tag", "Tage", "Tagen",
|
||||
"Woche", "Wochen", "Monat", "Monate", "Monaten", "Jahr", "Jahre", "Jahren"),
|
||||
"Masse": ("g", "kg", "t", "Tonne", "Tonnen"),
|
||||
"Länge": ("mm", "cm", "m", "km", "Meter", "Kilometer"),
|
||||
"Volumen": ("l", "Liter", "hl", "m³"),
|
||||
}
|
||||
_UNIT_OF = {einheit: art for art, einheiten in UNIT_FAMILIES.items() for einheit in einheiten}
|
||||
_UNIT_ALT = "|".join(sorted(map(re.escape, _UNIT_OF), key=len, reverse=True))
|
||||
# Kurze Einheiten (h, s, t, kg) decken nur mit Zahl davor oder in Klammern – sonst deckt „s.t." oder x_t alles
|
||||
_UNIT_COVER = re.compile(r"(?<![\wäöüß])([(\[]\s*|in\s+)?(" + _UNIT_ALT + r")(?![\wäöüß])")
|
||||
_UNIT_AFTER_NUMBER = re.compile(r"\d(?:[\d.,]*\d)?\s*(?:(?:Tsd|Mio|Mrd)\.?\s*)?(" + _UNIT_ALT + r")(?![\wäöüß])")
|
||||
# Neutrale Platzhalter sind keine Erfindung, wenn der Text die Größe benennt, nur nicht ihre Einheit
|
||||
_NEUTRAL_GELD = re.compile(r"kost|preis|gewinn|erlös|erloes|umsatz|budget|kapital|deckungsbeitrag|ausgabe|einnahme", re.I)
|
||||
_NEUTRAL_MENGE = re.compile(r"menge|stück|stueck|produz|liefer|bedarf|nachfrage", re.I)
|
||||
_NEUTRAL_UNITS = {"GE": _NEUTRAL_GELD, "Geldeinheit": _NEUTRAL_GELD, "Geldeinheiten": _NEUTRAL_GELD,
|
||||
"ME": _NEUTRAL_MENGE, "Mengeneinheit": _NEUTRAL_MENGE, "Mengeneinheiten": _NEUTRAL_MENGE}
|
||||
|
||||
|
||||
def check_answer_units(run, task: str, data_summary: str = "") -> list:
|
||||
"""Nennt eine Antwort eine Einheit, die weder Aufgabentext noch beigelegte Daten hergeben?
|
||||
|
||||
Beobachtet an `tasks/nur_formeln.md` (reines ILP ohne jede Einheit): „459 Geldeinheiten (GE)",
|
||||
obwohl die eigene Annahme die Zahlen dimensionslos nannte; in der Urfassung der Ersatzplanung
|
||||
(„Kosten" ohne Währung) dreimal „29 €". Die Regel im Erklär-Block allein wirkte nicht.
|
||||
Belegt ist nur, was im Text steht – `ParameterDefinition.unit` taugt nicht, dort erfand dasselbe
|
||||
LLM „Stück" und „Mengeneinheit". An 146 gespeicherten Läufen: 4 Treffer, alle echt, kein Fehlalarm.
|
||||
Der Größenfaktor („Tausend Euro" gegen „€") wird bewusst nicht geprüft.
|
||||
"""
|
||||
gedeckt = {_UNIT_OF[m.group(1)] for m in _UNIT_AFTER_NUMBER.finditer(task)}
|
||||
gedeckt |= {_UNIT_OF[m.group(2)] for m in _UNIT_COVER.finditer(task)
|
||||
if len(m.group(2)) > 2 or not m.group(2).isalpha() or m.group(1)}
|
||||
# Datenprofil: Spaltennamen wie emission_kg tragen die Einheit ohne Zahl davor
|
||||
gedeckt |= {_UNIT_OF[m.group(2)] for m in _UNIT_COVER.finditer(data_summary.replace("_", " "))}
|
||||
befunde = []
|
||||
for antwort in run.answers:
|
||||
erfunden = sorted({m.group(1) for m in _UNIT_AFTER_NUMBER.finditer(antwort.text or "")
|
||||
if _UNIT_OF[m.group(1)] not in gedeckt
|
||||
and not (m.group(1) in _NEUTRAL_UNITS and _NEUTRAL_UNITS[m.group(1)].search(task))})
|
||||
if erfunden:
|
||||
befunde.append(f"Antwort zu '{antwort.question_id}' nennt die Einheit {', '.join(erfunden)}, die weder im "
|
||||
f"Aufgabentext noch in beigelegten Daten steht. Nennt der Text keine Einheit, steht die "
|
||||
f"Zahl ohne Einheit – erfinde keine.")
|
||||
return befunde
|
||||
|
||||
|
||||
def check_graph(run, sense: str) -> list:
|
||||
"""Graph und kürzesten Weg deterministisch nachrechnen (ohne LLM).
|
||||
|
||||
|
|
@ -877,17 +929,24 @@ def solve(session: LLMSession, task: str, spec: OptimizationModelSpecification,
|
|||
solver_code.code = clean_generated_code(solver_code.code)
|
||||
run = _check_and_run(solver_code.code, timeout, spec, understanding, task, data_files, data_prompt)
|
||||
run.undeclared_data = check_generated_data(solver_code.code, solver_code, spec)
|
||||
einheiten = check_answer_units(run, task, data_prompt)
|
||||
attempt = 1
|
||||
while (run.status == SolverStatus.ERROR or run.feasibility or run.undeclared_data) and attempt <= retries:
|
||||
while (run.status == SolverStatus.ERROR or run.feasibility or run.undeclared_data or einheiten) \
|
||||
and attempt <= retries:
|
||||
fehler = run.error or (("Die Lösung hält das Modell nicht ein:\n- " + "\n- ".join(run.feasibility))
|
||||
if run.feasibility else
|
||||
("Eingabedaten nicht ausgewiesen:\n- " + "\n- ".join(run.undeclared_data)))
|
||||
("Eingabedaten nicht ausgewiesen:\n- " + "\n- ".join(run.undeclared_data))
|
||||
if run.undeclared_data else
|
||||
("Einheit in der Antwort erfunden:\n- " + "\n- ".join(einheiten)))
|
||||
log(f"Solver-Code fehlgeschlagen – Reparaturversuch {attempt}/{retries}: {fehler[:200]}")
|
||||
solver_code = generate_solver_code(session, task, spec, previous=solver_code, previous_error=fehler,
|
||||
understanding=understanding, data_prompt=data_prompt)
|
||||
solver_code.code = clean_generated_code(solver_code.code)
|
||||
run = _check_and_run(solver_code.code, timeout, spec, understanding, task, data_files, data_prompt)
|
||||
run.undeclared_data = check_generated_data(solver_code.code, solver_code, spec)
|
||||
einheiten = check_answer_units(run, task, data_prompt)
|
||||
attempt += 1
|
||||
# Bleibt die erfundene Einheit, ist das Ergebnis trotzdem richtig – Warnung, nicht unzuverlässig
|
||||
run.warnings += einheiten
|
||||
run.attempts = attempt
|
||||
return solver_code, run
|
||||
|
|
|
|||
Loading…
Reference in a new issue