Агент долбится в падающий tool
Агент вызывает tools, часть вызовов падает — это норма: сеть моргнула, rate-limit, случайный 500. Модель ретраит, и обычно всё чинится само. Проблема начинается, когда тот же tool падает раз за разом: протухла авторизация, у провайдера деградация, endpoint отдаёт 500 стабильно. Агент не понимает, что дело не в аргументах, и упорно долбится в стену — жжёт токены (см. runaway бюджета) и никуда не двигается.
Нужен трекер, который в пределах одного run-а считает долю ошибок по каждому tool-у и ловит момент, когда падения перестали быть случайными.
Подход 1: error-rate + серия подряд
Заголовок раздела «Подход 1: error-rate + серия подряд»Оборачиваем выполнение tool-а. Копим по имени инструмента: сколько всего вызовов, сколько ошибок, сколько фейлов подряд. Пушим на двух триггерах — либо N ошибок подряд (persistent), либо доля ошибок пробила порог на достаточном числе вызовов.
import os, requests, collections
class ToolErrorTracker: def __init__(self, run_id, streak=4, rate=0.5, min_calls=6): self.run_id = run_id self.streak = streak # столько фейлов подряд = persistent self.rate = rate # доля ошибок, которую считаем аварией self.min_calls = min_calls # не паникуем на малой выборке self.total = collections.Counter() self.errors = collections.Counter() self.consec = collections.Counter() self._fired = set() # чтобы не спамить по одному tool-у
def record(self, tool, ok, detail=""): self.total[tool] += 1 if ok: self.consec[tool] = 0 return self.errors[tool] += 1 self.consec[tool] += 1
if tool in self._fired: return
# persistent: одно и то же падает подряд if self.consec[tool] >= self.streak: self._fired.add(tool) notify("🛠️❌ Tool падает подряд", f"run {self.run_id}: {tool} упал {self.consec[tool]} раз " f"подряд. {detail} Похоже на persistent-сбой.", priority=9) return
# rate: много ошибок на заметной выборке n, e = self.total[tool], self.errors[tool] if n >= self.min_calls and e / n >= self.rate: self._fired.add(tool) notify("🛠️⚠️ Высокий error-rate у tool-а", f"run {self.run_id}: {tool} — {e}/{n} ошибок " f"({e/n:.0%}). Агент буксует.", priority=7)
def notify(title, message, priority): requests.post(f"{os.environ['NOTIFLY_URL']}/message", params={"token": os.environ["NOTIFLY_TOKEN"]}, json={"title": title, "message": message, "priority": priority}, timeout=5)Встраивание — обёртка вокруг вызова инструмента:
tracker = ToolErrorTracker(run_id="refactor-auth-2026-07-01")
def run_tool(tool, **kwargs): try: result = tool.execute(**kwargs) tracker.record(tool.name, ok=True) return result except Exception as e: tracker.record(tool.name, ok=False, detail=str(e)[:120]) raiseДва порога не случайны: streak ловит быстрый явный сбой (авторизация
протухла — сыпет ошибками мгновенно), rate ловит «плавающий» tool,
который падает через раз и медленно съедает run.
Подход 2: отличаем transient от persistent
Заголовок раздела «Подход 2: отличаем transient от persistent»Не каждая ошибка достойна push-а. 429 (rate-limit) и 503 — обычно
transient, само пройдёт после backoff-а. А вот 401/403 (авторизация),
стабильный 500 или ConnectionRefused почти всегда persistent — ретраить
бесполезно, нужен человек.
TRANSIENT = {408, 429, 502, 503, 504} # ретрай уместенPERSISTENT = {400, 401, 403, 404, 500} # ретрай не поможет
def classify(status): if status in PERSISTENT: return "persistent" if status in TRANSIENT: return "transient" return "unknown"
def record_http(tracker, tool, status): kind = classify(status) tracker.record(tool, ok=(status < 400), detail=f"HTTP {status} ({kind})") # persistent-код — не ждём серии, бьём тревогу сразу if kind == "persistent" and tool not in tracker._fired: tracker._fired.add(tool) loud = 9 if status in (401, 403) else 8 notify("🛠️🔒 Tool отдаёт persistent-ошибку", f"run {tracker.run_id}: {tool} → HTTP {status}. " f"{'Протухла авторизация?' if status in (401,403) else 'Endpoint лежит.'}", priority=loud)Смысл: на 401 бессмысленно ждать четырёх фейлов подряд — токен не
восстановится сам, буди человека с первого раза. А на 429 промолчи, пусть
backoff отработает.
Подход 3: внешний угол — без правки кода агента
Заголовок раздела «Подход 3: внешний угол — без правки кода агента»Если агент — чёрный ящик (сторонний CLI, чужой SDK) и обернуть вызовы нельзя, ловите ошибки снаружи. Многие tool-ы (API-шлюзы, MCP-серверы) уже шлют вебхуки при 5xx — направьте их в Webhook Router, а он превратит всплеск ошибок в push. Если вебхуков нет, а есть health-check у tool-а — повесьте на него Monitor: endpoint инструмента красный → push, и вы уже знаете, почему run буксует, ещё до его завершения.
Точность ниже (это про сам сервис, а не про конкретный run), зато работает для любого агента без единой строчки правок.
Что положить в push
Заголовок раздела «Что положить в push»run_idи имя падающего tool-а — чтобы найти в логах;- transient или persistent, HTTP-код / текст последней ошибки;
- сколько фейлов подряд / доля ошибок (
5/8,62%); - намёк на действие:
обнови токен,провайдер лежит — поставь run на паузу.
Связанные рецепты
Заголовок раздела «Связанные рецепты»- Агент проедает бюджет за один run — retry-шторм по падающему tool-у жжёт деньги.
- Зависший агент / loop — соседний симптом: агент крутит один и тот же вызов.
- Multi-agent deadlock — когда падающий tool блокирует всю оркестрацию.