Bonnes pratiques de production
Mis à jour le 29 juillet 2026
De la preuve de concept au déploiement
Vous maîtrisez maintenant tous les paramètres de l’API Chat Completions. Reste le passage le plus délicat : celui qui sépare un notebook qui fonctionne d’un service que d’autres personnes utilisent sans vous. Cette dernière leçon consolide l’ensemble du cours en une checklist de production actionnable, puis ouvre la voie vers les formations suivantes.
Authentification et versionnement du modèle
Deux décisions se prennent avant la première ligne de logique métier. La première concerne la clé API : elle vit dans une variable d’environnement, jamais dans le code, et une assertion au démarrage vaut mieux qu’un None qui se propage jusqu’à un message d’erreur incompréhensible trois couches plus bas. Prévoyez aussi la rotation — créez la nouvelle clé, déployez-la, puis seulement révoquez l’ancienne, dans cet ordre si vous tenez à ne pas couper le service.
import os
# TOUJOURS : clé API en variable d'environnement
api_key = os.getenv("MISTRAL_API_KEY")
assert api_key, "MISTRAL_API_KEY non définie"
# JAMAIS : clé en dur dans le code
# api_key = "sk-abc123..." # INTERDIT en production
# TOUJOURS : rotation régulière des clés
# Créez une nouvelle clé, déployez-la, puis révoquez l'ancienne
La seconde décision porte sur l’identifiant du modèle. En développement, -latest vous donne les améliorations sans effort. En production, ce même alias signifie qu’un changement de version côté fournisseur peut modifier le comportement de votre application sans avertissement, un mardi matin, sans qu’aucun de vos commits n’en porte la trace. Fixez la version.
# DÉVELOPPEMENT — dernière version
model = "mistral-large-latest"
# PRODUCTION — version fixe pour la reproductibilité
model = "mistral-large-2512"
# Pourquoi ? Un changement de "latest" peut modifier le comportement
# de votre application sans avertissement.
Un client qui encaisse les incidents
Le cœur d’une intégration de production n’est pas l’appel API mais tout ce qui l’entoure. La classe ci-dessous rassemble trois responsabilités dans un seul point de passage : le retry avec backoff exponentiel sur les 429, une pause courte sur les erreurs serveur, la remontée immédiate des erreurs client qu’aucune tentative supplémentaire ne corrigera. Elle normalise aussi la réponse en un dictionnaire stable — contenu, finish_reason, usage, modèle, identifiant — de sorte que le reste de votre code ne dépende plus de la forme exacte de l’objet renvoyé par le SDK. Le logger.warning sur les troncatures vaut à lui seul l’encapsulation : c’est la seule façon de découvrir qu’un max_tokens est trop serré avant que les utilisateurs ne le signalent.
from mistralai import Mistral
from mistralai.models import SDKError
import time
import logging
logger = logging.getLogger(__name__)
class MistralClient:
"""Client de production avec retry, timeout et logging."""
def __init__(self, api_key: str, max_retries: int = 3):
self.client = Mistral(api_key=api_key)
self.max_retries = max_retries
def complete(self, messages: list, **kwargs) -> dict:
for attempt in range(self.max_retries):
try:
response = self.client.chat.complete(
messages=messages,
**kwargs
)
result = {
"content": response.choices[0].message.content,
"finish_reason": response.choices[0].finish_reason,
"usage": {
"prompt": response.usage.prompt_tokens,
"completion": response.usage.completion_tokens,
"total": response.usage.total_tokens,
},
"model": response.model,
"id": response.id,
}
# Alerter si réponse tronquée
if result["finish_reason"] == "length":
logger.warning(f"Réponse tronquée — req_id={response.id}")
return result
except SDKError as e:
if e.status_code == 429:
wait = 2 ** attempt
logger.warning(f"Rate limit, retry {attempt+1}/{self.max_retries} dans {wait}s")
time.sleep(wait)
elif e.status_code >= 500:
logger.error(f"Erreur serveur {e.status_code}, retry {attempt+1}")
time.sleep(1)
else:
logger.error(f"Erreur client {e.status_code}: {e.message}")
raise
raise Exception(f"Échec après {self.max_retries} tentatives")
Suivre les coûts pendant qu’ils se créent
Découvrir sa consommation sur la facture mensuelle est un luxe que peu d’équipes peuvent se permettre. Le tracker ci-dessous écrit une ligne JSONL par appel — horodatage, modèle, tokens d’entrée et de sortie, finish_reason, identifiant de requête — et maintient un cumul de session consultable à tout moment. Le format JSONL n’est pas un détail : il s’agrège avec n’importe quel outil d’analyse sans étape de parsing, et la présence de l’identifiant de requête permet de relier une anomalie de coût à un appel précis.
import json
from datetime import datetime, timezone
class CostTracker:
"""Suivi des coûts API en temps réel."""
def __init__(self, log_path: str = "api_costs.jsonl"):
self.log_path = log_path
self.session_tokens = {"prompt": 0, "completion": 0}
def log(self, response_data: dict):
usage = response_data["usage"]
self.session_tokens["prompt"] += usage["prompt"]
self.session_tokens["completion"] += usage["completion"]
entry = {
"ts": datetime.now(timezone.utc).isoformat(),
"model": response_data["model"],
"prompt_tokens": usage["prompt"],
"completion_tokens": usage["completion"],
"finish_reason": response_data["finish_reason"],
"id": response_data["id"],
}
with open(self.log_path, "a") as f:
f.write(json.dumps(entry) + "\n")
def report(self) -> str:
total = self.session_tokens["prompt"] + self.session_tokens["completion"]
return (
f"Session — Prompt: {self.session_tokens['prompt']:,} | "
f"Completion: {self.session_tokens['completion']:,} | "
f"Total: {total:,} tokens"
)
Valider aux deux extrémités
Une application robuste ne fait confiance ni à ce qui entre ni à ce qui sort. En entrée, on refuse le message vide, on plafonne la longueur à 10 000 caractères pour éviter qu’un copier-coller malheureux ne parte en facture, et on nettoie les caractères de contrôle qui perturbent la tokenisation. En sortie, on ne rejette rien mais on lève des avertissements : réponse potentiellement tronquée, réponse anormalement courte sous cinq caractères, réponse anormalement longue au-delà de 50 000. Ces trois signaux détectent la grande majorité des dysfonctionnements silencieux.
def validate_input(user_message: str) -> str:
"""Valide et nettoie l'entrée utilisateur."""
if not user_message or not user_message.strip():
raise ValueError("Message vide")
if len(user_message) > 10_000:
raise ValueError("Message trop long (max 10 000 caractères)")
# Nettoyer les caractères de contrôle
cleaned = "".join(c for c in user_message if c.isprintable() or c in "\n\t")
return cleaned.strip()
def validate_output(content: str, finish_reason: str) -> dict:
"""Valide la sortie de l'API."""
warnings = []
if finish_reason == "length":
warnings.append("Réponse potentiellement tronquée")
if not content or len(content.strip()) < 5:
warnings.append("Réponse anormalement courte")
if len(content) > 50_000:
warnings.append("Réponse anormalement longue")
return {"content": content, "warnings": warnings, "valid": len(warnings) == 0}
Tenir la fenêtre de contexte
Reste la dérive lente des conversations longues. La fonction suivante estime le volume à raison d’un token pour quatre caractères en français — approximation grossière mais suffisante pour un garde-fou — et laisse passer tant qu’on reste sous 80 % de la limite. Au-delà, elle préserve le system prompt et retire les messages les plus anciens jusqu’à redescendre sous 70 %. Cet écart entre les deux seuils est délibéré : couper jusqu’au seuil de déclenchement ferait retomber la fonction dans le rouge au message suivant.
def manage_context(messages: list, max_tokens: int = 100_000) -> list:
"""Garde les messages dans la limite de tokens du modèle."""
# Estimation : 1 token ~ 4 caractères en français
total_chars = sum(len(m["content"]) for m in messages)
estimated_tokens = total_chars // 4
if estimated_tokens < max_tokens * 0.8:
return messages # Tout rentre
# Garder system + premiers et derniers messages
system = [m for m in messages if m["role"] == "system"]
history = [m for m in messages if m["role"] != "system"]
# Couper les messages les plus anciens
while estimated_tokens > max_tokens * 0.7 and len(history) > 2:
removed = history.pop(0)
estimated_tokens -= len(removed["content"]) // 4
return system + history
Ce que vous emportez de ce cours
Le parcours a suivi quatre étapes. Les fondamentaux d’abord : l’endpoint, les rôles, les paramètres de base et la structure de la réponse. Le streaming ensuite, avec le protocole SSE, son implémentation en Python, les stop sequences et la sécurisation par safe_prompt et guardrails. Le prompting a occupé le troisième temps — system prompts de production, few-shot, structuration XML et Markdown, anti-patterns. Les paramètres avancés ont clos l’ensemble : température et Top P en détail, pénalités de présence et de fréquence, N completions, et cette checklist de production.
La configuration ci-dessous condense ces choix en un point de départ défendable pour une première mise en production : version fixe, plafond de tokens adapté, température basse pour rester factuel, top_p intouché puisque la température est ajustée, pénalités neutres tant qu’aucun besoin spécifique ne les justifie, et safe_prompt activé.
# Configuration type pour une application en production
PRODUCTION_CONFIG = {
"model": "mistral-large-2512", # Version fixe
"max_tokens": 2000, # Adapté au cas d'usage
"temperature": 0.3, # Cohérent et factuel
"top_p": 1.0, # Pas touché si temperature ajustée
"presence_penalty": 0.0, # Par défaut sauf besoin spécifique
"frequency_penalty": 0.0, # Par défaut sauf besoin spécifique
"safe_prompt": True, # Sécurité activée
}
Pour aller plus loin avec l’API Mistral, quatre formations complémentaires prennent la suite sur Corsen Academy :
- Embeddings et RAG — Recherche sémantique avec
mistral-embedet bases vectorielles - Function calling — Connecter le modèle à vos outils et APIs externes
- Fine-tuning — Adapter un modèle à votre domaine spécifique
- Production et évaluation — Métriques, évals automatisées, optimisation des coûts
Points clés à retenir
- Utilisez des modèles versionnés en production, jamais
-latest - Implémentez retry + backoff + logging dès le premier déploiement
- Validez les entrées ET les sorties de chaque appel API
- Suivez les coûts en temps réel avec un tracker de tokens
- Gérez la fenêtre de contexte pour les conversations longues
- Activez
safe_promptcomme filet de sécurité minimal - Testez vos prompts avec des cas limites avant chaque mise en production
Testez vos connaissances
L’endpoint chat/completions n’a plus de secret ? Vérification.
1. Quels rôles structurent la liste de messages ?
Réponse : system (le cadre), user (les demandes), assistant (les réponses du modèle) — l’historique complet repart à chaque appel, l’API étant sans état.
2. Que trouve-t-on dans le champ usage de la réponse ?
Réponse : Les tokens consommés en entrée et en sortie — la matière de la facturation et la métrique à surveiller pour le dimensionnement et les coûts.
3. Comment fonctionne le streaming SSE ?
Réponse : La réponse arrive en événements Server-Sent Events, fragment par fragment : on affiche au fil de l’eau et on concatène pour obtenir le message final.
4. À quoi servent les stop sequences ?
Réponse : À couper la génération dès qu’une chaîne donnée apparaît : contrôler la longueur, s’arrêter à un délimiteur, garantir des formats propres.
5. Température, Top P, pénalités : quel usage en production ?
Réponse : Température/Top P règlent le déterminisme selon la tâche ; les pénalités presence/frequency limitent les répétitions — des réglages à fixer par cas d’usage et à documenter avec vos prompts.
Rôles, usage, streaming, arrêts, réglages : les fondamentaux tiennent — les bonnes pratiques de production ci-dessus les mettent au propre.