Перейти к содержимому

Таймаут tool-call у агента

Когда AI-агент «завис», первое подозрение — на модель. Но чаще виноват инструмент: function-call ушёл в ваш код (запрос к БД, HTTP к внешнему API, shell-команда), и там что-то повисло. Модель ждёт результата тула и со стороны выглядит намертво зависшей, хотя сама она в порядке — стоит конкретный tool call.

Инструментируем исполнитель тулов: меряем время каждого вызова и шлём push, когда тул повторно выбивает свой бюджет.

Каждый тул исполняется в потоке с дедлайном; таймауты копятся по имени тула, и при повторных срабатываниях идёт push — «висит именно этот инструмент».

import os, time, collections, requests
from 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 = 15
THRESHOLD = 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-сервера, зависание почти всегда в нём (сеть, сторонний API за ним). Дополните это мониторингом самого MCP — здоровье MCP-сервера — чтобы отличить «повис конкретный вызов» от «весь MCP-сервер лёг».

  • имя тула и его бюджет времени;
  • число таймаутов за окно;
  • аргументы последнего зависшего вызова (осторожно с секретами);
  • какой шаг агента его дёрнул — чтобы воспроизвести.