Übersetzt natürlichsprachige Optimierungsaufgaben (Deutsch) per LLM in eine geprüfte Modellspezifikation, erzeugt Solver-Code und führt ihn in einer bubblewrap-Sandbox aus. Deterministische Prüfungen ohne LLM, Report als Markdown/HTML, Benchmark mit 19 verifizierten Fällen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
15 KiB
Math Model Generator & Solver
Übersetzt eine natürlichsprachige Optimierungsaufgabe (Deutsch) mit einem LLM in ein formales mathematisches Modell, prüft es, korrigiert Fehler und löst es tatsächlich mit einem passenden Python-Solver. Ergebnis ist ein nachvollziehbarer Report mit Formeln, Prüfbericht, Lösung und dem erzeugten Python-Code.
Aufgabentext (+ optional beigelegte Daten: --daten kurse.csv)
│ ohne LLM: Profil je Datei – Zeilen, Spalten, Spannweiten, Lücken
▼
1. Verstehen ───────── LLM: Teilfragen a)/b)/…, alle Zahlen des Textes mit Fundstelle,
│ übergangene Zahlen mit Grund; ist es überhaupt eine Aufgabe?
│ ohne LLM geprüft: keine Zahl, keine Teilfrage übersehen
▼
2. Extraktion ──────── LLM: Problemklasse, Solver-Wahl, Indizes, Parameter,
│ Variablen, Zielfunktion, Restriktionen (LaTeX), Annahmen
▼
3. Konsistenz-Checks ─ ohne LLM: Solver passt zur Klasse? Einheiten? undeklarierte
│ Symbole? kamen die erfassten Zahlen im Modell an?
▼
4. LLM-Prüfung ─────── Zielfunktion = gefragte Größe? Annahmen textkonform?
│ ▲
▼ │ Korrekturschleife (max. 2 Runden, beste Runde gewinnt)
5. Korrektur ┘
│
▼
6. Solver ──────────── LLM erzeugt Python-Code, Ausführung in der Sandbox, Reparatur
│ bei Fehlern; Szenarien = Datenvarianten, ohne LLM nachgerechnet
▼
7. Abnahme ─────────── ohne LLM: jede Teilfrage beantwortet? jede Zahl der Antwort
│ belegt? Lösung zulässig? Tabellensummen richtig?
▼
Report (Markdown/HTML) + JSON + eigenständiges Solver-Skript
Unterstützte Problemklassen
| Klasse | Solver | Typische Aufgaben |
|---|---|---|
| Lineare Programmierung (LP) | SciPy linprog |
Mischungs-, einfache Produktionsprobleme |
| Gemischt-ganzzahlig (MILP) | HiGHS (highspy) |
Zuordnung, Rucksack, Standortwahl, Rüst-/Fixkosten |
| Constraint Programming (CP) | Google OR-Tools CP-SAT | Schicht- und Personalplanung, logische Bedingungen |
| Quadratisch / Kegel (QP, SOCP) | CVXPY | Portfoliooptimierung (Varianz, Volatilität) mit kontinuierlichen Anteilen |
| Gemischt-ganzzahlig quadratisch (MIQP) | SCIP (pyscipopt) |
Portfolio mit Kardinalität (mind./höchstens k Titel), Mindestgewicht nur für gewählte Titel, fixe Transaktionskosten |
| Nichtlinear, stetig (NLP) | Ipopt (cyipopt) |
nichtlineare Ausbeute- oder Kostenfunktionen, Kurvenanpassung; mehrere Startpunkte, Streuung wird berichtet |
| Kürzeste Wege (Graph) | NetworkX (Dijkstra) | Wegewahl im Netz, Ersatz- und Erneuerungsprobleme; der Report zeichnet den Graphen und zeigt den Markierungsverlauf |
Installation
Voraussetzungen: Python ≥ 3.10, Linux. Systempakete:
bubblewrapfür die Sandbox des erzeugten Codes (Debian/Ubuntu:sudo apt install bubblewrap, siehe Sicherheitshinweis)- die Ipopt-Bibliothek für
cyipopt(Debian/Ubuntu:sudo apt install coinor-libipopt-dev pkg-config), sofern pip kein fertiges Paket findet
python -m venv .venv
.venv/bin/pip install -r requirements.txt
API-Key als Umgebungsvariable setzen (nicht in Dateien speichern):
| Anbieter | Variable | Modellnamen |
|---|---|---|
| OpenRouter (Standard) | OPENROUTER_API_KEY |
mit /, z. B. deepseek/deepseek-v4.1-flash |
| Google Gemini | GEMINI_API_KEY |
ohne /, z. B. gemini-2.5-flash |
Der Anbieter wird am Modellnamen erkannt. Das kostenlose Gemini-Kontingent reicht nur für wenige Läufe pro Tag.
Benutzung
# Aufgabe aus Datei lösen, Report und Solver-Code speichern
.venv/bin/python TextToModel.py aufgabe.txt --report ausgabe/modell.html --code ausgabe/solve.py > ausgabe/ergebnis.json
# Aufgabe über stdin
echo "Ein Betrieb ..." | .venv/bin/python TextToModel.py --report ausgabe/modell.md
# nur modellieren und prüfen, nicht lösen; anderes Modell
.venv/bin/python TextToModel.py aufgabe.txt --no-solve --model deepseek/deepseek-v4-pro-0813
# Report nachträglich aus gespeichertem Ergebnis erzeugen (keine LLM-Kosten)
.venv/bin/python report.py ausgabe/ergebnis.json ausgabe/modell.md
# exportiertes Solver-Programm erneut ausführen
.venv/bin/python ausgabe/solve.py
# Variante rechnen: geänderte Eingabedaten, gleiches Programm, KEIN LLM-Aufruf
.venv/bin/python TextToModel.py --resolve ausgabe/ergebnis.json --data neue_daten.json > ausgabe/variante.json
# Aufgabe mit beigelegten Daten (CSV/Excel); das Profil geht in die Prompts, die Datei ins Programm
.venv/bin/python TextToModel.py aufgabe.md --daten kurse.csv --report ausgabe/modell.html
# nur die Solver-Stufe wiederholen: Modell und Prüfbericht bleiben, spart die Aufrufe davor
.venv/bin/python TextToModel.py --von solver ausgabe/ergebnis.json > ausgabe/neu.json
Der Ordner ausgabe/ ist in .gitignore eingetragen. Ohne Eingabe wird eine eingebaute Beispielaufgabe verwendet. Fortschritt und Kosten erscheinen auf stderr, das Ergebnis-JSON auf stdout.
| Option / Variable | Bedeutung | Standard |
|---|---|---|
--report DATEI |
Report als .md (z. B. VS Code-Vorschau: Strg+Umschalt+V) oder .html (Browser, Formeln via MathJax) |
– |
--code DATEI |
Solver-Programm als eigenständiges, dokumentiertes Python-Skript speichern | – |
--model / LLM_MODEL |
LLM-Modell | deepseek/deepseek-v4.1-flash |
--daten DATEI … |
beigelegte CSV-/Excel-Dateien; ihr Profil (Zeilen, Spalten, Spannweiten, Lücken) geht in die Prompts, die Dateien ins Arbeitsverzeichnis des Programms | – |
--von STUFE ERGEBNIS.JSON |
gespeichertes Ergebnis fortsetzen statt alles zu wiederholen: solver (nur Solver-Code neu) oder pruefung (ab Prüfung/Korrektur) |
– |
--max-revisions / MAX_REVISIONS |
Korrekturrunden | 2 |
--no-solve |
keinen Solver-Code erzeugen | aus |
SOLVER_RETRIES |
Reparaturversuche für fehlerhaften Solver-Code | 2 |
INFEASIBLE_RECHECK |
Gegenprobe mit unabhängiger Modellierung bei „unlösbar“ (0 schaltet ab) | 1 |
LLM_MAX_TOKENS |
Obergrenze je Antwort (OpenRouter) | 32000 |
--max-restarts / MAX_RESTARTS |
komplette Neustarts, wenn das Modell nach allen Korrekturrunden fehlerhaft ist | 1 |
OPENROUTER_MIN_THROUGHPUT |
bevorzugte Mindestgeschwindigkeit des OpenRouter-Anbieters (Tokens/s); Präferenz, kein harter Filter | 75 |
LLM_TIMEOUT |
Timeout je LLM-Aufruf (s) | 420 |
SOLVER_SANDBOX |
Isolierung des erzeugten Codes: auto (bubblewrap, wenn vorhanden), bwrap (Pflicht), aus |
auto |
LLM_MODEL_EXTRACTION, LLM_MODEL_REVIEW, LLM_MODEL_REVISION, LLM_MODEL_SOLVER |
Modell einzelner Pipeline-Stufen, z. B. ein schnelles für den Solver-Code; verschiedene Anbieter sind erlaubt | Modell aus --model |
Solver-Programme
Das erzeugte Programm (--code sowie im HTML-/Markdown-Report) ist separat verwendbar und aufgebaut als:
#!/usr/bin/env python3- Docstring mit Aufgabentext, mathematischem Modell (Indizes, Parameter, Variablen, Zielfunktion, Restriktionen – mit Erläuterungen und Annahmen) und dem Algorithmus samt Begründung, warum er hier passt
- kommentierter Python-Code: Imports, Eingabedaten als Konstanten, Modellfunktion(en),
main() if __name__ == "__main__": main()
Die Ausgabe ist lesbar – Status, Zielwert, verständliche Ergebnistabellen (z. B. je Aktie: Anteil, Rendite, Kosten, Beitrag zum Ziel, Summenzeile) und die Variablenwerte – und endet mit einer maschinenlesbaren RESULT_JSON-Zeile. Die Tabellen berechnet das Programm aus der tatsächlichen Lösung; die Pipeline prüft automatisch, dass die Beiträge sich zum Zielwert summieren, und warnt sonst. Der Report beginnt mit dem Ergebnis: ein Antwortsatz, die Kennzahlen, die Ergebnistabellen samt Diagramm (Balken je Objekt, Wasserfall für die Zielwert-Zerlegung aus Ertrag, Risiko und Kosten) und eine Tabelle der bindenden Restriktionen. Darunter folgen die Rohwerte nach Variablen gruppiert, danach Aufgabe, Modell, Prüfbericht und Programm. Die Diagramme sind reines SVG ohne externe Bibliothek und funktionieren offline, hell wie dunkel. Im HTML-Report lässt sich das Programm mit einem Klick auf das Kopier-Symbol in die Zwischenablage übernehmen. Das JSON-Ergebnis enthält weiterhin nur den rohen generierten Code.
Mit Datenanhängen (--daten): Das erzeugte Programm liest die Dateien über ihren Dateinamen aus dem Arbeitsverzeichnis. Ein mit --code exportiertes Programm braucht die Datendateien deshalb im selben Verzeichnis – sonst endet es mit FileNotFoundError. Kopiere sie mit:
.venv/bin/python TextToModel.py aufgabe.md --daten kurse.csv --code ausgabe/solve.py
cp kurse.csv ausgabe/ && (cd ausgabe && ../.venv/bin/python solve.py)
Wie verlässlich ist das?
Das LLM kann Fehler machen – deshalb gibt es mehrere Sicherungen:
- Zielgrößen-Prüfung: erkennt z. B., wenn die Varianz statt der gefragten Volatilität minimiert wird.
- Annahmen-Prüfung: Unklarheiten im Text müssen als Annahme ausgewiesen werden und werden gegen den Text geprüft (z. B. „wenn A, dann B“ ist keine Äquivalenz).
- Zulässigkeitsprüfung ohne LLM: Die gefundene Lösung wird in alle Restriktionen eingesetzt, Schranken und Ganzzahligkeit geprüft und abgeglichen, ob jede Restriktion des Modells im Programm vorkommt. Verletzungen lösen eine Reparatur aus – Solver-Toleranzen (beobachtet: 8 Cent über dem Budget) fallen damit auf.
- Unlösbare Aufgaben werden als
infeasiblegemeldet, nicht durch Weglassen von Bedingungen „passend gemacht“ – aber auch nicht ungeprüft geglaubt: Eine zweite, unabhängige Modellierung bestätigt oder widerlegt den Status. - Beste Korrekturrunde: Verschlechtert eine Korrektur das Modell, wird die frühere Fassung verwendet.
- Sensitivität: Für bindende Restriktionen rechnet das Programm mit verschobener rechter Seite neu; der Report zeigt, was eine Lockerung je Einheit bringt, und bei ganzzahligen Modellen den Preis der Ganzzahligkeit. Unplausible Richtungen werden gemeldet.
- Neustart bei Fremdproblem: Gelegentlich modelliert das LLM ein anderes als das gestellte Problem. Passt die Zielgröße auch nach allen Korrekturrunden nicht, wird die Modellierung komplett neu gestartet.
- Jede Teilfrage beantwortet: Der Aufgabentext wird vor dem Modellieren in seine Teilfragen zerlegt (a, b, …); das Programm liefert je Teilfrage einen Antwortsatz. Fehlt eine Antwort, gilt die Aufgabe als nicht erfüllt – auch wenn der Zielwert stimmt.
- Jede Zahl der Antwort belegt: Zahlen im Antworttext müssen aus dem Ergebnis oder der Aufgabe stammen (Rundungen und Einheitenumrechnungen werden akzeptiert). Ein Vergleichswert, der in keiner Tabelle steht, fällt auf.
- Jede Zahl der Aufgabe erfasst: Zahlen des Aufgabentexts müssen als Fakt mit Fundstelle oder mit Begründung als irrelevant ausgewiesen sein – sonst betreten sie die Pipeline erst im Solver-Code, hinter allen Prüfungen.
- Selbst erzeugte Daten sind gekennzeichnet: Erzeugt das Programm Eingabedaten (Zufallszahlen, synthetischer Markt, verkleinertes Universum), muss das in
data_assumptionsstehen, nicht nur im Fließtext – sonst folgt eine Reparatur und notfalls die Markierung „unzuverlässig“. - Szenarien nachgerechnet: Vergleicht die Aufgabe mehrere Konfigurationen, wird jede durch erneutes Ausführen des Programms bestätigt – ohne LLM.
- Sandbox: Der erzeugte Code läuft ohne Netzzugang, mit nur lesbarem Dateisystem und beschreibbar allein dem Arbeitsverzeichnis des Laufs.
- Kein falsches „optimal“: Bleibt das Modell fehlerhaft, wird das Ergebnis als unzuverlässig markiert – bei falscher Zielgröße ohne Solver-Lauf, mit Warnkasten im Report und Exit-Code 2.
Trotzdem gilt: Ergebnisse vor Verwendung prüfen – der Report zeigt dafür Modell, Annahmen, Prüfbericht und Code.
Benchmark
benchmark/cases/ enthält 19 Aufgaben mit unabhängig verifiziertem Optimum (Brute Force bzw. zweiter Solver) – von kleinen LPs bis zu Aufgaben mit eingebauten Fallen (Rüstkosten/Mindestlos, Wenn-dann-Regel als Implikation, Volatilität statt Varianz, nichtkonvexes NLP mit globalem Optimum, beweisbar unlösbare Mischung, Losgrößen als kürzester Weg). Gemessen wird der Zielwert und, strenger, ob die Aufgabe erfüllt ist: jede Teilfrage beantwortet, jede Zahl der Antwort belegt. Dateien im Fallverzeichnis außer task.txt/expected.json werden als Datenanhang übergeben.
.venv/bin/python benchmark.py --models deepseek/deepseek-v4.1-flash deepseek/deepseek-v4-flash-0731 --runs 2 --jobs 3
.venv/bin/python benchmark.py --cases hard_schichtplan_woche --runs 3
Letzter Stand (September 2026, DeepSeek V4.1-Flash über OpenRouter, je 2 Läufe): 38 von 38 Läufen über alle 19 Fälle mit richtigem Zielwert und erfüllter Aufgabe, etwa 0,05–0,15 $ und 3–10 Minuten je Lauf. Die Fallmenge ist damit gesättigt; die Grenzen der Pipeline zeigen sich eher an schlecht gestellten Aufgaben (fehlende Daten, mehrdeutige Angaben) – Beispiele dafür liegen in aufgaben/. Hinweis: Drei Fälle wurden nach dieser Messung sprachlich neu gefasst (gleiche Zahlen und Optima).
Tests
.venv/bin/python -m pytest -q # offline, ohne API-Kosten
.venv/bin/python -m pytest -q test_solver_stage.py -k cvxpy # einzelner Test
Projektstruktur
| Datei | Inhalt |
|---|---|
TextToModel.py |
CLI und Pipeline (run_pipeline) |
model_schema.py |
Pydantic-Schemas aller Zwischenergebnisse |
model_checks.py |
deterministische Konsistenz-Checks |
solver_stage.py |
Solver-Code erzeugen, prüfen, ausführen |
llm.py |
LLM-Zugriff (OpenRouter/Gemini), Wiederholungen, Kostenerfassung |
report.py |
Report als Markdown/HTML (mit Kopier-Symbol), auch als eigenständiges CLI |
script_export.py |
dokumentiertes, eigenständiges Solver-Programm (Kopf mit Aufgabe, Modell, Algorithmus) |
data_profile.py |
Profil beigelegter CSV-/Excel-Dateien, ohne LLM |
charts.py |
Diagramme und Graphenzeichnung als Inline-SVG |
benchmark.py, benchmark/cases/ |
Benchmark und Referenzaufgaben |
aufgaben/ |
weitere Aufgaben zum Ausprobieren (u. a. mit fehlenden Daten) |
*-Prompt.txt |
System-Prompts für Verstehen, Extraktion, Prüfung und Solver-Code |
test_*.py |
Offline-Tests |
Sicherheitshinweis
Der Solver-Code wird vom LLM erzeugt. Er läuft in einer bubblewrap-Sandbox: kein Netzzugang, das Dateisystem nur lesbar, beschreibbar allein das Temp-Verzeichnis des jeweiligen Laufs, HOME zeigt ebenfalls dorthin. Davor greifen weiterhin eine Import-Whitelist (nur Mathe- und Solver-Bibliotheken), das Verbot von open/eval/exec und ein Timeout.
Voraussetzung ist ein installiertes bubblewrap (Debian/Ubuntu: sudo apt install bubblewrap). Fehlt es, läuft der Code ungeschützt mit deinen Benutzerrechten – dann nur eigene, vertrauenswürdige Aufgabentexte verwenden. Mit SOLVER_SANDBOX=bwrap wird die Sandbox zur Pflicht: ohne bubblewrap wird der Lauf abgelehnt statt ungeschützt ausgeführt.
.venv/bin/python -m pytest -q -k sandbox # prüft, dass Schreiben außerhalb des Laufs scheitert
Lizenz
MIT – siehe LICENSE.