Was macht Regex vs. LLM für strukturierte Textanalyse?
Ein praktisches Entscheidungsframework zum Parsen strukturierter Texte (Quizze, Formulare, Rechnungen, Dokumente). Die Kernidee: Regex erledigt 95–98 % der Fälle günstig und deterministisch. Teure LLM-Aufrufe bleiben den verbleibenden Randfällen vorbehalten.
Wann aktivieren
- Parsen strukturierter Texte mit wiederkehrenden Mustern (Fragen, Formulare, Tabellen)
- Entscheidung zwischen Regex und LLM für Textextraktion
- Aufbau hybrider Pipelines, die beide Ansätze kombinieren
- Optimierung von Kosten-/Genauigkeits-Abwägungen bei der Textverarbeitung
Entscheidungsframework
Ist das Textformat konsistent und wiederkehrend?
├── Ja (>90 % folgt einem Muster) → Beginne mit Regex
│ ├── Regex schafft >95 % → Fertig, kein LLM nötig
│ └── Regex schafft <95 % → LLM nur für Randfälle
└── Nein (frei formuliert, stark variierend) → LLM direkt verwenden
Architekturmuster
Quelltext
│
▼
[Regex-Parser] ─── Extrahiert Struktur (95-98 % Genauigkeit)
│
▼
[Textbereiniger] ─── Entfernt Störungen (Markierungen, Seitenzahlen, Artefakte)
│
▼
[Konfidenz-Scorer] ─── Markiert Extraktionen mit niedriger Konfidenz
│
├── Hohe Konfidenz (≥0,95) → Direkte Ausgabe
│
└── Niedrige Konfidenz (<0,95) → [LLM-Validator] → Ausgabe
Implementierung
1. Regex-Parser (erledigt den Großteil)
import re
from dataclasses import dataclass
@dataclass(frozen=True)
class ParsedItem:
id: str
text: str
choices: tuple[str, ...]
answer: str
confidence: float = 1.0
def parse_structured_text(content: str) -> list[ParsedItem]:
"""Parsed strukturierten Text mittels Regex-Muster."""
pattern = re.compile(
r"(?P<id>\d+)\.\s*(?P<text>.+?)\n"
r"(?P<choices>(?:[A-D]\..+?\n)+)"
r"Answer:\s*(?P<answer>[A-D])",
re.MULTILINE | re.DOTALL,
)
items = []
for match in pattern.finditer(content):
choices = tuple(
c.strip() for c in re.findall(r"[A-D]\.\s*(.+)", match.group("choices"))
)
items.append(ParsedItem(
id=match.group("id"),
text=match.group("text").strip(),
choices=choices,
answer=match.group("answer"),
))
return items
2. Konfidenz-Scoring
Markiere Elemente, die eine LLM-Überprüfung benötigen könnten:
@dataclass(frozen=True)
class ConfidenceFlag:
item_id: str
score: float
reasons: tuple[str, ...]
def score_confidence(item: ParsedItem) -> ConfidenceFlag:
"""Bewertet die Extraktionskonfidenz und markiert Probleme."""
reasons = []
score = 1.0
if len(item.choices) < 3:
reasons.append("wenige_auswahlmoeglichkeiten")
score -= 0.3
if not item.answer:
reasons.append("fehlende_antwort")
score -= 0.5
if len(item.text) < 10:
reasons.append("kurzer_text")
score -= 0.2
return ConfidenceFlag(
item_id=item.id,
score=max(0.0, score),
reasons=tuple(reasons),
)
def identify_low_confidence(
items: list[ParsedItem],
threshold: float = 0.95,
) -> list[ConfidenceFlag]:
"""Gibt Elemente unterhalb des Konfidenz-Schwellenwerts zurück."""
flags = [score_confidence(item) for item in items]
return [f for f in flags if f.score < threshold]
3. LLM-Validator (nur Randfälle)
def validate_with_llm(
item: ParsedItem,
original_text: str,
client,
) -> ParsedItem:
"""Nutzt LLM zur Korrektur von Extraktionen mit niedriger Konfidenz."""
response = client.messages.create(
model="claude-haiku-4-5-20251001", # Günstigstes Modell für Validierung
max_tokens=500,
messages=[{
"role": "user",
"content": (
f"Extrahiere die Frage, die Auswahlmöglichkeiten und die Antwort aus diesem Text.\n\n"
f"Text: {original_text}\n\n"
f"Aktuelle Extraktion: {item}\n\n"
f"Gib korrigiertes JSON zurück, falls nötig, oder 'KORREKT', wenn alles stimmt."
),
}],
)
# Analysiere LLM-Antwort und gib korrigiertes Item zurück...
return corrected_item
4. Hybride Pipeline
def process_document(
content: str,
*,
llm_client=None,
confidence_threshold: float = 0.95,
) -> list[ParsedItem]:
"""Vollständige Pipeline: Regex -> Konfidenzprüfung -> LLM für Randfälle."""
# Schritt 1: Regex-Extraktion (erledigt 95-98 %)
items = parse_structured_text(content)
# Schritt 2: Konfidenz-Scoring
low_confidence = identify_low_confidence(items, confidence_threshold)
if not low_confidence or llm_client is None:
return items
# Schritt 3: LLM-Validierung (nur für markierte Elemente)
low_conf_ids = {f.item_id for f in low_confidence}
result = []
for item in items:
if item.id in low_conf_ids:
result.append(validate_with_llm(item, content, llm_client))
else:
result.append(item)
return result
Praxis-Metriken
Aus einer Produktionspipeline zum Parsen von Quizfragen (410 Elemente):
| Metrik | Wert |
|---|---|
| Regex-Erfolgsrate | 98,0 % |
| Elemente mit niedriger Konfidenz | 8 (2,0 %) |
| Benötigte LLM-Aufrufe | ~5 |
| Kosteneinsparung im Vergleich zu reinem LLM | ~95 % |
| Testabdeckung | 93 % |
Bewährte Praktiken
- Beginne mit Regex – selbst unvollkommenes Regex gibt dir eine Ausgangsbasis zur Verbesserung
- Nutze Konfidenz-Scoring, um programmatisch zu identifizieren, was LLM-Unterstützung braucht
- Verwende das günstigste LLM für die Validierung (Modelle der Haiku-Klasse sind ausreichend)
- Verändere geparste Elemente niemals – erstelle neue Instanzen bei Bereinigungs-/Validierungsschritten
- TDD eignet sich gut für Parser – schreibe zuerst Tests für bekannte Muster, dann für Randfälle
- Protokolliere Metriken (Regex-Erfolgsrate, Anzahl LLM-Aufrufe), um die Pipeline-Gesundheit zu überwachen
Anti-Patterns (vermeiden)
- Senden des gesamten Texts an ein LLM, obwohl Regex 95 %+ der Fälle abdeckt (teuer und langsam)
- Regex für frei formulierte, stark variierende Texte verwenden (LLM ist hier besser geeignet)
- Konfidenz-Scoring auslassen und hoffen, dass Regex „einfach funktioniert“
- Geparste Objekte während Bereinigungs-/Validierungsschritten verändern
- Randfälle nicht testen (fehlerhafte Eingabe, fehlende Felder, Kodierungsprobleme)
Wann verwenden
- Parsen von Quiz-/Prüfungsfragen
- Extraktion von Formulardaten
- Rechnungs-/Belegverarbeitung
- Parsen von Dokumentstrukturen (Kopfzeilen, Abschnitte, Tabellen)
- Beliebige strukturierte Texte mit wiederkehrenden Mustern, bei denen Kosten eine Rolle spielen