Aller au contenu principal

Anti-patterns de l'API

Mis à jour le 29 juillet 2026

Les erreurs qui coûtent cher en production

Après les bonnes pratiques, il faut regarder l’envers du décor. Les anti-patterns qui suivent sont les plus fréquents dans l’utilisation de l’API Chat Completions, et ils partagent une caractéristique désagréable : ils ne se voient pas en développement. Sur dix requêtes de test, tout fonctionne. C’est en production qu’ils explosent, sous forme de coûts imprévus, de réponses incohérentes, de latences élevées, ou pire — de données sensibles qui fuient.

Traiter une réponse tronquée comme une réponse complète

L’erreur la plus courante consiste à ignorer finish_reason. Vous demandez un contrat, vous récupérez un texte, vous l’enregistrez en base : sauf que max_tokens=100 a coupé la génération au milieu d’une clause, et personne ne s’en apercevra avant que le client ne signe. La vérification tient en trois lignes et doit devenir un réflexe.

# 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

Laisser l’historique de conversation grandir sans limite

Le deuxième piège se déclenche lentement. Une boucle de chat qui renvoie toute la conversation à chaque tour fonctionne parfaitement pendant les dix premiers échanges, coûte le double au vingtième, et finit par dépasser la fenêtre de contexte au centième — avec une facture qui a suivi la même courbe. La parade consiste à conserver le system prompt, non négociable, et à ne garder que les N derniers messages.

# 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

Supposer que le réseau répond toujours

Un appel API nu part du principe que rien ne va mal : ni timeout, ni 429, ni 500. En production, ces trois cas arrivent, et le troisième arrive au pire moment. Notez surtout la distinction dans le code : un 429 justifie une attente exponentielle, une erreur serveur une seconde de patience, mais une erreur client comme un 400 ou un 401 ne doit jamais être retentée — votre clé ne redeviendra pas valide à la troisième tentative, vous ne ferez que retarder le diagnostic.

# 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.models import SDKError
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 SDKError 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")

Demander au modèle ce qu’il ne sait pas faire

Trois anti-patterns se rejoignent ici. Le premier est le langage subjectif : « court », « beaucoup », « intéressant » ne désignent rien de vérifiable, et le modèle tranchera différemment d’un appel à l’autre. Remplacez chaque adjectif par un nombre ou un critère observable.

# 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."

Le deuxième est de laisser le modèle compter. Les LLMs sont notoirement mauvais en comptage de mots, de caractères et d’éléments : demander « exactement 280 caractères » produira un texte de 240 ou de 310. Le comptage relève de votre application, pas du modèle.

# 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}")

Le troisième relève de la même famille : les échelles numériques. « Évaluez de 1 à 5 » suppose que le modèle a une représentation stable de l’écart entre 3 et 4, ce qui n’est pas le cas. Une échelle verbale dont chaque échelon est défini par un critère donne des notes bien plus reproductibles.

# 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."""

Déployer sans laisser de traces

Le dernier anti-pattern est le plus discret : appeler l’API, renvoyer le contenu, et ne rien conserver. Le jour où un utilisateur signale une réponse aberrante, vous n’avez ni l’identifiant de requête, ni le modèle réellement servi, ni la consommation de tokens. Un log structuré par appel coûte quelques millisecondes et vous rend l’enquête possible.

# 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

Une routine de debugging

Quand une réponse ne correspond pas à vos attentes, la tentation est de réécrire le prompt. Commencez plutôt par cinq vérifications mécaniques, dans l’ordre : la réponse est-elle complète, le bon modèle a-t-il répondu, combien de tokens ont été consommés, la réponse est-elle vide, et le ratio sortie/entrée est-il vraisemblable. Un ratio inférieur à 1 % sur un prompt volumineux signale presque toujours que le modèle a refusé de traiter la demande plutôt qu’il ne l’a mal traitée.

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