Votre agent IA fonctionne en local. Les tests passent. Vous déployez, et trois jours plus tard il tombe à 2 h du matin, sans aucun log utile.
Les tutoriels s’arrêtent au premier agent qui répond. Les pannes qui comptent n’apparaissent qu’en production : sous charge, avec de vraies API, face à des réponses que personne ne simule en développement.
Voici quatre crashs qu’on retrouve sur presque tous les agents qui utilisent des outils, et le correctif de chacun. Les exemples sont écrits avec l’OpenAI Agents SDK en Python, mais les quatre pièges valent pour n’importe quel framework.
Crash n° 1 : le timeout fantôme
Votre agent appelle une API de prix. En développement, elle répond en 200 ms. En production, un vendredi soir, le serveur est sous charge : l’outil attend, l’agent attend, l’utilisateur attend.
Le code qui paraît correct :
@function_tool
async def get_stock_price(ticker: str) -> dict:
"""Récupère le prix actuel d'une action."""
async with httpx.AsyncClient() as client:
resp = await client.get(f"https://api.marketdata.example/v1/quote/{ticker}")
return resp.json()
httpx applique par défaut un timeout de 5 secondes, identique pour chaque phase : connexion, lecture, écriture, attente d’une connexion libre. Le jour où ça dépasse, vous obtenez un ReadTimeout générique. Vous savez qu’il y a un timeout, pas où ça coince.
Et le piège le plus fréquent n’est pas l’API lente : c’est le pool de connexions saturé. Avec la configuration par défaut (max_connections=100), la 101e requête parallèle attend une connexion libre, puis échoue sans dire pourquoi.
Le correctif : un timeout par phase, et une erreur explicite pour chaque cas.
TOOL_TIMEOUT = httpx.Timeout(connect=3.0, read=10.0, write=5.0, pool=5.0)
@function_tool
async def get_stock_price(ticker: str) -> dict:
"""Récupère le prix actuel d'une action."""
try:
async with httpx.AsyncClient(timeout=TOOL_TIMEOUT) as client:
resp = await client.get(f"https://api.marketdata.example/v1/quote/{ticker}")
resp.raise_for_status()
return resp.json()
except httpx.ConnectTimeout:
return {"error": f"API injoignable pour {ticker}", "hint": "réessayer dans 30 s"}
except httpx.ReadTimeout:
return {"error": f"API trop lente pour {ticker}", "hint": "connexion OK, réponse trop longue"}
except httpx.PoolTimeout:
return {"error": "Pool de connexions saturé", "hint": "réduire le parallélisme"}
except httpx.HTTPStatusError as e:
return {"error": f"HTTP {e.response.status_code} pour {ticker}"}
Deux choses changent. Vous savez enfin où ça bloque. Et l’outil renvoie un message que l’agent peut transmettre clairement à l’utilisateur, au lieu d’une exception qui remonte et le fait tomber.
Crash n° 2 : le JSON pourri
Votre agent de traduction tourne depuis trois semaines. D’un coup, il plante, sans aucun changement dans votre code : l’API a renvoyé une page de maintenance en HTML, avec un statut 200 OK.
@function_tool
async def translate_text(text: str, target_lang: str) -> dict:
"""Traduit le texte vers la langue cible."""
async with httpx.AsyncClient(timeout=10.0) as client:
resp = await client.post("https://api.translate.example/v2/translate",
json={"text": text, "target": target_lang})
data = resp.json()
return {"translated": data["translations"][0]["text"]}
Ce code peut casser de deux façons :
- Le crash bruyant.
resp.json()lève uneJSONDecodeError, parce que le corps commence par<!DOCTYPE html>. L’exception remonte et l’agent s’arrête. - Le crash silencieux, bien pire. Le fournisseur change la structure de sa réponse, ou renvoie
"text": null. Le LLM reçoit{"translated": null}, et au lieu de dire que la traduction a échoué, il en invente une. L’utilisateur ne saura jamais qu’elle est fausse.
Le correctif tient en trois vérifications, dans cet ordre :
content_type = resp.headers.get("content-type", "")
if "application/json" not in content_type: # 1. le bon format
return {"error": f"L'API a renvoyé {content_type} au lieu de JSON",
"body_preview": resp.text[:200]}
try:
data = resp.json() # 2. un JSON valide
except json.JSONDecodeError:
return {"error": "JSON invalide", "body_preview": resp.text[:200]}
translated = None # 3. une vraie valeur
if data.get("translations"):
translated = data["translations"][0].get("text")
if not translated:
return {"error": "Traduction vide ou absente", "keys_received": list(data)}
return {"translated": translated}
La troisième vérification est la plus oubliée et la plus importante. Sans elle, vous fabriquez une hallucination qui ne vient pas du modèle, mais de votre propre code.
Crash n° 3 : la boucle de recherche
Vous demandez : « Résume les dernières nouvelles tech. » L’agent cherche, reçoit des résultats, puis reformule et cherche encore, et encore. Chaque tour ajoute du contexte, et chaque résultat ouvre une nouvelle piste à creuser.
Au 10e tour, le SDK coupe tout avec MaxTurnsExceeded. Vous avez payé dix tours de tokens, et l’agent n’a rien synthétisé.
La cause : le modèle n’a aucun critère d’arrêt. Sa consigne dit « cherche des informations », jamais « arrête-toi quand ». Le correctif tient en trois niveaux :
MAX_SEARCHES = 3
_calls = {"web_search": 0} # en production : dans le contexte du run, pas en global
@function_tool
async def web_search(query: str) -> dict:
"""Search the web. MAXIMUM 3 searches per question."""
_calls["web_search"] += 1 # niveau 1 : un compteur dans l'outil
remaining = MAX_SEARCHES - _calls["web_search"]
if remaining < 0:
return {"error": "Limite atteinte", "action": "Synthétise avec ce que tu as."}
results = await search_api(query)
return {"results": results, "_calls_remaining": remaining}
agent = Agent(
name="ResearchAgent",
instructions="3 recherches maximum, puis tu synthétises. " # niveau 2 : la consigne
"Quand _calls_remaining vaut 0, réponds avec ce que tu as.",
tools=[web_search],
)
result = await Runner.run(agent, input=question, max_turns=10) # niveau 3 : le filet
Le vrai correctif, c’est la consigne : « fais 3 recherches maximum, puis synthétise ». Le compteur et max_turns ne sont que des filets de sécurité.
L’astuce la moins connue est le champ _calls_remaining dans le résultat de l’outil. Le modèle ne compte pas ses appels : lui montrer « il te reste 1 recherche » change nettement son comportement.
Crash n° 4 : l’orchestrateur qui contredit son sous-agent
Votre orchestrateur délègue la recherche documentaire à un sous-agent. Le sous-agent trouve trois documents et les renvoie. L’orchestrateur répond à l’utilisateur : « Je n’ai trouvé aucun document. »
orchestrator = Agent(
name="orchestrator",
instructions="Tu es un assistant. Utilise tes outils si nécessaire.",
tools=[content_finder.as_tool(tool_name="find_content",
tool_description="Cherche dans la base documentaire.")],
)
Avec as_tool(), la réponse du sous-agent redevient une simple donnée pour l’orchestrateur. Il la relit, la réinterprète, puis rédige sa propre réponse. Avec des consignes vagues comme « sois utile », cette réécriture peut aller jusqu’à contredire ce qu’il vient de recevoir.
Le correctif : quand la réponse du sous-agent est la réponse finale, utilisez handoffs au lieu de as_tool(). Le sous-agent prend la main, et sa réponse devient directement le résultat final.
orchestrator = Agent(
name="orchestrator",
instructions="Pour toute recherche documentaire, passe la main à content_finder.",
handoffs=[content_finder], # transfert complet, aucune réécriture
)
| Le résultat du sous-agent est… | À utiliser |
|---|---|
| la réponse finale (recherche, génération, traduction) | handoffs=[...] |
| une étape intermédiaire (extraction, validation, classification) | tools=[agent.as_tool(...)] |
| à relayer tel quel, sans nouveau tour du modèle | tool_use_behavior="stop_on_first_tool" |
Pour chaque sous-agent, posez-vous une seule question : sa sortie est-elle ce que l’utilisateur doit voir, ou une donnée que l’orchestrateur doit traiter ?
Pour aller plus loin
Ces quatre crashs ont un point commun : aucun n’apparaît en démo. Un outil qui ne renvoie jamais d’exception, mais toujours un message d’erreur lisible, évite déjà la plupart d’entre eux.
Ils sont tirés du Jour 2 de mon livre Agents IA en Python : créer, déployer et sécuriser un agent IA en production. Les autres chapitres couvrent la mémoire, les multi-agents, le retry et les garde-fous, le déploiement avec FastAPI et Docker, et la sécurité.
- L’avant-propos et le Jour 1 sont gratuits : mariusgamon.gumroad.com/l/extrait-agents-ia-python
- Le livre complet (PDF et EPUB) : mariusgamon.gumroad.com/l/agents-ia-python, aussi sur Amazon en Kindle et en broché
- Tout le code du livre : github.com/mariusgamon/agents-ia-python-book
Lequel de ces crashs avez-vous déjà rencontré ?