Aller au contenu principal

Anti-patterns de l'API

Les erreurs qui coûtent cher en production

Après avoir vu les bonnes pratiques, examinons les anti-patterns les plus fréquents dans l’utilisation de l’API Chat Completions. Ces erreurs sont souvent invisibles en développement mais explosent en production : coûts imprévus, réponses incohérentes, latences élevées, ou pire — des données sensibles qui fuient.

Anti-pattern 1 : ignorer finish_reason

L’erreur la plus courante. Si vous ne vérifiez pas finish_reason, vous risquez de traiter des réponses tronquées comme si elles étaient complètes :

# MAUVAIS — pas de vérification
response = client.chat.complete(
    model="mistral-large-latest",
    messages=[{"role": "user", "content": "Rédigez un contrat complet."}],
    max_tokens=100  # Trop peu pour un contrat !
)
contrat = response.choices[0].message.content  # Potentiellement tronqué !

# BON — vérification systématique
response = client.chat.complete(
    model="mistral-large-latest",
    messages=[{"role": "user", "content": "Rédigez un contrat complet."}],
    max_tokens=4000
)

if response.choices[0].finish_reason == "length":
    print("ATTENTION : réponse tronquée, augmentez max_tokens")
else:
    contrat = response.choices[0].message.content

Anti-pattern 2 : historique de conversation illimité

Renvoyer toute la conversation sans limite consomme de plus en plus de tokens et finit par dépasser la fenêtre de contexte :

# MAUVAIS — l'historique grandit indéfiniment
conversation = []
while True:
    user_input = input("Vous : ")
    conversation.append({"role": "user", "content": user_input})
    response = client.chat.complete(
        model="mistral-large-latest",
        messages=conversation  # 100 tours = des milliers de tokens !
    )
    reply = response.choices[0].message.content
    conversation.append({"role": "assistant", "content": reply})

# BON — fenêtre glissante avec résumé
MAX_MESSAGES = 20

def trim_conversation(messages: list, max_messages: int = MAX_MESSAGES) -> list:
    """Garde le system prompt + les N derniers messages."""
    system = [m for m in messages if m["role"] == "system"]
    history = [m for m in messages if m["role"] != "system"]

    if len(history) > max_messages:
        history = history[-max_messages:]

    return system + history

Anti-pattern 3 : ne pas gérer les erreurs réseau

# MAUVAIS — aucune gestion d'erreur
response = client.chat.complete(
    model="mistral-large-latest",
    messages=[{"role": "user", "content": "Bonjour"}]
)
# Crash si timeout, 429, 500...

# BON — retry avec backoff
from mistralai.exceptions import MistralAPIException
import time

def safe_complete(client, messages, max_retries=3, **kwargs):
    """Appel API avec retry et backoff exponentiel."""
    for attempt in range(max_retries):
        try:
            return client.chat.complete(messages=messages, **kwargs)
        except MistralAPIException as e:
            if e.status_code == 429:
                wait = 2 ** attempt
                print(f"Rate limit atteint, attente {wait}s...")
                time.sleep(wait)
            elif e.status_code >= 500:
                time.sleep(1)
            else:
                raise  # Erreur client (400, 401) — ne pas retenter
    raise Exception("Échec après 3 tentatives")

Anti-pattern 4 : le langage subjectif dans les prompts

Les modèles interprètent mal les termes vagues :

# MAUVAIS — subjectif, non mesurable
"Écrivez un résumé court."           # Court = 1 phrase ? 1 paragraphe ?
"Donnez beaucoup d'exemples."        # Beaucoup = 3 ? 10 ? 50 ?
"Répondez de manière intéressante."  # Intéressant selon quels critères ?

# BON — critères objectifs
"Écrivez un résumé de 50 à 80 mots."
"Donnez exactement 5 exemples, un par ligne."
"Commencez chaque réponse par une statistique chiffrée liée au sujet."

Anti-pattern 5 : laisser le modèle compter

Les LLMs sont notoirement mauvais en comptage de mots, caractères et éléments :

# MAUVAIS — le modèle ne sait pas compter précisément
"Écrivez exactement 280 caractères pour un tweet."

# BON — compter côté application
response = client.chat.complete(
    model="mistral-large-latest",
    messages=[{"role": "user", "content": "Rédigez un tweet sur l'IA en France."}],
    max_tokens=100
)

tweet = response.choices[0].message.content
if len(tweet) > 280:
    tweet = tweet[:277] + "..."

print(f"Tweet ({len(tweet)} chars) : {tweet}")

Anti-pattern 6 : pas de logging en production

# MAUVAIS — aucune trace
response = client.chat.complete(
    model="mistral-large-latest",
    messages=messages
)
return response.choices[0].message.content

# BON — logging structuré
import logging
import json

logger = logging.getLogger("mistral_api")

def logged_complete(client, messages, **kwargs):
    """Appel API avec logging structuré."""
    response = client.chat.complete(messages=messages, **kwargs)

    logger.info(json.dumps({
        "request_id": response.id,
        "model": response.model,
        "prompt_tokens": response.usage.prompt_tokens,
        "completion_tokens": response.usage.completion_tokens,
        "finish_reason": response.choices[0].finish_reason
    }))

    return response

Anti-pattern 7 : échelles numériques dans les prompts

# MAUVAIS — les modèles interprètent mal les échelles numériques
"Évaluez la qualité de ce texte de 1 à 5."

# BON — échelle verbale avec définitions
"""Évaluez la qualité de ce texte selon l'échelle suivante :
- EXCELLENT : clair, bien structuré, sans erreur
- BON : globalement correct, améliorations mineures possibles
- MOYEN : compréhensible mais manque de structure ou contient des erreurs
- FAIBLE : difficile à comprendre, erreurs significatives
- TRÈS FAIBLE : incompréhensible ou hors sujet

Répondez uniquement par le label en majuscules."""

Checklist de debugging

Quand une réponse ne correspond pas à vos attentes, vérifiez dans l’ordre :

def debug_response(response):
    """Checklist de debugging pour les réponses inattendues."""
    choice = response.choices[0]

    # 1. La réponse est-elle complète ?
    if choice.finish_reason == "length":
        print("PROBLÈME : Réponse tronquée — augmentez max_tokens")

    # 2. Le bon modèle a-t-il été utilisé ?
    print(f"Modèle : {response.model}")

    # 3. Combien de tokens consommés ?
    print(f"Tokens entrée : {response.usage.prompt_tokens}")
    print(f"Tokens sortie : {response.usage.completion_tokens}")

    # 4. La réponse est-elle vide ?
    if not choice.message.content or not choice.message.content.strip():
        print("PROBLÈME : Réponse vide")

    # 5. Ratio entrée/sortie suspect ?
    ratio = response.usage.completion_tokens / max(response.usage.prompt_tokens, 1)
    if ratio < 0.01:
        print(f"ATTENTION : Ratio sortie/entrée très bas ({ratio:.4f})")

Points clés à retenir

  • Vérifiez toujours finish_reason pour détecter les troncatures
  • Limitez l’historique de conversation avec une fenêtre glissante
  • Implémentez un retry avec backoff exponentiel pour les erreurs réseau
  • Utilisez des critères objectifs et mesurables dans vos prompts
  • Ne laissez jamais le modèle compter — validez côté application
  • Loguez chaque appel API en production pour le debugging et le suivi des coûts