Tool Spam Loops (fallo del agente + fixes + código)

  • Detecta el fallo temprano antes de que suba el gasto.
  • Entiende qué se rompe en producción y por qué.
  • Copia guardrails: budgets, stop reasons, validación.
  • Sabe cuándo esto no es la causa raíz.
Señales de detección
  • Tool calls por run suben (o repiten mismo args hash).
  • Gasto/tokens suben sin mejorar el resultado.
  • Retries pasan de raros a constantes (429/5xx).
Cuando un agente llama el mismo tool una y otra vez, lo pagas. Así nace el tool spam en producción y cómo pararlo.
En esta página
  1. El problema (en producción)
  2. Por qué esto se rompe en producción
  3. 1) No hay presupuesto de tool calls (o solo hay presupuesto de pasos)
  4. 2) El output del tool es ligeramente no determinista
  5. 3) No hay dedupe window
  6. 4) El agente no tiene memoria de “ya lo intenté”
  7. 5) Los retries multiplican el spam
  8. Ejemplo de implementación (código real)
  9. Incidente real (con números)
  10. Trade-offs
  11. Cuándo NO usarlo
  12. Checklist (copiar/pegar)
  13. Config segura por defecto (JSON/YAML)
  14. FAQ (3–5)
  15. Páginas relacionadas (3–6 links)
Flujo interactivo
Escenario:
Paso 1/2: Execution

Normal path: execute → tool → observe.

El problema (en producción)

Tu agente está “trabajando”.

Los logs dicen:

  • search.read llamado 47 veces
  • http.get llamado 19 veces
  • la request igual timeouteó

El usuario ve: nada.

Tú ves: factura.

El tool spam es uno de los “primeros incidentes” más comunes porque no parece catastrófico. Parece “el agente se está esforzando”. En realidad, casi siempre es un loop con texto más bonito.

Por qué esto se rompe en producción

El tool spam casi nunca viene de una sola causa. Es un ecosistema de errores pequeños.

1) No hay presupuesto de tool calls (o solo hay presupuesto de pasos)

Un step budget no sirve si un “paso” puede disparar 5 tools. Necesitas ambos:

  • max steps
  • max tool calls
  • max tiempo
  • max gasto

2) El output del tool es ligeramente no determinista

Search no es determinista. Las páginas cambian. Los resultados basados en tiempo se reordenan.

Si el agente espera “mismo input → mismo output”, seguirá intentando hasta que “se sienta confiado”. Confianza no es una stop condition.

3) No hay dedupe window

Si el agente llama el mismo tool con los mismos args, eso no es “thoroughness”. Es un bug.

El fix es aburrido: cachea tool calls por (tool_name, args_hash) dentro de un run (o dentro de una ventana corta).

4) El agente no tiene memoria de “ya lo intenté”

Los loops reactivos necesitan un scratchpad:

  • “Busqué X”
  • “Fetcheé Y”
  • “Esto no ayudó porque Z”

Sin eso, el agente vuelve a descubrir el mismo callejón sin salida.

5) Los retries multiplican el spam

Si el tool tiene retries y el agente también retría re-emitiendo la llamada, consigues:

  • una tormenta de retries del tool
  • más el loop del agente

Así derrites rate limits.

Ejemplo de implementación (código real)

Un gateway mínimo “anti-spam”:

  • presupuesto de tool calls por run
  • dedupe window por tool
  • caching barato por hash de args
PYTHON
import hashlib
import json
import time
from dataclasses import dataclass
from typing import Any, Callable


def stable_hash(obj: Any) -> str:
  raw = json.dumps(obj, sort_keys=True, ensure_ascii=False).encode("utf-8")
  return hashlib.sha256(raw).hexdigest()


@dataclass
class ToolBudgets:
  max_calls: int = 12
  dedupe_window_s: int = 60


class ToolSpamDetected(RuntimeError):
  pass


class ToolGateway:
  def __init__(self, *, impls: dict[str, Callable[..., Any]], budgets: ToolBudgets):
      self.impls = impls
      self.budgets = budgets
      self.calls = 0
      self.cache: dict[str, tuple[float, Any]] = {}

  def call(self, name: str, args: dict[str, Any]) -> Any:
      self.calls += 1
      if self.calls > self.budgets.max_calls:
          raise ToolSpamDetected(f"tool budget exceeded (calls={self.calls})")

      key = f"{name}:{stable_hash(args)}"
      now = time.time()
      hit = self.cache.get(key)
      if hit:
          ts, val = hit
          if now - ts <= self.budgets.dedupe_window_s:
              return val

      fn = self.impls.get(name)
      if not fn:
          raise RuntimeError(f"unknown tool: {name}")

      val = fn(**args)
      self.cache[key] = (now, val)
      return val
JAVASCRIPT
import crypto from "node:crypto";

export class ToolSpamDetected extends Error {}

export function stableHash(obj) {
const raw = JSON.stringify(obj);
return crypto.createHash("sha256").update(raw).digest("hex");
}

export class ToolGateway {
constructor({ impls = {}, budgets = { maxCalls: 12, dedupeWindowS: 60 } } = {}) {
  this.impls = impls;
  this.budgets = budgets;
  this.calls = 0;
  this.cache = new Map(); // key -> { ts, val }
}

call(name, args) {
  this.calls += 1;
  if (this.calls > this.budgets.maxCalls) {
    throw new ToolSpamDetected("tool budget exceeded (calls=" + this.calls + ")");
  }

  const key = name + ":" + stableHash(args);
  const now = Date.now() / 1000;
  const hit = this.cache.get(key);
  if (hit && now - hit.ts <= this.budgets.dedupeWindowS) return hit.val;

  const fn = this.impls[name];
  if (!fn) throw new Error("unknown tool: " + name);

  const val = fn(args);
  this.cache.set(key, { ts: now, val });
  return val;
}
}

Esto no “arregla agentes”. Arregla una cosa aburrida: llamadas repetidas con los mismos args dejan de quemar presupuesto.

Igual necesitas:

  • loop detection en el agent loop
  • stop reasons claros
  • y poder devolver resultados parciales cuando se llegue al budget

Incidente real (con números)

Shipeamos un agente de soporte que usaba search.read para encontrar páginas relevantes de la KB.

Durante un outage del proveedor de search, los resultados se volvieron inestables (timeouts + respuestas parciales). El agente lo interpretó como “no tengo suficiente confianza” y siguió buscando.

Impacto (una mañana):

  • tool calls promedio por run: 3 → 28
  • se dispararon rate limits y degradaron otros servicios
  • gasto modelo + tools: +$310 ese día (mayormente basura)

Fix:

  1. budgets duros de tool calls por run (hard stop)
  2. dedupe window por tool+args
  3. safe-mode: “no puedo buscar ahora; aquí está lo que sé sin search”
  4. alertas cuando tool_calls/run se dispare

El tool spam no es “curiosidad”. Es falta de frenos.

Trade-offs

  • Caching/dedupe puede esconder cambios reales (bien para estabilidad, mal para frescura).
  • Los budgets pueden cortar runs “casi listos” (mejor que runs que te arruinan).
  • Safe-mode baja calidad, pero sube fiabilidad y control de coste.

Cuándo NO usarlo

  • Si la frescura importa más que el coste, no cachees agresivo (ventanas más pequeñas).
  • Si un tool es determinista y barato, dedupe quizá no aporta (aun así, mantén budgets).
  • Si la tarea es determinista, no uses un agente. Usa un workflow.

Checklist (copiar/pegar)

  • [ ] Max tool calls por run
  • [ ] Max tiempo por run
  • [ ] Dedupe window por (tool, args hash)
  • [ ] Cache de read tools (TTL corto)
  • [ ] Retry policy en un solo sitio (gateway), no en agente + tool
  • [ ] Loop detection: action keys repetidas paran el run
  • [ ] Stop reasons: tool budget vs time budget vs loop detected
  • [ ] Alertas: tool_calls/run, spend/run, latency/run

Config segura por defecto (JSON/YAML)

YAML
budgets:
  max_steps: 25
  max_tool_calls: 12
  max_seconds: 60
tools:
  dedupe_window_s: 60
  cache_ttl_s: 30
  retries:
    max_attempts: 2
    retryable_status: [408, 429, 500, 502, 503, 504]

FAQ (3–5)

¿No es mejor buscar más?
No si es la misma búsqueda 30 veces. En producción, tool calls repetidas son un síntoma, no diligencia.
¿Debo deduplicar entre runs?
Normalmente no. Dedupe dentro de un run (o ventana corta). Cache cross-run necesita invalidación cuidadosa.
¿Dónde van los retries?
En un choke point: el tool gateway. Si el agente y el tool retrían, creas tormentas.
¿Qué devuelvo cuando se acaba el budget?
Resultados parciales + stop reason claro. Timeouts silenciosos entrenan a la gente a refrescar.

Q: ¿No es mejor buscar más?
A: No si es la misma búsqueda 30 veces. En producción, tool calls repetidas son un síntoma, no diligencia.

Q: ¿Debo deduplicar entre runs?
A: Normalmente no. Dedupe dentro de un run (o ventana corta). Cache cross-run necesita invalidación cuidadosa.

Q: ¿Dónde van los retries?
A: En un choke point: el tool gateway. Si el agente y el tool retrían, creas tormentas.

Q: ¿Qué devuelvo cuando se acaba el budget?
A: Resultados parciales + stop reason claro. Timeouts silenciosos entrenan a la gente a refrescar.

No sabes si este es tu caso?

Disena tu agente ->
⏱️ 7 min de lecturaActualizado Mar, 2026Dificultad: ★★☆
Implementar en OnceOnly
Guardrails for loops, retries, and spend escalation.
Usar en OnceOnly
# onceonly guardrails (concept)
version: 1
budgets:
  max_steps: 25
  max_tool_calls: 12
  max_seconds: 60
  max_usd: 1.00
policy:
  tool_allowlist:
    - search.read
    - http.get
controls:
  loop_detection:
    enabled: true
    dedupe_by: [tool, args_hash]
  retries:
    max: 2
    backoff_ms: [200, 800]
stop_reasons:
  enabled: true
logging:
  tool_calls: { enabled: true, store_args: false, store_args_hash: true }
Integrado: control en producciónOnceOnly
Guardrails para agentes con tool-calling
Lleva este patrón a producción con gobernanza:
  • Presupuestos (pasos / topes de gasto)
  • Kill switch y parada por incidente
  • Audit logs y trazabilidad
  • Idempotencia y dedupe
  • Permisos de herramientas (allowlist / blocklist)
Mención integrada: OnceOnly es una capa de control para sistemas de agentes en producción.
Ejemplo de policy (concepto)
# Example (Python — conceptual)
policy = {
  "budgets": {"steps": 20, "seconds": 60, "usd": 1.0},
  "controls": {"kill_switch": True, "audit": True},
}
Autor

Esta documentación está curada y mantenida por ingenieros que despliegan agentes de IA en producción.

El contenido es asistido por IA, con responsabilidad editorial humana sobre la exactitud, la claridad y la relevancia en producción.

Los patrones y las recomendaciones se basan en post-mortems, modos de fallo e incidentes operativos en sistemas desplegados, incluido durante el desarrollo y la operación de infraestructura de gobernanza para agentes en OnceOnly.