Die aktuelle Pipeline verarbeitet eingescannte Dokumente in sieben Stufen. Kernänderung gegenüber dem ursprünglichen Ansatz: KVP liefert räumliche Paare als strukturierten Kontext für das LLM. Das LLM bekommt keine Format-Restriktionen — nur Labels, Enums und semantische Hints. Alle Formatprüfungen passieren deterministisch danach.
DLA + OCR (Surya /analyze) Deterministisch
Layout-Erkennung + OCR in einem Schritt. Liefert Zeilen mit Bounding Boxes, gruppiert in Layout-Regionen (Sections).
OCR-Qualitätsprüfung Deterministisch
Proxy-Score aus Zeichenqualität, Worterkennung, Strukturerkennung und Längenverhältnis. Kein ML — rein heuristisch.
KVP — Räumliche Paare Deterministisch Kernänderung
Findet räumlich benachbarte Textblöcke per asymmetrischer Distanz (rechts/unten bevorzugt). Keine Label/Value-Klassifikation — das Zuordnen übernimmt das LLM. Mutual-Nearest-Neighbor-Pairing (Pass 1) + distanzsortierter Fallback (Pass 2). Sektionsübergreifend, ~0ms.
LLM-Extraktion (Ollama) LLM
Prompt enthält param_hints (semantische Feldbeschreibungen aus YAML), Enum-Werte und gefilterte KVP-Paare als räumlichen Kontext („ORT steht neben Stuttgart“). Bewusst keine Format/Pattern/MaxLength-Hints — die biased das LLM zum Erfinden passender Werte statt zum Lesen.
Deterministische Validierung Deterministisch
Prüft jeden extrahierten Wert gegen die ParamMeta-Restriktionen. Auto-Korrekturen: trim, strip, Datumsformat, Dezimalbereinigung. Ergebnis pro Feld: ok / corrected / failed / missing.
Bestätigungsdialog Human
Quelldokument + extrahierte Werte mit Ampel-Status. Fehlgeschlagene Felder hervorgehoben. User bestätigt, korrigiert oder bricht ab.
Form Fill + Self-Repair Deterministisch LLM
Formulardaten werden eingetragen. Bei Server-seitigen Validierungsfehlern greift der bestehende Self-Repair-Mechanismus (LLM-Korrektur → ggf. Human Fallback).
Surya liefert keinen eigenen Konfidenz-Score. Stattdessen wird ein Proxy-Score aus dem Text selbst berechnet — rein deterministisch, kein ML.
| Check | Gewichtung | Was wird gemessen |
|---|---|---|
| Zeichenqualität | 30% | Anteil druckbarer Zeichen vs. Steuerzeichen, Box-Symbole, Null-Bytes |
| Worterkennung | 30% | Anteil erkannter Wörter (einfaches Wörterbuch / Sprachcheck) |
| Strukturerkennung | 20% | Enthält der Text erkennbare Muster: Datumsformate, PLZ-artige Zahlen, Namens-Patterns |
| Längenverhältnis | 20% | Ist die Textlänge plausibel für die Dokumentgröße, oder verdächtig kurz/leer |
KVP findet räumlich benachbarte Textblöcke — ohne zu wissen, was Labels und was Werte sind. Die Zuordnung zu YAML-Feldern ist nicht KVPs Aufgabe. Es liefert nur: „diese zwei Blöcke stehen nebeneinander.“
| Schritt | Was passiert |
|---|---|
| Flatten | Alle OCR-Zeilen über alle Sektionen sammeln. Noise-Filter: leere Zeilen und Zeilen >80 Zeichen raus. |
| Nearest Neighbor | Für jede Zeile den nächsten Nachbarn per asymmetrischer Distanz finden. Rechts-von und unterhalb werden bevorzugt (Formular-Pattern). |
| Pass 1 — Mutual | Wenn A→B und B→A → Paar mit hoher Konfidenz (gegenseitig nächste Nachbarn). |
| Pass 2 — Fallback | Verbleibende Zeilen, sortiert nach Distanz. A→B aber B→C → trotzdem Paar, niedrigere Konfidenz. Sortierung verhindert, dass entfernte Zeilen nahe Nachbarn „stehlen“. |
| Filter | Paare wo beide Seiten ALL-CAPS ohne Ziffern sind (zwei Headings/Labels) werden vor der LLM-Übergabe entfernt. |
Das LLM bekommt drei Informationsquellen: param_hints (semantische Feldbeschreibungen ohne Beispielwerte), Enum-Listen (erlaubte Werte) und KVP-Paare als räumlichen Kontext.
Prüft jeden extrahierten Wert gegen die Restriktionen aus ParamMeta. Keine Prüfung ohne Deklaration — wenn ein
Feld kein pattern hat, wird es nicht geprüft. Das System macht keine Annahmen.
| Check | ParamMeta-Feld | Aktion bei Fehler |
|---|---|---|
| Regex-Match | pattern |
Auto-Fix versuchen (strip non-matching chars), sonst failed |
| Maximale Länge | maxLength |
Abschneiden + corrected |
| Erlaubte Werte | enum |
Fuzzy-Match (Levenshtein) gegen enum-Liste, sonst failed |
| Format | format |
Datum: parse + reformat zu erwartetem Format, sonst failed |
| Pflichtfeld leer | required |
Status missing |
Keine fachliche Logik. “Zählernummer existiert im System” oder “Adresse ist im Versorgungsgebiet” sind Server-Validierungen. Die fängt der bestehende Self-Repair-Mechanismus nach dem Form Fill.
Die ParamMeta-Felder (pattern, maxLength, enum, format) werden aus verschiedenen Quellen befüllt
— manuell heute, automatisch morgen.
Szenario-YAML
Der Szenario-Autor trägt bekannte Restriktionen direkt in die YAML-Datei ein. Jedes Szenario definiert seine eigenen Regeln.
Extension Validation Probe
Der Run-Typ “Eingabeprüfung” testet Formularfelder gezielt mit ungültigen Werten und leitet daraus technische Restriktionen ab (Regex, Zeichenklassen, Längen).
OpenAPI-Spec (API Actor)
Wenn der API Actor gegen eine dokumentierte API arbeitet, enthält die OpenAPI-Spec bereits Pattern, Enum und Format-Definitionen für jeden Parameter.
Wichtige Grenze: Nur technische Restriktionen sind automatisch entdeckbar — Zeichenklassen, Längen, erlaubte Werte. Fachspezifische Logik (Adresse muss im Versorgungsgebiet liegen, Zählernummer muss im System existieren) ist nicht durch Probieren ermittelbar und wird weiterhin reaktiv vom Self-Repair behandelt.
Der neue Bestätigungsdialog ersetzt die bisherige einfache Wertabfrage. Er zeigt das Quelldokument neben den extrahierten Werten, mit Ampel-Status pro Feld.
Quelldokument
Name: Maria Huber
Straße: Hauptstr. 12
PLZ / Ort: 80331 München
Zählernr.: 48O7123
Einzug: 1. April 2025
Tarif: ÖkoStrom
Extrahierte Daten
Grün = Wert ok. Gelb = automatisch korrigiert (Original sichtbar). Rot = ungültig oder fehlend, User muss eingeben. Der OCR-Qualitätsscore wird als Info-Badge angezeigt.
GLiNER übernimmt das semantische Mapping für eindeutige Fälle (~50ms, deterministisch). Das LLM wird nur noch für Felder aufgerufen, die GLiNER nicht zuordnen kann. Entscheidend: GLiNER bekommt strukturierte KVP-Paare als Input, nicht rohen OCR-Text.
KVP — Räumliche Paare Deterministisch
Wie Phase 1. Output: räumliche Paare mit Konfidenz.
GLiNER — Semantisches Mapping Deterministisch Neu
Input: strukturierter Text aus KVP-Paaren („NACHNAME neben Schneider“) + YAML-Feldlabels als Entity-Liste. Muss nur noch semantisch zuordnen, nicht mehr Labels von Werten unterscheiden. ~50ms.
LLM-Fallback LLM
Nur für Felder die GLiNER nicht zuordnen konnte. Selber Prompt wie Phase 1, aber weniger Felder. ~1500ms.
Eskalationskette:
KVP
„was steht nebeneinander?“
~0ms, deterministisch
GLiNER
„welches Feld passt?“
~50ms, deterministisch
LLM
„was bedeutet das im Kontext?“
~1500ms, probabilistisch
Mensch
„bitte prüfen“
∞
Der LLM ist die letzte Instanz vor dem Menschen — nicht die zweite. Wenn GLiNER nicht verfügbar ist, fällt die Pipeline auf Phase 1 zurück (LLM übernimmt alles).
Was NICHT funktioniert hat:
| Ansatz | Problem |
|---|---|
| GLiNER auf rohem OCR-Text | Kann Labels nicht von Werten unterscheiden. Gibt Labels als Werte zurück, klebt Zeilen zusammen. 4/10 Felder. |
| ALL-CAPS-Heuristik für Label-Erkennung | Bei handschriftlichen deutschen Formularen sind auch Werte in Großbuchstaben. Falsche Paare mit 0.95 Konfidenz. |
| GLiNER-Ergebnisse als LLM-Kontext | Falsche GLiNER-Zuordnungen vergiften den LLM-Prompt. 2/10 statt 5/10 ohne GLiNER. |
| Format-Hints im Extraktions-Prompt | LLM erfindet passende Werte statt zu lesen. Auch Beispielwerte in param_hints biased das Ergebnis. |
Was funktioniert hat (Phase 1 → 9/10):
| Ansatz | Wirkung |
|---|---|
| KVP Spatial Pairing ohne Klassifikation | Räumliche Paare als Kontext: „ORT steht neben Stuttgart“ — LLM weiß wo Werte stehen. |
| param_hints ohne Beispielwerte | Semantische Beschreibungen: „oft als NR. beschriftet“ half dem LLM, Hausnummer zu finden obwohl KVP sie nicht gepairt hatte. |
| Gefilterte KVP-Paare | Paare wo beide Seiten ALL-CAPS sind (zwei Headings) werden entfernt → weniger Noise für den LLM. |