Problème
La demande semble simple : vérifier le statut d'un retour et répondre brièvement.
Mais dans les traces, c'est autre chose : en 6 minutes, un run a fait 52 appels d'outils
(search.read - 31, crm.lookup - 14, http.get - 7) et a quand même fini en timeout.
Pour ce type de tâche, cela peut coûter ~$3 au lieu des ~$0.10 habituels.
L'API est formellement "alive" : la plupart des réponses sont 200, sans crash explicite.
Mais l'utilisateur n'a pas de réponse, et le coût du run augmente à chaque répétition.
Le système ne crash pas.
Il multiplie les mêmes appels et brûle le budget en silence.
Analogie : imagine un opérateur support qui appuie sur redial vers le même numéro, au lieu d'escalader la tâche ou de changer de plan. Il est occupé, mais le problème n'avance pas. Le tool spam dans les agents ressemble exactement à ça : beaucoup d'actions, peu de progrès utile.
Pourquoi cela arrive
Le tool spam n'arrive pas parce que l'agent "essaie trop fort", mais parce que la runtime ne distingue pas une nouvelle action utile d'un doublon sans progrès.
En production, cela ressemble souvent à ceci :
- LLM choisit un
tool_call; - l'outil renvoie un signal instable ou insuffisant ;
- l'agent répète le même appel (ou presque le même) ;
- sans dedupe, budget gates et retry policy unique, le cycle s'amplifie.
Le problème n'est pas un outil isolé. Le problème est que le système ne limite pas les appels répétés avant qu'ils deviennent un incident.
Pannes les plus fréquentes
Pour rester pratique, en production on voit surtout quatre patterns de tool spam.
Repeated signature spam
L'agent appelle le même tool avec les mêmes arguments plusieurs fois d'affilée.
Cause typique : pas de dedupe sur tool+args_hash dans le run.
Argument jitter spam
Seuls de petits détails changent dans les arguments : casse, espaces, ordre des mots. Sémantiquement c'est la même requête, mais le système la traite comme nouvelle.
Cause typique : pas de normalisation des arguments avant dedupe.
Retry amplification
Les retries se produisent dans l'agent, dans le gateway, et dans le SDK de l'outil. Une seule erreur devient une série d'appels dupliqués.
Cause typique : retry policy dispersée à plusieurs endroits.
Fan-out spam
Une étape de l'agent lance beaucoup d'appels parallèles sans limite stricte. Même sans cycle, cela surcharge vite les API externes.
Cause typique : pas de bounded fan-out ni de per-tool caps.
Comment détecter ces problèmes
Le tool spam se voit bien avec la combinaison des métriques runtime et gateway.
| Métrique | Signal de tool spam | Action |
|---|---|---|
tool_calls_per_task | hausse brutale des appels par run | poser max_tool_calls et des per-tool caps |
repeated_tool_signature_rate | répétitions fréquentes de tool+args dans un run | ajouter dedupe window et cache court |
unique_signature_ratio | la part des appels uniques baisse | ajouter une règle no-progress sur N étapes |
retry_amplification_rate | les retries se dupliquent entre couches | centraliser la retry policy dans un seul gateway |
cost_per_run | le coût du run augmente sans gain de qualité | activer budget gate et kill switch sur l'outil problématique |
Comment distinguer tool spam d'une recherche vraiment large
Un grand nombre d'appels n'est pas toujours un échec. La question clé : chaque appel apporte-t-il un nouveau signal utile ?
Normal si :
- les nouveaux
tool_callouvrent réellement de nouvelles sources ou faits ; unique_signature_ratioreste stable ;- les coûts augmentent avec la qualité de réponse.
Dangereux si :
- la même signature (ou presque la même) se répète ;
- 3-5 étapes d'affilée n'apportent aucune information nouvelle ;
- coût et latence augmentent, mais la réponse ne s'améliore pas.
Comment arrêter ces pannes
En pratique, cela ressemble à ceci :
- poser
max_tool_callspar run et des limites par outil ; - ajouter dedupe sur
tool+args_hashavec une fenêtre courte ; - garder retry policy uniquement dans le gateway (avec liste claire des erreurs non retryables) ;
- en cas de doublons ou de limite dépassée, retourner un résultat cached/partial et un stop reason.
Garde minimale pour contrôler les appels répétés :
from dataclasses import dataclass
import json
def call_signature(tool: str, args: dict) -> str:
normalized_args = normalize_args(args)
normalized = json.dumps(normalized_args, sort_keys=True, ensure_ascii=False)
return f"{tool}:{normalized}"
def normalize_text(value: str) -> str:
return " ".join(value.strip().lower().split())
def normalize_args(args: dict) -> dict:
normalized: dict = {}
for key, value in args.items():
if isinstance(value, str):
normalized[key] = normalize_text(value)
else:
normalized[key] = value
return normalized
@dataclass(frozen=True)
class ToolSpamLimits:
max_tool_calls: int = 12
max_repeat_per_signature: int = 2
class ToolSpamGuard:
def __init__(self, limits: ToolSpamLimits = ToolSpamLimits()):
self.limits = limits
self.total_calls = 0
self.by_signature: dict[str, int] = {}
def on_tool_call(self, tool: str, args: dict) -> str | None:
self.total_calls += 1
if self.total_calls > self.limits.max_tool_calls:
return "budget:tool_calls"
sig = call_signature(tool, args)
self.by_signature[sig] = self.by_signature.get(sig, 0) + 1
if self.by_signature[sig] > self.limits.max_repeat_per_signature:
return "tool_spam:repeated_signature"
return None
C'est une garde de base : en production, on ajoute souvent une normalisation métier avant args_hash
(trim/lowercase/collapse spaces pour le texte, et canonical ordering pour certains champs),
et on_tool_call(...) est exécuté avant l'exécution réelle du tool pour stopper le doublon avant l'appel externe inutile.
Où c'est implémenté dans l'architecture
Le contrôle du tool spam en production se répartit généralement sur trois couches.
Agent Runtime gère les limites du run,
les stop reasons, les règles no-progress et la fin contrôlée.
C'est ici qu'on enregistre en général budget:tool_calls et tool_spam:*.
Tool Execution Layer gère dedupe, retry policy, cache court et normalisation des erreurs d'outils. Si cette couche est faible, le spam se propage rapidement dans tout le workflow.
Policy Boundaries définit quels outils peuvent être appelés, à quelle fréquence et sous quelles conditions. Cela permet de limiter les outils risqués avant même l'exécution.
Auto-vérification
Vérification rapide avant release. Coche les points et regarde le statut ci-dessous.
C'est un sanity-check court, pas un audit formel.
Progression: 0/8
⚠ Il y a des signaux de risque
Il manque des contrôles de base. Fermez les points clés de la checklist avant release.
FAQ
Q : max_steps seul suffit ?
R : Non. Une étape d'agent peut contenir plusieurs tool_call, il faut donc une limite dédiée aux outils.
Q : Le dedupe tue la freshness ?
R : Non, si le dedupe est court et scoped par run. Son but est de retirer les doublons de bruit, pas de garder une "vieille vérité" trop longtemps.
Q : Où doivent vivre les retries ?
R : Dans un choke point unique, généralement le tool gateway. Il faut aussi couper explicitement les erreurs non retryables : 401, 403, 404, schema validation errors et policy denials doivent en général terminer le run immédiatement.
Q : Que montrer à l'utilisateur si le run est stoppé pour spam ?
R : La raison de l'arrêt, ce qui a déjà été vérifié, et la prochaine étape sûre (fallback ou escalade manuelle).
Le tool spam ressemble rarement à une panne bruyante.
C'est un gonflement progressif des appels, de la latence et des coûts, visible surtout dans les traces.
C'est pourquoi les agents en production ont besoin non seulement de meilleurs modèles, mais aussi d'un contrôle strict des tool_call au niveau runtime et gateway.
Pages liées
Pour couvrir ce problème en profondeur, voir :
- Pourquoi les agents IA échouent - carte générale des pannes en production.
- Infinite loop - comment une boucle se transforme vite en appels répétés.
- Budget explosion - comment le tool spam gonfle les coûts.
- Tool failure - comment un outil instable déclenche des vagues de retries.
- Agent Runtime - où poser stop reasons et limites d'exécution.
- Tool Execution Layer - où garder dedupe, retries et contrôle des appels.