Ce qui peut échouer
| Panne | Signal typique | Réponse adaptée |
|---|---|---|
| Surcharge, quota | HTTP 429, 529 (Claude « overloaded »), RESOURCE_EXHAUSTED sur Vertex AI | Retry avec backoff exponentiel + aléa |
| Erreur serveur transitoire | HTTP 500, 503 | Retry limité |
| Lenteur | Pas de réponse après N secondes | Délai maximal, puis repli |
| Sortie invalide | Schéma non respecté | Un nouvel essai avec le message d'erreur, puis repli |
| Requête invalide | HTTP 400 | Pas de retry : c'est un bug |
stateDiagram-v2 [*] --> Principal Principal --> Succes : "réponse valide" Principal --> Retry : "429 / 5xx / timeout" Retry --> Principal : "essai suivant (backoff)" Retry --> Repli : "essais épuisés" Principal --> Repli : "sortie invalide 2 fois" Repli --> Succes : "modèle de repli OK" Repli --> Degrade : "échec" Degrade --> [*] Succes --> [*]
Retry avec backoff exponentiel
Les SDK officiels (Anthropic, Google Gen AI) intègrent déjà des retries configurables : commencez par eux (max_retries côté client Anthropic, options HTTP côté Google). Si vous devez l'écrire vous-même :
import random, time
class ErreurTransitoire(Exception):
"""429, 5xx, timeout : à relancer."""
def avec_retry(appel, essais=4, base=1.0, plafond=20.0):
for i in range(essais):
try:
return appel()
except ErreurTransitoire:
if i == essais - 1:
raise
attente = min(plafond, base * 2 ** i) * random.uniform(0.5, 1.0) # aléa : évite les rafales
time.sleep(attente)Modèle de repli
Un repli peut être un autre modèle (version plus légère, autre région, autre fournisseur) ou une réponse dégradée (« le service est momentanément indisponible, voici les liens vers la documentation »).
def repondre_resilient(question, principal, secondaire):
try:
return avec_retry(lambda: principal(question)), "principal"
except ErreurTransitoire:
try:
return avec_retry(lambda: secondaire(question), essais=2), "secondaire"
except ErreurTransitoire:
return {"reponse": "Service momentanément indisponible. Consultez l'intranet RH.",
"sources": []}, "degrade"Quelle erreur ne doit PAS déclencher de retry automatique ?
Un 400 signale une requête invalide : la renvoyer à l'identique échouera encore. C'est un bug à corriger et à journaliser. 429 et 503 sont transitoires.
Cache : ne pas payer deux fois la même réponse
| Type de cache | Principe | Attention |
|---|---|---|
| Cache exact applicatif | Clé = hash(modèle + prompt + question normalisée) | Invalider quand l'index ou le prompt change (inclure leur version dans la clé) |
| Mise en cache de prompt (Claude) | Le préfixe stable (prompt système, documents) est mis en cache côté API | Structurer le prompt : partie stable en premier |
| Context caching (Vertex AI / Gemini) | Un gros contexte réutilisé est stocké côté Google | Coût de stockage, durée de vie à fixer |
| Cache sémantique | Questions proches → même réponse | Risque de servir une réponse à une question différente : à évaluer |
import hashlib, json
from functools import lru_cache
VERSION = {"prompt": "v7", "index": "2026-10-01", "modele": "gemini-2.5-flash"}
def cle(question):
q = " ".join(question.lower().split())
return hashlib.sha256(json.dumps([VERSION, q]).encode()).hexdigest()
# ponytail: cache en mémoire par processus ; Memorystore/Redis si plusieurs instances
@lru_cache(maxsize=5000)
def _repondre_cache(cle_q, question):
return repondre_protege(question, appeler_rag) # leçon précédente
def repondre_avec_cache(question):
return _repondre_cache(cle(question), question)Limites de coûts
- Par appel :
max_tokens(Claude) oumax_output_tokens(Gemini), nombre maximal d'extraits dans le contexte. - Par utilisateur : quota de requêtes par jour.
- Global : alertes de budget Cloud Billing sur GCP, limites de dépenses dans la console Anthropic.
- Par campagne d'évaluation : le juge coûte aussi ; estimez avant de lancer (cas × critères × répétitions).
Vous ajoutez un cache exact. Quels éléments doivent faire partie de la clé ?
Sans version, un changement de prompt ou d'index sert d'anciennes réponses. Sans périmètre de droits, un utilisateur peut recevoir une réponse fondée sur des documents qu'il ne doit pas voir. L'heure rendrait le cache inutile.
Exercice : estimer le budget d'une campagne d'évaluation
Votre jeu doré compte 30 cas. Chaque cas est exécuté 3 fois. Chaque exécution coûte un appel à l'assistant et deux appels au juge (fidélité, complétude). Combien d'appels LLM au total ? Que proposez-vous pour réduire ce coût en CI sans perdre en sécurité ?
Voir la solution
30 cas × 3 répétitions × (1 assistant + 2 juge) = 270 appels.
Pistes pour la CI :
- Lancer d'abord les contrôles par code (gratuits) et n'appeler le juge que si le cas passe ces contrôles (un cas déjà en échec n'a pas besoin d'être jugé).
- Répéter 3 fois seulement les cas marqués instables, une fois les autres.
- Utiliser un modèle juge plus léger, après avoir vérifié son accord avec l'humain.
- Mettre en cache les réponses de l'assistant pour une même version de prompt/index/modèle : relancer la CI sans changement ne coûte rien.
Le jeu complet avec répétitions peut tourner chaque nuit, la version allégée à chaque pull request.
Je sais faire
- Distinguer erreurs transitoires (retry) et erreurs permanentes (pas de retry).
- Implémenter un backoff exponentiel avec aléa et un nombre d'essais borné.
- Mettre en place un modèle de repli lui-même évalué.
- Construire une clé de cache qui inclut versions et périmètre de droits.
- Borner les coûts par appel, par utilisateur, globalement et par campagne d'évaluation.