~/blog / ocr-cartes-grises-claude
🤖 ai in production · cas SaaS automobile

OCR de cartes grises avec Claude : le pipeline 3 phases qui coûte moitié moins

Luc Del Beato 11 juin 2026 11 min de lecture

Chez un SaaS B2B de l'automobile, des pros uploadent des photos de documents prises au téléphone, souvent floues, parfois de travers. Il faut reconnaître 11 types de documents et en extraire les champs critiques, sans se tromper. Parce qu'une immatriculation mal lue, c'est le rejet assuré. Voici comment classifier-puis-extraire bat extraire-et-prier, en coût comme en précision.

TL;DR

L'enjeu : un champ qui décide de tout

Ce SaaS automobile s'appuie sur une API d'immatriculation officielle. Le métier paraît banal, un pro prend en photo une carte grise, on en sort les infos, jusqu'à ce qu'on regarde de près ce qui se passe quand ça rate. L'extraction d'un document auto français finit, presque toujours, par alimenter une démarche d'immatriculation officielle. Et là, une seule erreur ne pardonne pas : une lecture erronée est la première cause de rejet.

Le problème, c'est que les conditions sont hostiles. Les glyphes français (le 0 contre le O, le 1 contre le I, les tirets), des photos prises au téléphone dans une cour de garage, du flou de bougé, des reflets de plastique. Et il ne s'agit pas que de la carte grise : il faut savoir reconnaître 11 types de documents, carte grise, Cerfa 15776 de cession, Cerfa 13750 d'immatriculation, mandat, attestation d'assurance, contrôle technique, pièce d'identité, permis, KBIS, justificatif de domicile, attestation d'hébergement, puis en extraire les champs qui comptent : immatriculation, VIN, SIRET, dates.

La vraie question n'était pas « comment extraire le texte ? » mais « comment ne pas se tromper sur le seul champ dont l'erreur fait tout dérailler ? »

L'approche naïve, et pourquoi elle saigne

La première version, comme souvent, faisait au plus simple : on suppose que c'est une carte grise (le cas le plus fréquent), on lance un gros prompt d'extraction, et si le résultat ne colle pas au schéma attendu, on réessaie avec un autre type. Extraire-et-prier. Ça marche en démo. En production, ça coûte cher : on mesurait environ 1,76 appel par document, l'appel initial, plus les reprises sur tous les cas où le pari « carte grise » était faux.

Pire, chaque appel d'extraction est un gros appel : un prompt long, multimodal, qui demande au modèle de faire deux choses à la fois, deviner le type et extraire les champs. On payait le prix fort pour une tâche d'extraction même quand le document n'était finalement pas celui qu'on croyait. Le coût n'était pas le seul problème : mélanger « identifier » et « extraire » dans un même prompt dégrade la précision des deux.

Phase 1, un classifieur jetable et quasi gratuit

La bascule a été de séparer les deux décisions. Avant toute extraction, un premier appel ne répond qu'à une question : « c'est quoi, ce document ? » Le modèle utilisé est claude-haiku-4-5, le plus rapide et le moins cher de la gamme. Le prompt lui interdit explicitement d'extraire quoi que ce soit. La sortie tient en deux champs.

// Phase 1, classifieur léger (sortie typée, ~80 tokens)
{
  "docTypeDetected": "carte_grise",
  "confidence": 0.97
}
// pas de champs, pas de VIN, pas de date, juste le type.
// modèle : claude-haiku-4-5 · ~80 tokens de sortie · ~200 ms

~80 tokens de sortie, ~200 ms de latence. À l'échelle d'un flux de production, c'est négligeable. Et comme on ne demande qu'une classification, on peut prendre le modèle le moins cher sans rien sacrifier, au contraire, on va voir qu'on y gagne.

💡
Le détail qui change l'économie : ce classifieur ne renvoie jamais de champ extrait. C'est ce qui le rend trivialement cacheable, parallélisable et débuggable. Quand il se trompe, il se trompe sur une étiquette, pas sur un VIN qu'on aurait propagé en aval.

La surprise : Haiku bat Sonnet sur les photos pourries

Voilà le résultat le plus contre-intuitif du projet. Sur les photos basse résolution prises au téléphone, exactement notre quotidien, Haiku classifie mieux que Sonnet. On s'attendait à l'inverse : le modèle plus gros « voit » mieux, non ? Pas pour cette tâche.

L'explication tient à ce pour quoi chaque modèle est optimisé. Haiku est taillé pour la vitesse et la décision rapide, exactement ce qu'est une classification : un coup d'œil, une étiquette. Sonnet est taillé pour l'extraction détaillée et le raisonnement, quand on lui montre une image floue et qu'on lui demande juste un type, sa tendance à scruter et à raisonner se retourne contre lui sur un signal dégradé. La leçon est simple et générale : utiliser le modèle le moins cher là où il est réellement meilleur, et le modèle fort là où ça compte.

Le bon modèle n'est pas le plus gros. C'est celui dont l'optimisation correspond à la tâche. Ici, le moins cher gagne sur la phase la plus volumineuse.

Phase 2, extraction typée, une fois le type connu

Maintenant qu'on sait que c'est une carte grise, on appelle un prompt d'extraction spécifique à ce type. Le prompt « carte_grise » n'est pas le prompt « cerfa_cession » : champs différents, contraintes différentes, exemples différents. Un prompt typé est plus court, plus précis, et n'a pas à gérer l'ambiguïté du « et si c'était autre chose ? », on l'a déjà tranchée en Phase 1.

Cette extraction passe par un dispatcher de providers. Le primaire est claude-opus, c'est là qu'on veut la précision maximale, sur le champ qui compte. En cas de 529, 503 ou 504 (surcharge ou indisponibilité), on retente une fois avec 1 seconde de backoff. Si ça persiste, on bascule sur un fallback auto-hébergé chez OVH, un modèle multimodal open-source (Mistral-Small-3.2-24B ou Qwen2.5-VL-72B), activable par feature flag par client.

// Phase 2, dispatcher d'extraction typée
async function extract(docType, image) {
  const prompt = TYPED_PROMPTS[docType];   // carte_grise ≠ cerfa_cession

  try {
    return await claudeOpus(prompt, image);
  } catch (e) {
    if ([529, 503, 504].includes(e.status)) {
      await sleep(1000);                    // backoff 1 s, une fois
      try { return await claudeOpus(prompt, image); }
      catch { /* on tombe sur le fallback */ }
    }
    // fallback souverain, auto-hébergé OVH
    // Mistral-Small-3.2-24B | Qwen2.5-VL-72B (multimodaux)
    return await ovhFallback(prompt, image);  // feature flag par client
  }
}

Ce fallback n'est pas qu'une roue de secours. C'est aussi une réponse de souveraineté et de résilience : certains clients veulent que leurs documents ne quittent pas une infra européenne maîtrisée, et un provider primaire surchargé ne doit jamais bloquer un flux de production. Le feature flag par client laisse choisir au cas par cas.

Phase 3, la boucle de validation sur le champ qui casse tout

Reste le nerf de la guerre : l'immatriculation. L'extraction la renvoie avec un score de confiance. Si ce score est inférieur à 85 %, on ne se contente pas de l'accepter ou de la jeter, on relance un petit appel à Haiku, en lui posant une question fermée, oui/non : « est-ce une plaque d'immatriculation française valide ? »

// Phase 3, validation post-OCR de la seule plaque
if (result.immatriculation.confidence < 0.85) {
  const ok = await haikuYesNo(
    `Est-ce une plaque française valide : "${plate}" ?`
  );
  result.immatriculation.confidence = ok ? 0.95   // confirmée → on remonte
                                          : 0.5;   // réfutée  → on plafonne
}

Si Haiku confirme, on remonte la confiance à 0,95. S'il réfute, on la plafonne à 0,5, ce qui la route vers une revue humaine plutôt que vers un rejet silencieux. On ne valide pas tout le document : on dépense un appel minuscule uniquement sur le seul champ dont l'erreur fait tout dérailler. C'est la définition même d'une dépense bien placée.

⚖️
Le compromis assumé : on ajoute un troisième appel, mais seulement quand la confiance est basse, et c'est l'appel le moins cher de la gamme. Le coût marginal est dérisoire face à ce qu'il évite : un dossier d'immatriculation rejeté, c'est des jours perdus et un concessionnaire furieux.

L'économie : moitié moins de tokens, plus de précision

Faisons le compte. L'approche naïve tournait à ~1,76 appel d'extraction par document, et chaque appel était lourd. Le nouveau pipeline, c'est un classifieur quasi gratuit + une extraction typée, soit ~50 % de tokens en moins sur le cas médian. La Phase 3 n'ajoute un appel que sur la fraction de documents à faible confiance, et c'est l'appel le plus cheap.

Le plus satisfaisant, c'est qu'on ne troque pas le coût contre la qualité, on gagne sur les deux. En séparant « identifier » et « extraire », chaque prompt fait une seule chose, donc la fait mieux. Le classifieur Haiku surpasse le gros modèle sur sa tâche. L'extraction typée n'a plus à deviner. Et la validation rattrape le seul champ qui pouvait tout faire échouer.

La tuyauterie qui rend ça viable

Deux optimisations discrètes mais décisives à l'échelle. D'abord, une déduplication MD5 au sein d'une session : si le même fichier est uploadé deux fois (ça arrive bien plus souvent qu'on ne croit), on réutilise le résultat au lieu de repayer tout le pipeline. Ensuite, un cache éphémère de 5 minutes sur le system prompt, les prompts typés sont stables, donc cacheables, ce qui réduit encore le coût d'entrée des appels rapprochés.

Dernier détail que j'aime beaucoup : chaque document trimballe une trace: string[], la liste des étapes de raisonnement du pipeline, propagée jusqu'au front. Sur un document limite, un opérateur peut copier cette trace et la coller dans un second modèle pour un sanity check indépendant. Pas de boîte noire : la décision est inspectable, et on garde un humain dans la boucle exactement là où le doute existe.

Ce que je retiens

Ce raisonnement, découper la décision, choisir le bon outil par étape, dépenser l'effort là où l'erreur coûte le plus, c'est exactement la mentalité que j'applique aux agents IA en production. La question n'est jamais « est-ce que le modèle sait lire ? » mais « combien ça coûte quand il a raison, et qu'est-ce qui se passe quand il a tort ? »

// gerer.ai

Des agents IA en production, souverains, qui ne coûtent pas une fortune ?

C'est exactement le terrain de Gérer.ai : des agents IA déployés sur votre infra, avec des modèles open-source auto-hébergés. Banque, santé, juridique, public, auto, pipelines pensés pour le coût, la précision et la souveraineté.

Découvrir Gérer.ai
L
Luc Del Beato

Senior Lead Engineer, ~20 ans de web. Do-er passionné de résolution de problèmes, de belle architecture et d'automatisation ; les agents IA, c'est ma direction. Mon parcours →