Problema
La solicitud parece normal: verificar el estado de una devolución y dar una respuesta breve.
En los traces se ve otra cosa: en 6 minutos, un run hizo 52 llamadas a herramientas
(search.read - 31, crm.lookup - 14, http.get - 7) y aun así terminó en timeout.
Para este tipo de tarea, puede ser ~$3 en lugar de los ~$0.10 habituales.
La API está formalmente "alive": la mayoría de respuestas son 200, sin caída explícita.
Pero el usuario no recibe respuesta, y el costo del run crece con cada repetición.
El sistema no se cae.
Simplemente multiplica llamadas idénticas y quema presupuesto en silencio.
Analogía: imagina a un operador de soporte que pulsa redial al mismo número, en vez de escalar la tarea o cambiar el plan. Está ocupado, pero el problema no avanza. El tool spam en agentes se ve igual: muchas acciones, poco progreso útil.
Por qué pasa
El tool spam no aparece porque el agente "se esfuerce demasiado", sino porque la runtime no distingue una acción nueva útil de un duplicado sin progreso.
En producción, normalmente pasa así:
- LLM elige un
tool_call; - la herramienta devuelve una señal inestable o insuficiente;
- el agente repite la misma llamada (o casi la misma);
- sin dedupe, budget gates y una retry policy única, el ciclo se expande.
El problema no es una herramienta concreta. El problema es que el sistema no limita llamadas repetidas antes de que se vuelvan un incidente.
Fallos más frecuentes
Para mantenerlo práctico, en producción suelen verse cuatro patrones de tool spam.
Repeated signature spam
El agente llama el mismo tool con los mismos argumentos varias veces seguidas.
Causa típica: no hay dedupe por tool+args_hash dentro del run.
Argument jitter spam
Solo cambian detalles menores en los argumentos: mayúsculas, espacios, orden de palabras. Semánticamente es la misma solicitud, pero el sistema la trata como nueva.
Causa típica: no hay normalización de argumentos antes de dedupe.
Retry amplification
Hay retries en el agente, en el gateway y en el SDK de la herramienta. Un solo fallo se convierte en una serie de llamadas duplicadas.
Causa típica: retry policy repartida en varios lugares.
Fan-out spam
Un paso del agente lanza muchas llamadas paralelas sin límite estricto. Incluso sin ciclo, esto sobrecarga APIs externas muy rápido.
Causa típica: no hay bounded fan-out ni per-tool caps.
Cómo detectar estos problemas
El tool spam se ve bien con la combinación de métricas de runtime y gateway.
| Métrica | Señal de tool spam | Qué hacer |
|---|---|---|
tool_calls_per_task | crecimiento brusco de llamadas por run | definir max_tool_calls y per-tool caps |
repeated_tool_signature_rate | repeticiones frecuentes de tool+args dentro del run | agregar dedupe window y caché de vida corta |
unique_signature_ratio | cae la proporción de llamadas únicas | agregar regla no-progress para N pasos |
retry_amplification_rate | los retries se duplican entre capas | centralizar retry policy en un único gateway |
cost_per_run | el costo del run sube sin mejorar calidad | activar budget gate y kill switch para la herramienta problemática |
Cómo distinguir tool spam de una búsqueda realmente amplia
No toda cantidad alta de llamadas significa fallo. La pregunta clave: ¿cada llamada agrega una señal nueva útil?
Normal si:
- nuevos
tool_callrealmente abren fuentes o hechos nuevos; unique_signature_ratiose mantiene estable;- el costo crece junto con la calidad de la respuesta.
Peligroso si:
- se repite la misma signature (o casi la misma);
- 3-5 pasos seguidos no aportan información nueva;
- costo y latencia suben, pero la respuesta no mejora.
Cómo detener estos fallos
En la práctica se ve así:
- defines
max_tool_callspor run y límites por herramienta; - agregas dedupe por
tool+args_hashcon ventana corta; - dejas retry policy solo en gateway (con lista clara de errores non-retryable);
- ante duplicados o límite excedido, devuelves resultado cached/partial y stop reason.
Guard mínimo para controlar llamadas repetidas:
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
Este es un guard base: en producción suele añadirse normalización de dominio antes de args_hash
(trim/lowercase/collapse spaces para texto, y canonical ordering para campos específicos),
y on_tool_call(...) se ejecuta antes del tool real para cortar duplicados antes de una llamada externa innecesaria.
Dónde se implementa en la arquitectura
El control de tool spam en producción suele repartirse en tres capas.
Agent Runtime se encarga de límites del run,
stop reasons, reglas no-progress y cierre controlado.
Aquí normalmente se registran budget:tool_calls y tool_spam:*.
Tool Execution Layer se encarga de dedupe, retry policy, caché corto y normalización de errores de herramientas. Si esta capa es débil, el spam se extiende rápido por todo el workflow.
Policy Boundaries define qué herramientas se pueden llamar, con qué frecuencia y en qué condiciones. Esto permite limitar herramientas de riesgo incluso antes de ejecutar la llamada.
Autoevaluación
Verificación rápida antes del release. Marca los puntos y mira el estado abajo.
Este es un sanity-check corto, no una auditoría formal.
Progreso: 0/8
⚠ Hay señales de riesgo
Faltan controles básicos. Cierra los puntos clave del checklist antes del release.
FAQ
Q: ¿Basta solo con max_steps?
A: No. Un paso de agente puede incluir múltiples tool_call, por eso hace falta un límite separado para herramientas.
Q: ¿Dedupe mata la freshness?
A: No, si dedupe es corto y scoped por run. Su objetivo es quitar duplicados de ruido, no cachear "verdad vieja" por mucho tiempo.
Q: ¿Dónde deben vivir los retries?
A: En un único choke point, normalmente en el tool gateway. Ahí también conviene cortar explícitamente errores non-retryable: 401, 403, 404, schema validation errors y policy denials normalmente deben terminar el run de inmediato.
Q: ¿Qué mostrar al usuario si el run se detiene por spam?
A: Motivo de parada, qué ya se verificó y el siguiente paso seguro (fallback o escalación manual).
El tool spam casi nunca parece una caída ruidosa.
Es una inflación lenta de llamadas, latencia y gasto, visible sobre todo en traces.
Por eso los agentes de producción necesitan no solo mejores modelos, sino control estricto de tool_call a nivel runtime y gateway.
Páginas relacionadas
Para cubrir este problema a fondo, revisa:
- Por qué fallan los agentes de IA - mapa general de fallos en producción.
- Infinite loop - cómo un bucle se convierte rápido en llamadas repetidas.
- Budget explosion - cómo tool spam infla costos.
- Tool failure - cómo herramientas inestables disparan olas de retries.
- Agent Runtime - dónde definir stop reasons y límites de ejecución.
- Tool Execution Layer - dónde mantener dedupe, retries y control de llamadas.