Gestion des Erreurs
Mis à jour le 29 juillet 2026
Les erreurs sont inévitables
En production, les fonctions échouent. L’API externe est indisponible, l’identifiant n’existe pas, les paramètres sont invalides, le timeout est dépassé. La question n’est pas « est-ce que ça va échouer ? » mais « comment gérer l’échec proprement ? ». Un tracker de paiements qui plante parce qu’un utilisateur a tapé T9999 au lieu de T1001 n’est pas un système de production, c’est une démonstration.
La bonne nouvelle est que vous disposez ici d’une ressource dont les architectures classiques sont privées : le modèle Mistral est capable de comprendre les messages d’erreur que vous lui renvoyez et d’adapter sa réponse en conséquence. Là où un back-end traditionnel doit prévoir un message d’interface pour chaque code d’erreur, vous pouvez laisser le modèle traduire l’échec en une phrase utile pour l’utilisateur. Encore faut-il lui donner l’information.
Ne jamais cacher une erreur au modèle
C’est la règle d’or de cette leçon. Une erreur se renvoie dans le message tool exactement comme se renvoie un résultat normal, sous forme de JSON structuré. Le réflexe inverse — attraper l’exception, renvoyer une chaîne vide ou un null — laisse le modèle croire que la fonction a réussi et qu’elle n’a simplement rien trouvé à dire, ce qui produit les réponses les plus déroutantes qui soient.
def retrieve_payment_status(df, transaction_id: str) -> str:
"""Récupère le statut avec gestion d'erreurs complète."""
try:
if transaction_id not in df["transaction_id"].values:
return json.dumps({
"error": "transaction_not_found",
"message": f"La transaction {transaction_id} n'existe pas dans la base."
})
status = df[df["transaction_id"] == transaction_id]["payment_status"].values[0]
return json.dumps({"status": status, "transaction_id": transaction_id})
except Exception as e:
return json.dumps({
"error": "internal_error",
"message": f"Erreur lors de la récupération : {str(e)}"
})
Deux champs suffisent : un code machine dans error, une explication lisible dans message. Le code permet à votre monitoring de compter les occurrences, l’explication permet au modèle de formuler une réponse utile. Voyez le résultat sur une transaction inexistante.
# L'utilisateur demande une transaction qui n'existe pas
messages = [{"role": "user", "content": "Statut du paiement T9999 ?"}]
# Le modèle appelle retrieve_payment_status(transaction_id="T9999")
# Votre fonction retourne : {"error": "transaction_not_found", ...}
# Le modèle formule :
# → "Je n'ai pas trouvé de transaction avec l'identifiant T9999.
# Pourriez-vous vérifier le numéro et réessayer ?"
Personne n’a écrit cette phrase. Elle découle du code d’erreur, et elle propose même une action de correction à l’utilisateur.
Un exécuteur qui ne laisse rien passer
Protéger une fonction ne suffit pas : la couche qui dispatche les tool_calls a ses propres modes de défaillance, indépendants de votre code métier. Le modèle peut halluciner un nom de fonction qui n’existe pas dans votre dictionnaire. Il peut produire un JSON d’arguments mal formé, cas rare mais réel sur les schémas complexes. Il peut aussi fournir un JSON valide dont les clés ne correspondent pas à la signature Python, ce qui provoque un TypeError au moment du **function_params. Chacun de ces trois cas mérite son propre code d’erreur, parce que chacun appelle une correction différente de votre part.
def execute_tool_call(tool_call, available_functions):
"""Exécute un tool_call avec gestion d'erreurs complète."""
function_name = tool_call.function.name
# Vérifier que la fonction existe
if function_name not in available_functions:
return {
"role": "tool",
"name": function_name,
"content": json.dumps({
"error": "function_not_found",
"message": f"La fonction {function_name} n'est pas disponible."
}),
"tool_call_id": tool_call.id
}
# Parser les arguments
try:
function_params = json.loads(tool_call.function.arguments)
except json.JSONDecodeError as e:
return {
"role": "tool",
"name": function_name,
"content": json.dumps({
"error": "invalid_arguments",
"message": f"Arguments JSON invalides : {str(e)}"
}),
"tool_call_id": tool_call.id
}
# Exécuter la fonction
try:
result = available_functions[function_name](**function_params)
return {
"role": "tool",
"name": function_name,
"content": result,
"tool_call_id": tool_call.id
}
except TypeError as e:
return {
"role": "tool",
"name": function_name,
"content": json.dumps({
"error": "invalid_params",
"message": f"Paramètres incompatibles : {str(e)}"
}),
"tool_call_id": tool_call.id
}
except Exception as e:
return {
"role": "tool",
"name": function_name,
"content": json.dumps({
"error": "execution_error",
"message": f"Erreur d'exécution : {str(e)}"
}),
"tool_call_id": tool_call.id
}
Observez que toutes les branches, y compris les branches d’échec, retournent un message tool complet avec son tool_call_id. C’est indispensable : l’API refuse un historique où un tool_call reste sans réponse, et un échec silencieux vous vaudrait une erreur de validation au tour suivant, bien loin de sa cause réelle.
Distinguer l’échec passager de l’échec définitif
Toutes les erreurs ne se valent pas. Une transaction introuvable le restera à la seconde tentative, et réessayer ne fait que gaspiller du temps. Un timeout réseau ou un service momentanément saturé, en revanche, se résolvent souvent d’eux-mêmes. Pour cette seconde catégorie, un retry avec attente exponentielle — une seconde, puis deux, puis quatre — laisse au service le temps de se rétablir sans le marteler de requêtes. Après épuisement des tentatives, on retombe sur le comportement habituel : un JSON d’erreur destiné au modèle.
import time
def execute_with_retry(func, params, max_retries=3, base_delay=1.0):
"""Exécute une fonction avec retry exponentiel."""
for attempt in range(max_retries):
try:
result = func(**params)
return result
except ConnectionError as e:
if attempt < max_retries - 1:
delay = base_delay * (2 ** attempt)
time.sleep(delay)
else:
return json.dumps({
"error": "service_unavailable",
"message": f"Service indisponible après {max_retries} tentatives.",
"details": str(e)
})
L’API Mistral peut elle aussi échouer
Tout ce qui précède concerne vos fonctions. L’appel à l’API Mistral elle-même constitue un point de défaillance distinct, qui se gère séparément : ici, il n’y a pas de modèle à qui expliquer le problème, puisque c’est justement le modèle qui est injoignable. Une HTTPValidationError signale généralement une erreur dans la structure de vos messages ou de vos tools, à corriger côté code ; les autres exceptions relèvent du réseau ou du service, et se traitent avec la même logique de retry que plus haut.
from mistralai import Mistral
from mistralai.models import HTTPValidationError
def safe_chat_complete(client, model, messages, tools):
"""Appel API Mistral avec gestion d'erreurs."""
try:
return client.chat.complete(
model=model,
messages=messages,
tools=tools
)
except HTTPValidationError as e:
print(f"Erreur de validation : {e}")
return None
except Exception as e:
print(f"Erreur API : {e}")
return None
Voir ce qui se passe réellement
Un système de function calling est difficile à déboguer après coup si vous ne savez pas quelle fonction a été appelée, avec quels arguments, et ce qu’elle a répondu. Le logging n’est pas un raffinement : c’est ce qui vous permettra de découvrir qu’une description ambiguë pousse le modèle à confondre deux fonctions, ou qu’un utilisateur sur cinq déclenche systématiquement un transaction_not_found. Loggez l’appel avant l’exécution, puis le verdict, en distinguant le niveau info du niveau error pour que vos alertes puissent filtrer.
import logging
logger = logging.getLogger("function_calling")
def execute_and_log(tool_call, available_functions):
"""Exécute et journalise le résultat."""
function_name = tool_call.function.name
function_params = json.loads(tool_call.function.arguments)
logger.info(f"Appel : {function_name}({function_params})")
result = execute_tool_call(tool_call, available_functions)
content = json.loads(result["content"])
if "error" in content:
logger.error(f"Échec {function_name}: {content['error']} - {content['message']}")
else:
logger.info(f"Succès {function_name}: {content}")
return result
Points clés à retenir
- Renvoyez toujours les erreurs au modèle dans le message
tool— il sait les interpréter - Utilisez des codes d’erreur structurés (
transaction_not_found,invalid_arguments, etc.) - Implémentez un retry avec backoff pour les erreurs transitoires
- Gérez séparément les erreurs de vos fonctions et les erreurs de l’API Mistral
- Loggez chaque appel et chaque erreur pour le monitoring en production