From 101d55b7139734139352a37931ebdab24b974a8c Mon Sep 17 00:00:00 2001 From: dschlueter Date: Thu, 17 Sep 2026 18:01:37 +0200 Subject: [PATCH] fix: Gegenprobe der Abbildungen je Datenzeile; Standardmodell ~deepseek/deepseek-flash-latest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - check_figure_readings vergleicht Zahlentupel je Datenzeile statt aller Zahlen des Freitexts (Fehlalarm im ersten echten Lauf: zusätzliche Knotenaufzählung); fängt falsch zugeordnete Bewertungen und vertauschte Pfeilrichtungen - feste Form 'Start → Ziel: Bewertung' in Verstehen- und Abbildungs-Prompt; abweichende Lesungen stehen im Log - Standardmodell ist der Alias ~deepseek/deepseek-flash-latest (derzeit V4.1 Flash); Benchmarks weiter mit festen IDs Co-Authored-By: Claude Opus 5 --- BEDIENUNGSANLEITUNG.md | 6 ++-- README.md | 2 +- TextToModel.py | 3 ++ checks/model_checks.py | 45 ++++++++++++++++++----------- core/llm.py | 4 ++- core/model_schema.py | 4 +-- tests/test_figures.py | 13 ++++++++- understanding/Abbildungs-Prompt.txt | 4 +-- understanding/Verstehen-Prompt.txt | 2 +- 9 files changed, 55 insertions(+), 28 deletions(-) diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md index b9df96a..b4797d3 100644 --- a/BEDIENUNGSANLEITUNG.md +++ b/BEDIENUNGSANLEITUNG.md @@ -325,7 +325,7 @@ Was dabei passiert: Voraussetzungen und Grenzen: -- Das Modell der Verstehensstufe muss Bilder lesen können. `deepseek/deepseek-v4.1-flash` kann das; sonst ein Bildmodell über `LLM_MODEL_UNDERSTANDING` wählen (z. B. `qwen/qwen3.8-flash`). +- Das Modell der Verstehensstufe muss Bilder lesen können. Das Standardmodell `~deepseek/deepseek-flash-latest` (derzeit DeepSeek V4.1 Flash) kann das; sonst ein Bildmodell über `LLM_MODEL_UNDERSTANDING` wählen (z. B. `qwen/qwen3.8-flash`). - Höchstens 6 Abbildungen, je 5 MB. - Mehrkosten: ein bis zwei zusätzliche kleine Aufrufe für die Gegenprobe. - Verweise auf Webadressen werden nicht geladen (das Programm lädt nichts aus dem Internet). @@ -401,7 +401,7 @@ Wo das Geld hingeht: rund zwei Drittel der Kosten sind **erzeugter Output**, dav **Prompt-Cache:** Alle Aufrufe eines Laufs gehen an denselben OpenRouter-Anbieter, damit dessen Cache wiederholt gelesene Prompt-Teile billiger abrechnen kann. Der Cache-Anteil steht in der Abschlusszeile. -**Feste Modellnamen verwenden** (z. B. `deepseek/deepseek-v4.1-flash`), keine `…-latest`-Aliase – sonst ändern sich Ergebnisse unbemerkt. +**Standardmodell ist der Alias `~deepseek/deepseek-flash-latest`** – er zeigt immer auf das neueste DeepSeek-Flash-Modell (September 2026: V4.1 Flash). Das hält die Pipeline aktuell, kann Ergebnisse aber unbemerkt verändern, sobald DeepSeek ein neues Flash-Modell veröffentlicht. **Für Benchmarks und Vergleiche feste Modellnamen verwenden** (z. B. `--models deepseek/deepseek-v4.1-flash`). --- @@ -429,7 +429,7 @@ Wo das Geld hingeht: rund zwei Drittel der Kosten sind **erzeugter Output**, dav |---|---|---| | `OPENROUTER_API_KEY` | Schlüssel für OpenRouter | – | | `GEMINI_API_KEY` | Schlüssel für Gemini | – | -| `LLM_MODEL` | Modell, falls `--model` fehlt | `deepseek/deepseek-v4.1-flash` | +| `LLM_MODEL` | Modell, falls `--model` fehlt | `~deepseek/deepseek-flash-latest` | | `LLM_MODEL_UNDERSTANDING` | Modell für Verstehen und Abbildungs-Gegenprobe (muss bei Abbildungen Bilder lesen können) | Session-Modell | | `LLM_MODEL_EXTRACTION` | Modell nur für die Extraktion | Session-Modell | | `LLM_MODEL_REVIEW` | Modell nur für die Prüfung | Session-Modell | diff --git a/README.md b/README.md index b31be28..597e57f 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ Exit-Codes: `0` Ergebnis zuverlässig, `2` Ergebnis vorhanden, aber unzuverläss |---|---|---| | `--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` | +| `--model` / `LLM_MODEL` | LLM-Modell | `~deepseek/deepseek-flash-latest` (Alias, derzeit 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 | diff --git a/TextToModel.py b/TextToModel.py index 08f85eb..3bcd6b1 100644 --- a/TextToModel.py +++ b/TextToModel.py @@ -81,6 +81,9 @@ def _checked_figure_readings(session, task: str, figures, first, label: str) -> if not befunde: return first, [] log(f"[{label}] Abbildungen uneinheitlich gelesen – dritte Lesung: " + "; ".join(i.message for i in befunde)[:300]) + for nummer, lesung in ((1, first), (2, zweite)): + for f in lesung: + log(f" Lesung {nummer}, {f.id}: " + f.transcription.replace("\n", " | ")[:300]) dritte = read_figures(session, task, figures).figures if not check_figure_readings(first, dritte): return first, [] diff --git a/checks/model_checks.py b/checks/model_checks.py index 4090a65..fdc8554 100644 --- a/checks/model_checks.py +++ b/checks/model_checks.py @@ -206,23 +206,35 @@ def check_understanding(understanding, task: str) -> List[CheckIssue]: return issues -def _numbers_multiset(text: str) -> Counter: - """Zahlen eines Textes mit Häufigkeit – eine Kante mit Bewertung 3 zweimal ist nicht dasselbe wie einmal.""" - zaehler = Counter() - for roh in NUMBER_PATTERN.findall(text or ""): - try: - zaehler[normalize_number(roh)] += 1 - except ValueError: - continue - return zaehler +def _number_rows(text: str) -> Counter: + """Je Datenzeile das Tupel ihrer Zahlen: „Pfeil 1 → 2: 3“ und „(1,2): 3“ ergeben beide (1, 2, 3). + + Zeilen trennt der Zeilenumbruch oder ein Semikolon; Zeilen ohne Zahl zählen nicht. So ist der Vergleich + unabhängig von der Schreibweise, bemerkt aber eine Bewertung am falschen Pfeil. + """ + zeilen = Counter() + # „(1,2)“ ist ein Pfeil, keine Dezimalzahl 1,2 – ganzzahlige Paare in Klammern vorher trennen + text = re.sub(r"\((\d+)\s*,\s*(\d+)\)", r"(\1 \2)", text or "") + for zeile in re.split(r"[\n;]", text): + tupel = [] + for roh in NUMBER_PATTERN.findall(zeile): + try: + tupel.append(normalize_number(roh)) + except ValueError: + continue + if tupel: + zeilen[tuple(tupel)] += 1 + return zeilen def check_figure_readings(first, second) -> List[CheckIssue]: """Zwei unabhängige Übertragungen derselben Abbildungen vergleichen – Zahl für Zahl, ohne LLM. Ein Bildmodell kann eine Pfeilbewertung dem falschen Pfeil zuordnen oder eine Ziffer verlesen; eine einzige - Lesung wäre dann ungeprüft die Datengrundlage jeder weiteren Stufe. Verglichen werden die Zahlen je Abbildung - samt Häufigkeit. Richtungsfehler bei gleichen Zahlen fängt das nicht – dafür steht das Original im Report. + Lesung wäre dann ungeprüft die Datengrundlage jeder weiteren Stufe. Verglichen werden die Datenzeilen als + Zahlentupel (`_number_rows`). Erster Ansatz – alle Zahlen des Freitexts – schlug im ersten echten Lauf + fälschlich an: eine Lesung zählte zusätzlich „Knoten: 1, 2, 3, 4“ auf (test_Image.md, 17.09.2026). + Eine vertauschte Pfeilrichtung (2 → 3 statt 3 → 2) fällt auf; eine in BEIDEN Lesungen gleich falsche nicht. """ issues = [] erste = {f.id: f for f in first} @@ -232,15 +244,14 @@ def check_figure_readings(first, second) -> List[CheckIssue]: issues.append(CheckIssue(severity=CheckSeverity.ERROR, code="FIGURE_READINGS_DIFFER", message=f"{kennung} wurde nur in einer von zwei Lesungen übertragen.")) continue - a = _numbers_multiset(erste[kennung].transcription) - b = _numbers_multiset(zweite[kennung].transcription) + a = _number_rows(erste[kennung].transcription) + b = _number_rows(zweite[kennung].transcription) if a != b: - nur_a = sorted((a - b).elements(), key=float) - nur_b = sorted((b - a).elements(), key=float) + zeilen = lambda z: ", ".join("(" + " ".join(t) + ")" for t in sorted(z.elements())) or "–" issues.append(CheckIssue( severity=CheckSeverity.ERROR, code="FIGURE_READINGS_DIFFER", - message=f"{kennung} uneinheitlich gelesen – nur in Lesung 1: {', '.join(nur_a) or '–'}; " - f"nur in Lesung 2: {', '.join(nur_b) or '–'}.")) + message=f"{kennung} uneinheitlich gelesen – Datenzeilen nur in Lesung 1: {zeilen(a - b)}; " + f"nur in Lesung 2: {zeilen(b - a)}.")) return issues diff --git a/core/llm.py b/core/llm.py index 0e65fc4..b80e33c 100644 --- a/core/llm.py +++ b/core/llm.py @@ -14,7 +14,9 @@ import instructor from core.model_schema import CallRecord, LLMUsage -DEFAULT_MODEL = os.environ.get("LLM_MODEL", "deepseek/deepseek-v4.1-flash") +# Alias auf das jeweils neueste DeepSeek-Flash-Modell (September 2026: V4.1 Flash, nimmt auch Bilder an). +# Für vergleichbare Benchmarks ein festes Modell mit --models angeben. +DEFAULT_MODEL = os.environ.get("LLM_MODEL", "~deepseek/deepseek-flash-latest") # Modell je Pipeline-Stufe: ein starkes Modell für Extraktion/Prüfung, ein schnelles für den Solver-Code # (oder umgekehrt, je nach Messung). Leer = das Modell der Session (LLM_MODEL bzw. --model). STEP_MODEL_ENV = { diff --git a/core/model_schema.py b/core/model_schema.py index 928f75c..68ca9e6 100644 --- a/core/model_schema.py +++ b/core/model_schema.py @@ -132,8 +132,8 @@ class FigureTranscription(BaseModel): id: str = Field(description="Kennung wie im Text, z. B. 'Abbildung 1'") shows: str = Field(description="Was die Abbildung zeigt, in einem Satz (z. B. 'gerichteter Graph mit 4 Knoten')") transcription: str = Field( - description="Vollständige Übertragung ALLER Inhalte: jede Zahl, jede Beschriftung, jede Pfeilrichtung, " - "am besten als Tabelle oder Liste (z. B. 'Pfeil 1→2: 3'). Nichts ergänzen, was nicht zu sehen ist.") + description="Nur die Daten, eine Zeile je Datenpunkt, jede Zahl genau einmal (Graph: '1 → 2: 3' je Pfeil; " + "Tabelle: 'Zeile / Spalte: Wert'). Keine Knotenaufzählung, keine Lagebeschreibung – das gehört nach shows.") uncertain: List[str] = Field( default_factory=list, description="Stellen, die nicht sicher lesbar sind (verdeckt, unscharf, mehrdeutig); leer = alles sicher gelesen") diff --git a/tests/test_figures.py b/tests/test_figures.py index f8269f9..2efd72d 100644 --- a/tests/test_figures.py +++ b/tests/test_figures.py @@ -88,7 +88,7 @@ def test_identical_readings_have_no_finding(): def test_misread_value_is_found_with_both_numbers(): befunde = check_figure_readings([GRAPH], [VERLESEN]) assert len(befunde) == 1 and befunde[0].code == "FIGURE_READINGS_DIFFER" - assert "nur in Lesung 1: 3" in befunde[0].message and "nur in Lesung 2: 5" in befunde[0].message + assert "nur in Lesung 1: (2 4 3)" in befunde[0].message and "nur in Lesung 2: (2 4 5)" in befunde[0].message def test_figure_missing_in_one_reading_is_found(): @@ -139,3 +139,14 @@ def test_report_shows_original_and_transcription(): assert f'