Limites et Optimisation de la Vision API
Mis à jour le 29 juillet 2026
Connaître les limites pour mieux les contourner
Les dix leçons précédentes ont montré ce que la Vision API sait faire. Celle-ci montre ce qu’elle fait mal, et ce n’est pas un exercice de modestie : la différence entre un prototype qui impressionne en démonstration et une application qui tient en production se joue exactement là. Vous découvrirez ici les contraintes techniques, les pièges courants — au premier rang desquels les hallucinations visuelles — et les stratégies d’optimisation du coût et de la latence.
Limites techniques
Les contraintes de format ont déjà été croisées et méritent d’être rassemblées. Chaque image est plafonnée à environ 20 Mo et sera de toute façon redimensionnée en interne, généralement à 768x768 ou équivalent : une image 4K n’améliore rien. Les formats acceptés sont JPEG, PNG, GIF (première frame uniquement) et WebP. Le PDF, lui, n’est pas supporté directement par la Vision API — c’est Document AI qu’il faut solliciter, comme évoqué en leçon 8.
La contrainte la plus structurante reste la consommation de tokens. Chaque image en absorbe un nombre significatif, et cette fonction traduit l’estimation en euros plutôt qu’en abstraction :
# Estimation du coût d'un appel vision
def estimer_cout(nb_images=1, tokens_texte=100, tokens_reponse=500,
tokens_par_image=1000, prix_input=0.10, prix_output=0.30):
"""Estime le coût en dollars pour un appel vision (Small 3.2)."""
input_tokens = (nb_images * tokens_par_image) + tokens_texte
output_tokens = tokens_reponse
cout_input = (input_tokens / 1_000_000) * prix_input
cout_output = (output_tokens / 1_000_000) * prix_output
cout_total = cout_input + cout_output
print(f"Tokens input : {input_tokens} ({cout_input:.4f} $)")
print(f"Tokens output : {output_tokens} ({cout_output:.4f} $)")
print(f"Coût total : {cout_total:.4f} $")
return cout_total
# 1 image avec Small 3.2
estimer_cout(nb_images=1)
# 5 images avec Large 3
estimer_cout(nb_images=5, prix_input=2.00, prix_output=6.00)
Comparez les deux appels de fin de script : le second coûte plusieurs dizaines de fois le premier. Multipliez par votre volume quotidien avant d’arbitrer. À cela s’ajoute la fenêtre de contexte, que le total images + prompt + réponse ne doit pas dépasser — un plafond que plusieurs images atteignent bien plus vite qu’un long texte.
Les hallucinations visuelles
Le modèle peut « inventer » des détails absents de l’image. C’est le problème le plus critique en production, parce qu’il est silencieux : la réponse est bien formée, plausible, et fausse.
Quatre formes reviennent. Le texte inventé, quand le modèle lit un mot qui n’existe pas dans l’image. Les chiffres approximatifs, les valeurs lues sur un graphique pouvant s’écarter de 5 à 15 % du réel. Les détails ajoutés, où le modèle complète ce qu’il ne voit pas clairement par des informations vraisemblables — un numéro de SIRET flou devient un SIRET parfaitement formé mais faux. La confusion d’éléments, enfin, lorsque deux objets proches sont fondus en un seul.
Trois parades existent, à combiner selon la criticité. La première consiste à obliger le modèle à qualifier sa propre certitude ; la deuxième à lui faire relire l’image après transcription ; la troisième à confronter deux modèles sur la même question et à ne retenir que ce sur quoi ils convergent.
# 1. Demander un niveau de confiance
prompt_confiance = """Analysez cette image. Pour chaque information extraite, indiquez votre niveau de confiance :
- [CERTAIN] : clairement visible
- [PROBABLE] : partiellement visible ou déduit
- [INCERTAIN] : peu lisible, estimation
Si un élément est illisible, indiquez [ILLISIBLE] plutôt que de deviner."""
# 2. Demander une vérification croisée
prompt_verification = """Lisez le texte de ce document. Après la transcription, relisez l'image et vérifiez chaque chiffre et chaque nom propre. Corrigez les erreurs détectées."""
# 3. Double lecture avec deux modèles
def double_verification(image_uri, question):
"""Envoie la même image à deux modèles et compare les résultats."""
resultats = {}
for modele in ["mistral-small-latest", "mistral-large-latest"]:
response = client.chat.complete(
model=modele,
messages=[{
"role": "user",
"content": [
{"type": "text", "text": question},
{"type": "image_url", "image_url": image_uri}
]
}]
)
resultats[modele] = response.choices[0].message.content
return resultats
La double lecture double le coût : réservez-la aux montants, aux identifiants et à tout ce qui déclenche une action irréversible.
Optimiser les coûts
L’optimisation la plus rentable a déjà été rencontrée en leçon 5, et c’est la plus simple : réduire la taille des images avant l’envoi. Une photo de 6 Mo redimensionnée à 1024 pixels ne perd rien pour l’analyse et allège tout le reste de la chaîne.
from PIL import Image
from io import BytesIO
import base64
def optimiser_image(chemin, taille_max=1024, qualite=80):
"""Redimensionne et compresse une image pour l'API Vision."""
img = Image.open(chemin)
taille_originale = img.size
# Redimensionner
if max(img.size) > taille_max:
img.thumbnail((taille_max, taille_max))
# Compresser en JPEG
buffer = BytesIO()
img.convert("RGB").save(buffer, format="JPEG", quality=qualite)
taille_fichier = buffer.tell()
b64 = base64.standard_b64encode(buffer.getvalue()).decode("utf-8")
print(f"Original : {taille_originale} | Optimisé : {img.size} | Taille : {taille_fichier/1024:.0f} Ko")
return f"data:image/jpeg;base64,{b64}"
Vient ensuite le routage par complexité : toutes les tâches ne méritent pas le modèle le plus cher. Une fonction de trois lignes suffit à trancher, à condition d’avoir classé vos tâches en amont.
def choisir_modele(tache):
"""Sélectionne le modèle optimal selon la tâche."""
taches_simples = ["classification", "ocr_simple", "description", "triage"]
taches_complexes = ["extraction_json", "comparaison", "analyse_graphique", "raisonnement"]
if tache in taches_simples:
return "mistral-small-latest" # ~0.10 $/M tokens
else:
return "mistral-large-latest" # ~2.00 $/M tokens
Le troisième levier est le plus souvent oublié : ne payez pas deux fois la même analyse. En production, la même image revient plus souvent qu’on ne l’imagine — relance d’un traitement, doublon d’envoi, nouvelle tentative après incident. Un cache disque indexé sur le hash du fichier et du prompt élimine ces appels redondants.
import hashlib
import json
from pathlib import Path
CACHE_DIR = Path("cache_vision/")
CACHE_DIR.mkdir(exist_ok=True)
def analyser_avec_cache(image_path, prompt, modele="mistral-small-latest"):
"""Analyse une image avec cache disque."""
# Générer une clé de cache
with open(image_path, "rb") as f:
image_hash = hashlib.md5(f.read()).hexdigest()
cache_key = hashlib.md5(f"{image_hash}{prompt}{modele}".encode()).hexdigest()
cache_file = CACHE_DIR / f"{cache_key}.json"
# Vérifier le cache
if cache_file.exists():
return json.loads(cache_file.read_text())
# Appel API
response = client.chat.complete(
model=modele,
messages=[{
"role": "user",
"content": [
{"type": "text", "text": prompt},
{"type": "image_url", "image_url": encoder_image(image_path)}
]
}]
)
resultat = response.choices[0].message.content
# Sauvegarder en cache
cache_file.write_text(json.dumps(resultat, ensure_ascii=False))
return resultat
La clé intègre le prompt et le modèle, et non la seule image : changer de question sur la même photo doit bien relancer un appel.
Optimiser la latence
Pour une application interactive, quatre réglages agissent sur le temps de réponse ressenti. Redimensionnez les images côté client, à 1024 pixels maximum, ce qui raccourcit l’upload. Choisissez Small ou Ministral, dont la génération est nettement plus rapide. Passez les images par URL quand elles sont déjà hébergées, pour éviter l’envoi d’un corps de requête volumineux en Base64. Activez enfin le streaming, qui ne réduit pas le temps total mais affiche la réponse à mesure qu’elle se construit :
# Streaming pour une réponse progressive
stream = client.chat.stream(
model="mistral-small-latest",
messages=[{
"role": "user",
"content": [
{"type": "text", "text": "Décrivez cette image."},
{"type": "image_url", "image_url": url_image}
]
}]
)
for chunk in stream:
if chunk.data.choices[0].delta.content:
print(chunk.data.choices[0].delta.content, end="", flush=True)
C’est chat.stream qui remplace chat.complete ; le reste du message est rigoureusement identique.
Checklist de mise en production
Avant de déployer une application Vision en production, vérifiez chacun de ces points :
- Redimensionnement automatique des images (max 1024px)
- Validation du format et de la taille avant envoi
- Gestion des erreurs API (retry, timeout, fallback)
- Cache des résultats pour les images identiques
- Routage intelligent (Small pour le simple, Large pour le complexe)
- Monitoring des coûts et de la latence
- Tests avec des images de mauvaise qualité (flou, basse résolution, rotation)
- Stratégie anti-hallucination (niveaux de confiance, double vérification)
L’avant-dernier point est celui qu’on saute le plus volontiers, et celui qui fait le plus de dégâts : constituez un jeu d’images délibérément mauvaises et faites-le passer avant chaque mise en production.
Points clés à retenir
- Les images sont redimensionnées en interne — inutile d’envoyer de la très haute résolution
- Les hallucinations visuelles sont le risque principal — demandez des niveaux de confiance
- Le redimensionnement côté client est l’optimisation la plus rentable
- Le routage par complexité (Small vs Large) réduit les coûts de 90 % sur les tâches simples
- Le cache disque évite les appels API redondants sur les mêmes images