Regex vs LLM pour l'Analyse de Texte Structuré
Cadre décisionnel pratique pour analyser du texte structuré (quiz, formulaires, factures, documents). L'idée clé : les regex traitent 95-98 % des cas de manière peu coûteuse et déterministe. Réservez les appels LLM coûteux aux cas limites restants.
Quand l'Activer
- Analyse de texte structuré avec motifs répétitifs (questions, formulaires, tableaux)
- Décision entre regex et LLM pour l'extraction de texte
- Construction de pipelines hybrides combinant les deux approches
- Optimisation du rapport coût/précision dans le traitement de texte
Cadre Décisionnel
Le format du texte est-il cohérent et répétitif ?
├── Oui (>90 % suit un motif) → Commencez par les Regex
│ ├── Les Regex gèrent 95%+ → Terminé, aucun LLM nécessaire
│ └── Les Regex gèrent <95 % → Ajoutez un LLM pour les cas limites uniquement
└── Non (texte libre, très variable) → Utilisez directement le LLM
Architecture du Pipeline
Texte Source
│
▼
[Analyseur Regex] ─── Extrait la structure (95-98 % de précision)
│
▼
[Nettoyeur de Texte] ─── Supprime le bruit (marqueurs, numéros de page, artefacts)
│
▼
[Scoreur de Confiance] ─── Signale les extractions à faible confiance
│
├── Haute confiance (≥0.95) → Sortie directe
│
└── Faible confiance (<0.95) → [Validateur LLM] → Sortie
Implémentation
1. Analyseur Regex (Gère la Majorité)
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]:
"""Analyse le texte structuré à l'aide de motifs regex."""
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. Score de Confiance
Signale les éléments pouvant nécessiter une révision par LLM :
@dataclass(frozen=True)
class ConfidenceFlag:
item_id: str
score: float
reasons: tuple[str, ...]
def score_confidence(item: ParsedItem) -> ConfidenceFlag:
"""Score la confiance de l'extraction et signale les problèmes."""
reasons = []
score = 1.0
if len(item.choices) < 3:
reasons.append("few_choices")
score -= 0.3
if not item.answer:
reasons.append("missing_answer")
score -= 0.5
if len(item.text) < 10:
reasons.append("short_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]:
"""Retourne les éléments en dessous du seuil de confiance."""
flags = [score_confidence(item) for item in items]
return [f for f in flags if f.score < threshold]
3. Validateur LLM (Cas Limites Uniquement)
def validate_with_llm(
item: ParsedItem,
original_text: str,
client,
) -> ParsedItem:
"""Utilise un LLM pour corriger les extractions à faible confiance."""
response = client.messages.create(
model="claude-haiku-4-5-20251001", # Modèle le moins cher pour la validation
max_tokens=500,
messages=[{
"role": "user",
"content": (
f"Extrayez la question, les choix et la réponse de ce texte.\n\n"
f"Texte : {original_text}\n\n"
f"Extraction actuelle : {item}\n\n"
f"Retournez un JSON corrigé si nécessaire, ou 'CORRECT' si exact."
),
}],
)
# Analyse de la réponse du LLM et retour de l'élément corrigé...
return corrected_item
4. Pipeline Hybride
def process_document(
content: str,
*,
llm_client=None,
confidence_threshold: float = 0.95,
) -> list[ParsedItem]:
"""Pipeline complet : regex → vérification de confiance → LLM pour les cas limites."""
# Étape 1 : Extraction par regex (gère 95-98 %)
items = parse_structured_text(content)
# Étape 2 : Score de confiance
low_confidence = identify_low_confidence(items, confidence_threshold)
if not low_confidence or llm_client is None:
return items
# Étape 3 : Validation par LLM (uniquement pour les éléments signalés)
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
Métriques Réelles
D'après un pipeline de production d'analyse de quiz (410 éléments) :
| Métrique | Valeur |
|---|---|
| Taux de succès des regex | 98,0 % |
| Éléments à faible confiance | 8 (2,0 %) |
| Appels LLM nécessaires | ~5 |
| Économies vs tout-LLM | ~95 % |
| Couverture des tests | 93 % |
Bonnes Pratiques
- Commencez par les regex — même une regex imparfaite vous donne une base de référence à améliorer
- Utilisez le score de confiance pour identifier programmatiquement ce qui nécessite l'aide du LLM
- Utilisez le LLM le moins cher pour la validation (les modèles de classe Haiku sont suffisants)
- Ne mutez jamais les éléments analysés — retournez de nouvelles instances lors des étapes de nettoyage/validation
- Le TDD fonctionne bien pour les analyseurs — écrivez d'abord des tests pour les motifs connus, puis pour les cas limites
- Enregistrez les métriques (taux de succès des regex, nombre d'appels LLM) pour surveiller la santé du pipeline
Anti-Patterns à Éviter
- Envoyer tout le texte à un LLM alors que les regex gèrent 95 %+ des cas (coûteux et lent)
- Utiliser les regex pour du texte libre très variable (le LLM est meilleur ici)
- Omettre le score de confiance et espérer que les regex « fonctionnent comme ça »
- Muter les objets analysés pendant les étapes de nettoyage/validation
- Ne pas tester les cas limites (entrée mal formée, champs manquants, problèmes d'encodage)
Quand l'Utiliser
- Analyse de questions d'examen/quiz
- Extraction de données de formulaires
- Traitement de factures/reçus
- Analyse de structure de documents (en-têtes, sections, tableaux)
- Tout texte structuré avec motifs répétitifs où le coût est important