Ретраи и таймауты для 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()
Деградация вместо падения
Когда бюджет закончился, агент должен ответить, а не упасть: «Не удалось найти ответ — передали вопрос оператору». Для этого держите запасную модель подешевле и короткий сценарий без инструментов.
- Основная модель недоступна — запасная с тем же промптом.
- Инструмент поиска не ответил — ответ без цитат, с пометкой.
- Бюджет исчерпан — передача оператору и запись в лог со стоимостью шагов.