Таймаут tool-call у агента
Когда AI-агент «завис», первое подозрение — на модель. Но чаще виноват инструмент: function-call ушёл в ваш код (запрос к БД, HTTP к внешнему API, shell-команда), и там что-то повисло. Модель ждёт результата тула и со стороны выглядит намертво зависшей, хотя сама она в порядке — стоит конкретный tool call.
Инструментируем исполнитель тулов: меряем время каждого вызова и шлём push, когда тул повторно выбивает свой бюджет.
Оборачиваем исполнитель тулов таймаутом
Заголовок раздела «Оборачиваем исполнитель тулов таймаутом»Каждый тул исполняется в потоке с дедлайном; таймауты копятся по имени тула, и при повторных срабатываниях идёт push — «висит именно этот инструмент».
import os, time, collections, requestsfrom concurrent.futures import ThreadPoolExecutor, TimeoutError as FTimeout
NOTIFLY_URL = os.environ["NOTIFLY_URL"]NOTIFLY_TOKEN = os.environ["NOTIFLY_TOKEN"]
TIMEOUTS = {"db_query": 5, "web_search": 10, "run_shell": 30} # бюджет, секDEFAULT = 15THRESHOLD = 3 # столько таймаутов одного тула = алёртWINDOW_SEC = 600
_pool = ThreadPoolExecutor(max_workers=8)_timeouts = collections.defaultdict(collections.deque) # tool -> timestamps
def notify(title, msg, prio): requests.post(f"{NOTIFLY_URL}/message", params={"token": NOTIFLY_TOKEN}, json={"title": title, "message": msg, "priority": prio}, timeout=5)
def _record_timeout(tool, secs): now = time.time() dq = _timeouts[tool] dq.append(now) while dq and dq[0] < now - WINDOW_SEC: dq.popleft() if len(dq) >= THRESHOLD: notify( f"🧰 Тул '{tool}' виснет", f"{len(dq)} таймаутов за {WINDOW_SEC // 60} мин " f"(бюджет {secs}с). Агент выглядит зависшим, но дело в инструменте.", priority=8, ) dq.clear() # сбрасываем, чтобы не спамить, пока не накопится снова
def run_tool(name, fn, *args, **kwargs): """Исполнить тул с дедлайном; при таймауте — учесть и пробросить.""" budget = TIMEOUTS.get(name, DEFAULT) fut = _pool.submit(fn, *args, **kwargs) try: return fut.result(timeout=budget) except FTimeout: _record_timeout(name, budget) raise TimeoutError(f"tool '{name}' превысил {budget}s")
# использование внутри цикла агента, когда модель попросила вызвать тулdef dispatch(tool_call): return run_tool(tool_call.name, TOOLS[tool_call.name], **tool_call.args)Бюджет — per-tool (TIMEOUTS): у поиска в вебе и у запроса к БД разные
нормальные времена, единый порог бесполезен. Алёрт идёт по повторам
(THRESHOLD), а не на первый таймаут: разовый медленный вызов — норма,
а вот три подряд — это уже «инструмент сломан».
Отличайте «тул виснет» от «агент зациклился»
Заголовок раздела «Отличайте «тул виснет» от «агент зациклился»»Таймаут тула — это про внешний инструмент. Если же сам цикл агента крутится и жжёт токены без прогресса, это другой кейс — зависший агент / loop через heartbeat. Ставьте оба: один ловит «инструмент повис», другой — «модель ходит по кругу».
MCP-инструменты — частый виновник
Заголовок раздела «MCP-инструменты — частый виновник»Если тулы приходят из MCP-сервера, зависание почти всегда в нём (сеть, сторонний API за ним). Дополните это мониторингом самого MCP — здоровье MCP-сервера — чтобы отличить «повис конкретный вызов» от «весь MCP-сервер лёг».
Что положить в текст алёрта
Заголовок раздела «Что положить в текст алёрта»- имя тула и его бюджет времени;
- число таймаутов за окно;
- аргументы последнего зависшего вызова (осторожно с секретами);
- какой шаг агента его дёрнул — чтобы воспроизвести.