Проблема
Запит виглядає звичайним: перевірити статус повернення і дати коротку відповідь.
У трейсах видно інше: за 6 хвилин один run зробив 52 виклики інструментів
(search.read — 31, crm.lookup — 14, http.get — 7) і все одно завершився timeout.
Для задачі такого класу це може бути ~$3 замість звичних ~$0.10.
API формально "живе": більшість відповідей 200, явного падіння немає.
Але користувач не отримує відповідь, а вартість run'а росте з кожним повтором.
Система не падає.
Вона просто множить однакові виклики і тихо спалює бюджет.
Аналогія: уяви оператора підтримки, який натискає redial на той самий номер, замість того щоб ескалувати задачу або змінити план. Він зайнятий, але проблема не рухається. Tool spam в агентах виглядає так само: дій багато, корисного прогресу мало.
Чому це стається
Tool spam виникає не тому, що агент "занадто старається", а тому, що runtime не відрізняє нову корисну дію від дубля без прогресу.
У production зазвичай так:
- LLM обирає
tool_call; - інструмент повертає нестабільний або недостатній сигнал;
- агент повторює той самий виклик (або майже той самий);
- без dedupe, budget gates і єдиної retry policy цикл розростається.
Проблема не в одному конкретному інструменті. Проблема в тому, що система не обмежує повторні виклики до того, як вони стають інцидентом.
Які збої трапляються найчастіше
Щоб не ускладнювати, у production найчастіше бачать чотири патерни tool spam.
Повтори того самого signature (Repeated signature spam)
Агент викликає той самий tool з тими самими аргументами кілька разів поспіль.
Типова причина: немає dedupe за tool+args_hash у межах run.
Косметичні зміни аргументів (Argument jitter spam)
Змінюється лише дрібниця в аргументах: регістр, пробіл, порядок слів. Семантично це той самий запит, але система сприймає його як новий.
Типова причина: немає нормалізації аргументів перед dedupe.
Множення ретраїв між шарами (Retry amplification)
Ретраї робить і агент, і gateway, і сам SDK інструмента. Один збій перетворюється на серію дубльованих викликів.
Типова причина: retry policy розмазана по кількох місцях.
Неконтрольований fan-out (Fan-out spam)
Один крок агента запускає багато паралельних викликів без жорсткого ліміту. Навіть без циклу це швидко перевантажує зовнішні API.
Типова причина: немає bounded fan-out і per-tool caps.
Як виявляти ці проблеми
Tool spam добре видно по поєднанню runtime і gateway-метрик.
| Метрика | Сигнал tool spam | Що робити |
|---|---|---|
tool_calls_per_task | різкий ріст викликів на один run | поставити max_tool_calls і per-tool caps |
repeated_tool_signature_rate | часті повтори tool+args у межах run | додати dedupe window і кеш короткого життя |
unique_signature_ratio | падає частка унікальних викликів | додати no-progress правило на N кроків |
retry_amplification_rate | ретраї дублюються між шарами | централізувати retry policy в одному gateway |
cost_per_run | ціна run'а росте без приросту якості | вмикати budget gate і kill switch на проблемний інструмент |
Як відрізнити tool spam від справді широкого пошуку
Не кожна велика кількість викликів означає збій. Ключове питання: чи додає кожен виклик новий корисний сигнал.
Нормально, якщо:
- нові
tool_callреально відкривають нові джерела або факти; unique_signature_ratioтримається стабільно;- витрати ростуть разом із якістю відповіді.
Небезпечно, якщо:
- повторюється та сама signature або майже та сама;
- 3-5 кроків підряд не дають нової інформації;
- вартість і latency ростуть, а відповідь не стає кращою.
Як зупиняти такі збої
Практично це виглядає так:
- ставиш
max_tool_callsна run і окремі ліміти на кожен інструмент; - додаєш dedupe за
tool+args_hashз коротким вікном; - тримаєш retry policy тільки в gateway (з чітким списком non-retryable помилок);
- при дублях або перевищенні ліміту повертаєш cached/partial результат і stop reason.
Мінімальний guard для контролю повторних викликів:
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
Це базовий guard: у production перед args_hash часто додають доменну нормалізацію
(trim/lowercase/collapse spaces для тексту, а для окремих полів — canonical ordering),
а on_tool_call(...) викликають перед фактичним tool виконанням, щоб зупиняти дубль ще до зайвого зовнішнього виклику.
Де це реалізується в архітектурі
Контроль tool spam у production зазвичай лежить у трьох шарах.
Agent Runtime відповідає за ліміти run'а,
stop reasons, no-progress правила і кероване завершення.
Саме тут зазвичай фіксують budget:tool_calls і tool_spam:*.
Tool Execution Layer відповідає за dedupe, retry policy, короткий кеш і нормалізацію помилок інструментів. Якщо цей шар слабкий, spam швидко розповзається по всьому workflow.
Policy Boundaries задає, які інструменти можна викликати, як часто і за яких умов. Це дозволяє обмежувати ризикові інструменти ще до виконання виклику.
Самоперевірка
Швидка перевірка перед релізом. Відмічайте пункти і дивіться статус нижче.
Це короткий sanity-check, а не формальний аудит.
Прогрес: 0/8
⚠ Є сигнали ризику
Бракує базових контролів. Закрийте ключові пункти цього чекліста перед релізом.
FAQ
Q: Чи достатньо лише max_steps?
A: Ні. Один крок агента може містити кілька tool_call, тому потрібен окремий ліміт саме на інструменти.
Q: Дедуп не вбиває freshness?
A: Ні, якщо дедуп короткий і scoped per run. Його мета — прибрати шумові дублікати, а не кешувати "стару правду" надовго.
Q: Де мають жити retries?
A: У єдиному choke point, зазвичай у tool gateway. Там же варто явно відсікати non-retryable помилки: 401, 403, 404, schema validation errors і policy denials зазвичай завершують run одразу.
Q: Що показувати користувачу, якщо run зупинено через spam?
A: Причину зупинки, що вже встигли перевірити, і безпечний наступний крок (fallback або ручна ескалація).
Tool spam майже ніколи не виглядає як гучна аварія.
Це повільне роздування викликів, latency і витрат, яке добре видно лише у трейсах.
Тому production-агентам потрібні не лише кращі моделі, а й жорсткий контроль tool_call на рівні runtime і gateway.
Пов'язані сторінки
Щоб глибше закрити цю проблему, подивись:
- Чому AI агенти ламаються — загальна карта збоїв у production.
- Infinite loop — як зациклення швидко переходить у повтори викликів.
- Budget explosion — як spam інструментами роздуває витрати.
- Tool failure — як нестабільний інструмент провокує хвилі ретраїв.
- Agent Runtime — де ставити stop reasons і execution-ліміти.
- Tool Execution Layer — де тримати dedupe, retries і контроль викликів.