Your Company

Ретраи и таймауты для LLM-агента: рецепты на asyncio

Автор: Кирилл Сермягин

LLM-агент делает десятки сетевых вызовов на один ответ: модель, поиск, база, внешние API. Каждый из них может зависнуть или вернуть ошибку, а наивный ретрай превращает минутный сбой провайдера в счёт на тысячи долларов. Нужны три предела: по времени, по попыткам и по деньгам.

Таймауты: на шаг и на ответ

asyncio.timeout из Python 3.11 отменяет всё, что выполняется внутри блока, и корректно работает с вложенностью. Внешний таймаут ограничивает весь ответ, внутренний — один инструмент.

async def call_tool(tool: Tool, args: dict) -> ToolResult:
    try:
        async with asyncio.timeout(10):
            return await tool(**args)
    except TimeoutError:
        return ToolResult.error(f"{tool.name}: нет ответа за 10 с")

Ретраи с бюджетом

Повторять имеет смысл только временные ошибки: 429, 500, 502, 503 и 529 «overloaded». Ошибки 400 и 401 повтор не исправит. Общий бюджет на запрос не даёт ретраям разных шагов сложиться в бесконечность.

RETRYABLE = {429, 500, 502, 503, 529}


async def with_retries(call, budget: Budget, attempts: int = 4):
    for attempt in range(attempts):
        try:
            return await budget.spend(call)
        except ProviderError as e:
            if e.status not in RETRYABLE or not budget.left:
                raise
            await asyncio.sleep(min(2 ** attempt + random.random(), 20))
    raise BudgetExceeded()

Деградация вместо падения

Когда бюджет закончился, агент должен ответить, а не упасть: «Не удалось найти ответ — передали вопрос оператору». Для этого держите запасную модель подешевле и короткий сценарий без инструментов.

  • Основная модель недоступна — запасная с тем же промптом.
  • Инструмент поиска не ответил — ответ без цитат, с пометкой.
  • Бюджет исчерпан — передача оператору и запись в лог со стоимостью шагов.