Normal path: execute → tool → observe.
Problem (aus der Praxis)
Dein Agent “arbeitet”.
Die Logs sagen:
search.read47×http.get19×- 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
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 valimport 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:
- tool-call budgets pro Run (hard stop)
- dedupe window (tool+args)
- safe-mode: “search ist kaputt; hier ist, was ich ohne search kann”
- alerting auf
tool_calls/runspikes
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)
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)
Von Patterns genutzt
Verwandte Failures
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”.
Verwandte Seiten (3–6 Links)
- Foundations: Planning vs Reactive Agents · Production-ready Agent
- Failure: Budget Explosion · Infinite Loop
- Governance: Tool Permissions
- Production stack: Production Stack