Tool-Spam Loops (Agent Failure Mode + Fixes + Code)

  • Erkenne den Fehler früh, bevor die Rechnung steigt.
  • Verstehe, was in Prod bricht – und warum.
  • Guardrails kopieren: Budgets, Stop-Reasons, Validation.
  • Wissen, wann das nicht die Root Cause ist.
Erkennungs-Signale
  • Tool-Calls pro Run steigen (oder wiederholen sich mit args-hash).
  • Kosten/Tokens pro Request steigen ohne bessere Ergebnisse.
  • Retries kippen von selten zu konstant (429/5xx).
Wenn ein Agent dasselbe Tool immer wieder aufruft, zahlst du dafür. Warum Tool Spam in Prod passiert und wie du’s stoppst.
Auf dieser Seite
  1. Problem (aus der Praxis)
  2. Warum das in Production bricht
  3. 1) Kein Tool-Call Budget (oder nur Step Budget)
  4. 2) Tool Output ist leicht nondeterministisch
  5. 3) Kein Dedupe Window
  6. 4) Keine “ich hab das schon probiert” Scratchpad
  7. 5) Retries multiplizieren Spam
  8. Implementierungsbeispiel (echter Code)
  9. Echter Incident (mit Zahlen)
  10. Abwägungen
  11. Wann du es NICHT nutzen solltest
  12. Checkliste (Copy/Paste)
  13. Sicheres Default-Config-Snippet (JSON/YAML)
  14. FAQ (3–5)
  15. Verwandte Seiten (3–6 Links)
Interaktiver Ablauf
Szenario:
Schritt 1/2: Execution

Normal path: execute → tool → observe.

Problem (aus der Praxis)

Dein Agent “arbeitet”.

Die Logs sagen:

  • search.read 47×
  • http.get 19×
  • Request timed out

Der User sieht: nichts.

Du siehst: Kosten.

Tool Spam ist oft der erste Production-Incident, weil es harmlos aussieht: “der Agent bemüht sich”. In Wirklichkeit ist es eine Loop mit besserem Text.

Warum das in Production bricht

Tool Spam ist selten ein einzelner Bug. Es ist ein Bundle kleiner Versäumnisse.

1) Kein Tool-Call Budget (oder nur Step Budget)

Ein Step Budget hilft nicht, wenn ein “Step” 5 Tools callen kann. Du brauchst:

  • max steps
  • max tool calls
  • max time
  • max spend

2) Tool Output ist leicht nondeterministisch

Search ist nondeterministisch. Webseiten ändern sich. Rankings reorder’n.

Wenn der Agent “gleiches Input → gleiches Output” erwartet, probiert er es weiter, bis er “confidence” fühlt. Confidence ist keine Stop Condition.

3) Kein Dedupe Window

Dasselbe Tool mit denselben Args ist keine “Gründlichkeit”. Das ist ein Bug.

Fix: cache/dedupe pro (tool_name, args_hash) innerhalb eines Runs (oder kurzer Window).

4) Keine “ich hab das schon probiert” Scratchpad

Reactive Loops brauchen Memory:

  • “ich habe nach X gesucht”
  • “ich habe Y gefetcht”
  • “das hilft nicht wegen Z”

Ohne das rediscovered der Agent dieselben Dead Ends.

5) Retries multiplizieren Spam

Wenn das Tool retryst und der Agent retryst (indem er den Call neu erzeugt), bekommst du:

  • retry storm
  • plus agent loop

So schmilzt du Rate Limits.

Implementierungsbeispiel (echter Code)

Minimaler “anti-spam” Tool Gateway:

  • Tool-Call Budget pro Run
  • Dedupe Window pro Tool+Args
  • cheap caching via args hash
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;
}
}

Das löst nicht “Agents”. Das löst einen langweiligen Teil: gleiche Calls verbrennen nicht dein Budget.

Du brauchst trotzdem:

  • Loop Detection in der Agent Loop
  • Stop Reasons
  • Partial Results wenn Budgets hitten

Echter Incident (mit Zahlen)

Wir shippten einen Support Agent, der search.read für KB Links nutzte.

Während eines Vendor-Outages wurden Ergebnisse instabil (Timeouts + partial responses). Der Agent interpretierte das als “nicht genug confidence” und suchte weiter.

Impact (ein Vormittag):

  • avg tool calls/run: 3 → 28
  • Rate Limits triggerten und degradierten andere Services
  • model + tool spend: +$310 (größtenteils Waste)

Fix:

  1. tool-call budgets pro Run (hard stop)
  2. dedupe window (tool+args)
  3. safe-mode: “search ist kaputt; hier ist, was ich ohne search kann”
  4. alerting auf tool_calls/run spikes

Tool Spam ist nicht “der Agent ist neugierig”. Es sind fehlende Bremsen.

Abwägungen

  • Caching/Dedupe kann echte Änderungen verstecken (stabiler, weniger frisch).
  • Budgets cutten “fast fertig” Runs (besser als bankrott).
  • Safe-mode senkt Answer Quality, erhöht Reliability und Cost Control.

Wann du es NICHT nutzen solltest

  • Wenn Freshness wichtiger ist als Cost: cache nicht aggressiv (kleinere windows).
  • Wenn ein Tool deterministisch und billig ist, brauchst du weniger Dedupe (Budgets trotzdem).
  • Wenn’s deterministisch ist: Workflow statt Agent.

Checkliste (Copy/Paste)

  • [ ] Max tool calls pro Run
  • [ ] Max time pro Run
  • [ ] Dedupe window pro (tool, args hash)
  • [ ] Read-Tools cachen (kurzes TTL)
  • [ ] Retry Policy an einem Ort (gateway), nicht Agent + Tool
  • [ ] Loop Detection: repeated action keys stoppen
  • [ ] Stop Reasons: tool budget vs time budget vs loop detected
  • [ ] Alerts: tool_calls/run, spend/run, latency/run

Sicheres Default-Config-Snippet (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)

Ist mehr Suchen nicht besser?
Nicht wenn es dieselbe Suche 30× ist. Repeated Tool Calls sind in Prod ein Symptom, keine Gründlichkeit.
Soll ich über Runs dedupen?
Meist nein. Dedupe im Run (oder kurze Window). Cross-run caching braucht harte Invalidation.
Wo gehören Retries hin?
An einen Choke Point: Tool Gateway. Wenn Agent und Tool retry’n, baust du Stürme.
Was returne ich wenn Budgets hitten?
Partial Results + klarer Stop Reason. Stille Timeouts trainieren User auf ‘Refresh-Spam’.

Q: Ist mehr Suchen nicht besser?
A: Nicht wenn es dieselbe Suche 30× ist. Repeated Tool Calls sind in Prod ein Symptom, keine Gründlichkeit.

Q: Soll ich über Runs dedupen?
A: Meist nein. Dedupe im Run (oder kurze Window). Cross-run caching braucht harte Invalidation.

Q: Wo gehören Retries hin?
A: An einen Choke Point: Tool Gateway. Wenn Agent und Tool retry’n, baust du Stürme.

Q: Was returne ich wenn Budgets hitten?
A: Partial Results + klarer Stop Reason. Stille Timeouts trainieren User auf “Refresh-Spam”.

Nicht sicher, ob das dein Fall ist?

Agent gestalten ->
⏱️ 6 Min. LesezeitAktualisiert Mär, 2026Schwierigkeit: ★★☆
In OnceOnly umsetzen
Guardrails for loops, retries, and spend escalation.
In OnceOnly nutzen
# 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 }
Integriert: Production ControlOnceOnly
Guardrails für Tool-Calling-Agents
Shippe dieses Pattern mit Governance:
  • Budgets (Steps / Spend Caps)
  • Kill switch & Incident Stop
  • Audit logs & Nachvollziehbarkeit
  • Idempotenz & Dedupe
  • Tool-Permissions (Allowlist / Blocklist)
Integrierter Hinweis: OnceOnly ist eine Control-Layer für Production-Agent-Systeme.
Beispiel-Policy (Konzept)
# Example (Python — conceptual)
policy = {
  "budgets": {"steps": 20, "seconds": 60, "usd": 1.0},
  "controls": {"kill_switch": True, "audit": True},
}
Autor

Diese Dokumentation wird von Engineers kuratiert und gepflegt, die AI-Agenten in der Produktion betreiben.

Die Inhalte sind KI-gestützt, mit menschlicher redaktioneller Verantwortung für Genauigkeit, Klarheit und Produktionsrelevanz.

Patterns und Empfehlungen basieren auf Post-Mortems, Failure-Modes und operativen Incidents in produktiven Systemen, auch bei der Entwicklung und dem Betrieb von Governance-Infrastruktur für Agenten bei OnceOnly.