Erreurs, timeouts et réessais
Trois pannes, trois traitements
flowchart TB
E["Quelque chose échoue"] --> Q{"Où ?"}
Q -- "API RH répond 4xx" --> R1["Renvoyer {erreur, code_http} au modèle"]
Q -- "API RH muette" --> R2["Timeout httpx, puis {erreur} au modèle"]
Q -- "Modèle 429 / 503" --> R3["Réessai avec attente croissante"]
R1 --> M["Le modèle explique à l'utilisateur"]
R2 --> M
R3 --> G["generate_content relancé (4 essais max)"]
Le principe pour les outils : ne jamais lever d'exception vers la boucle. Une exception casse la conversation ; un dictionnaire {"erreur": ...} est un résultat comme un autre que le modèle sait reformuler (« vous n'avez pas accès à ce solde »).
Côté API métier : un seul point d'appel HTTP
Tous les outils passent par _appel, qui transforme chaque panne en dictionnaire :
# Timeout explicite : sans lui, un appel bloqué fige tout l'assistant.
http = httpx.Client(base_url=API_URL, timeout=httpx.Timeout(5.0, connect=2.0))
def _appel(utilisateur: str, methode: str, chemin: str, **kw) -> dict:
try:
r = http.request(methode, chemin, headers={"X-Utilisateur": utilisateur}, **kw)
except httpx.TimeoutException:
return {"erreur": "L'API RH n'a pas répondu à temps, réessayez plus tard."}
except httpx.HTTPError as e:
return {"erreur": f"API RH injoignable ({type(e).__name__})."}
if r.status_code >= 400:
try:
detail = r.json().get("detail")
except ValueError:
detail = r.text[:200]
return {"erreur": detail, "code_http": r.status_code}
return r.json()| Code | Signification dans l'API RH | Ce que le modèle doit en faire |
|---|---|---|
| 403 | l'utilisateur n'est ni l'employé ni son manager | expliquer le refus, ne pas réessayer |
| 404 | employé ou demande inconnu | proposer de chercher par nom |
| 409 | solde insuffisant, demande déjà annulée | donner le solde disponible |
| 422 | données invalides (dates inversées…) | reformuler la demande |
| timeout | l'API n'a pas répondu en 5 s | dire de réessayer plus tard |
Côté modèle : réessayer intelligemment
Les erreurs 429 (quota dépassé, trop de requêtes) et 503 (service surchargé) sont temporaires. On réessaie avec une attente qui double (backoff exponentiel). Les autres erreurs (400 : requête mal formée, 403 : clé invalide) ne guériront pas : on les remonte tout de suite.
def generer(client, contents, config, essais=4):
for i in range(essais):
try:
return client.models.generate_content(model=MODEL, contents=contents, config=config)
except errors.APIError as e:
if e.code not in (429, 500, 503) or i == essais - 1:
raise
time.sleep(2 ** i * 2) # 2 s, 4 s, 8 sL'API RH répond 403 à consulter_solde. Que doit renvoyer l'outil à la boucle ?
Une erreur métier est un résultat : le modèle l'explique. Inventer un solde à zéro serait un mensonge, et une exception casserait la conversation.
Quelles erreurs de l'API Gemini justifient un réessai automatique ?
429 (quota) et 503 (surcharge) sont temporaires. 400 (requête invalide) et 403 (clé refusée) se reproduiront à l'identique : réessayer ne fait que perdre du temps.
Exercice : tester le timeout sans réseau
Écrivez un test pytest qui vérifie qu'un timeout de l'API devient un message lisible, sans lancer l'API ni attendre 5 secondes.
Voir la solution
httpx.MockTransport remplace le réseau par une fonction ; on lui fait lever un ReadTimeout :
def test_timeout_devient_message(monkeypatch):
def lent(request):
raise httpx.ReadTimeout("trop lent", request=request)
monkeypatch.setattr(outils_rh, "http", httpx.Client(base_url="http://x",
transport=httpx.MockTransport(lent)))
assert "pas répondu à temps" in outils_rh.consulter_solde("E001", "E001")["erreur"]Le test s'exécute en quelques millisecondes et ne dépend d'aucun service.
Résultat obtenu
python e2e.py, scénario 5, exécuté le 2026-10-15 avec gemini-3.5-flash. E004 (comptabilité) demande le solde d'une employée du marketing : l'API répond 403, le modèle l'explique.
[E004] Quel est le solde de Lucie Moreau ? (mode AUTO)
[tour 1] 1 appel(s) : chercher_employe({"nom": "Lucie Moreau"})
[tour 2] 1 appel(s) : consulter_solde({"employe_id": "E003"})
→ L'accès au solde de Lucie Moreau vous est refusé car vous n'êtes ni l'employée concernée ni son manager.Et la ligne correspondante du journal d'audit :
{"ts": "2026-10-15T14:35:26+00:00", "utilisateur": "E004", "outil": "consulter_solde", "args": {"employe_id": "E003"}, "statut": "erreur", "duree_ms": 3, "resultat": {"erreur": "Accès refusé : vous n'êtes ni l'employé ni son manager", "code_http": 403}}Je sais faire
- Fixer un timeout explicite sur un client HTTP
- Transformer toute panne d'outil en dictionnaire
{"erreur": ...} - Réessayer les 429/503 avec un backoff exponentiel, et seulement eux
- Expliquer pourquoi on ne réessaie pas automatiquement une écriture