English translation is not available yet. Showing Russian content.

Как вы реализуете retry с exponential backoff для LLM API с rate limit?

Краткий тезис

Retry с backoff|exponential backoff — это стратегия повторных попыток вызова API с увеличивающейся задержкой между ними, необходимая для корректной обработки rate limit (ограничения частоты запросов) и временных сбоев. В контексте LLM API (OpenAI, Anthropic, локальные модели) такая реализация критична, так как агентные RAG-системы совершают десятки последовательных вызовов, и без backoff они быстро получат ошибку 429 (Too Many Requests). Практическая реализация на Python обычно использует библиотеку tenacity с параметрами: начальная задержка 1 с, множитель 2, максимальная задержка 16 с, случайный jitter (дрожание) 0–1 с, и после 3–5 неудачных попыток — fallback (запасной вариант) или проброс ошибки.


1. Термины и определения

  • Rate limit — ограничение на количество запросов к API за единицу времени (например, 10 запросов в минуту). При превышении сервер возвращает HTTP-статус 429 (Too Many Requests) или 5xx.
  • backoff|Exponential backoff — алгоритм, при котором задержка между повторными попытками растёт экспоненциально: delay = initial_delay * (multiplier ** attempt). Это предотвращает «штурм» сервера повторными запросами.
  • Jitter (дрожание) — случайное отклонение задержки (например, delay + random(0, 1)), чтобы избежать синхронизации множества клиентов (эффект «грозы»).
  • Retry — повторная попытка выполнить запрос после неудачи.
  • Fallback — запасной сценарий, если все попытки исчерпаны: вернуть кэшированный ответ, переключиться на другую модель или сообщить пользователю об ошибке.
  • Circuit breaker (предохранитель) — паттерн, который временно отключает вызовы после серии ошибок, давая системе восстановиться (часто используется вместе с retry).

2. Зачем нужен retry с exponential backoff в LLM API

LLM API (особенно публичные) имеют жёсткие rate limits. В Agentic RAG агенты могут делать множество последовательных вызовов: ретривер → LLM для генерации ответа → LLM для проверки фактов → LLM для планирования следующего шага. Без retry:

  • Одиночный сбой (например, временная перегрузка) приводит к падению всего пайплайна.
  • Повторные запросы с одинаковой задержкой создают «шторм» повторных вызовов, усугубляя проблему.
  • Exponential backoff даёт серверу время восстановиться, а jitter предотвращает коллизии.

3. Алгоритм exponential backoff

Базовая формула:
delay = min(initial_delay * (multiplier ** attempt), max_delay) + jitter

Параметры:

  • initial_delay — начальная задержка (обычно 1 с).
  • multiplier — множитель (чаще 2, реже 3).
  • max_delay — максимальная задержка (например, 16 с или 60 с).
  • jitter — случайное значение (например, от 0 до 1 с).

Пример роста задержки без jitter:

ПопыткаЗадержка (initial=1, mult=2, max=16)
11 с
22 с
34 с
48 с
516 с
616 с (cap)

С jitter (random 0–1 с) задержка будет колебаться, снижая вероятность одновременных повторных запросов.


4. Реализация на Python с библиотекой tenacity

Tenacity — самая популярная библиотека для retry в Python. Пример для OpenAI API:

import openai
from tenacity import (
    retry,
    stop_after_attempt,
    wait_exponential,
    retry_if_exception_type,
    before_sleep_log,
    after_log
)
import logging

logger = logging.getLogger(__name__)

# Настройка retry: 3 попытки, exponential backoff 1с->2с->4с, jitter 0-1с
@retry(
    stop=stop_after_attempt(3),
    wait=wait_exponential(multiplier=2, min=1, max=16) + wait_random(0, 1),
    retry=retry_if_exception_type(
        (openai.RateLimitError, openai.APITimeoutError, openai.APIConnectionError)
    ),
    before_sleep=before_sleep_log(logger, logging.WARNING),
    after=after_log(logger, logging.INFO)
)
def call_llm(prompt: str) -> str:
    response = openai.ChatCompletion.create(
        model="gpt-4",
        messages=[{"role": "user", "content": prompt}]
    )
    return response.choices[0].message.content

Пояснение

  • stop_after_attempt(3) — не более 3 попыток.
  • wait_exponential(multiplier=2, min=1, max=16) — задержка растёт от 1 до 16 с.
  • wait_random(0, 1) — добавляет jitter.
  • retry_if_exception_type(...) — повторяем только при rate limit, таймауте или ошибке соединения.
  • before_sleep_log и after_log — логирование для мониторинга.

5. Настройка параметров: что выбрать

ПараметрРекомендация для LLM APIОбоснование
initial_delay1–2 сСлишком маленькая (0.1 с) — не даст серверу восстановиться; большая (10 с) — замедлит пользователя.
multiplier2Классический компромисс между скоростью и вежливостью.
max_delay16–60 сДля публичных API (OpenAI) 16 с достаточно; для локальных моделей можно меньше.
jitter0–1 с (или 10% от задержки)Снижает вероятность одновременных retry у многих клиентов.
max_attempts3–5Больше 5 — пользователь ждёт слишком долго; меньше 3 — не хватает для временных сбоев.

6. Обработка ошибок: какие исключения ловить

Для OpenAI:

  • openai.RateLimitError (HTTP 429)
  • openai.APITimeoutError (таймаут)
  • openai.APIConnectionError (сетевая ошибка)
  • openai.InternalServerError (HTTP 500, редко)

Для других провайдеров (Anthropic, Cohere) — аналогичные классы. Важно не повторять ошибки, которые не имеют смысла повторять (например, openai.AuthenticationError — неверный ключ).


7. Fallback-стратегии

Когда все попытки исчерпаны, нужно решить, что делать:

  1. Пробросить ошибку — просто выбросить исключение, пусть вызывающий код решает.
  2. Вернуть кэшированный ответ — если запрос похож на предыдущий, можно отдать старый результат (риск устаревания).
  3. Переключиться на другую модель — например, с GPT-4 на GPT-3.5-turbo (дешевле, быстрее, но менее качественно).
  4. Использовать локальную модель — если есть локальный LLM (например, Llama 3), можно отправить запрос туда.
  5. Сообщить пользователю — вернуть сообщение «Сервис временно недоступен, попробуйте позже».

Пример fallback с переключением модели:

from tenacity import RetryError

def call_with_fallback(prompt: str) -> str:
    try:
        return call_llm(prompt)
    except RetryError:
        # fallback на более дешёвую модель
        return call_llm_fallback(prompt, model="gpt-3.5-turbo")

8. Мониторинг и логирование

Без мониторинга retry скрывает проблемы. Обязательно логировать:

  • Количество попыток.
  • Задержку перед каждой попыткой.
  • Тип ошибки.
  • Успешность после retry.

В tenacity это делается через before_sleep_log и after_log. Также можно добавить метрики в Prometheus:

from tenacity import retry, before_sleep
import prometheus_client

retry_counter = prometheus_client.Counter('llm_retries_total', 'Total LLM retries')
retry_delay_histogram = prometheus_client.Histogram('llm_retry_delay_seconds', 'Delay before retry')

def log_retry(retry_state):
    retry_counter.inc()
    retry_delay_histogram.observe(retry_state.next_action.sleep)

@retry(before_sleep=log_retry, ...)
def call_llm(prompt):
    ...

9. Продвинутые техники

9.1 Асинхронный retry (asyncio)

В Agentic RAG часто используется асинхронный код. Tenacity поддерживает asyncio:

import asyncio
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type

@retry(
    stop=stop_after_attempt(3),
    wait=wait_exponential(multiplier=2, min=1, max=16),
    retry=retry_if_exception_type(openai.RateLimitError)
)
async def async_call_llm(prompt: str) -> str:
    async with openai.AsyncOpenAI() as client:
        response = await client.chat.completions.create(...)
    return response.choices[0].message.content

9.2 Circuit breaker

Если retry не помогают (например, сервер упал надолго), circuit breaker временно блокирует вызовы. Реализация через библиотеку pybreaker:

import pybreaker
breaker = pybreaker.CircuitBreaker(fail_max=5, reset_timeout=30)

@breaker
def call_llm(prompt):
    # retry-логика внутри
    ...

9.3 Адаптивный backoff

Динамически подстраивать initial_delay на основе истории ошибок (например, если сервер часто возвращает 429, увеличить базовую задержку).


10. Связь с Agentic RAG

В агентных системах каждый шаг агента может включать вызов LLM. Без retry:

  • Агент может зависнуть на одном шаге из-за rate limit.
  • Время выполнения становится непредсказуемым.
  • Падает общая надёжность.

Правильная реализация retry с exponential backoff — это часть resilience (устойчивости) агента. Вместе с timeout (таймаутом на каждый вызов) и circuit breaker она обеспечивает стабильную работу даже при нестабильном API.


Пет-проект для закрепления

Задача Написать асинхронного агента, который задаёт 10 вопросов к OpenAI API с rate limit 5 запросов в минуту. Реализовать retry с exponential backoff и fallback на локальную модель (через Ollama).

Инструменты

  • Python 3.10+
  • openai (или httpx для кастомного API)
  • tenacity
  • asyncio
  • ollama (локальный LLM)

Шаги:

  1. Создать асинхронную функцию call_llm(prompt, model="gpt-4") с декоратором tenacity (3 попытки, backoff 1–8 с, jitter 0.5 с).
  2. Создать fallback-функцию call_ollama(prompt).
  3. Написать агента, который в цикле отправляет 10 запросов, используя asyncio.gather (параллельно).
  4. Добавить логирование каждой попытки.
  5. Протестировать: искусственно ограничить скорость через asyncio.sleep или мок-сервер.

Ожидаемый результат Агент успешно обрабатывает все 10 запросов, несмотря на rate limit. В логах видны задержки и повторные попытки. При исчерпании попыток для GPT-4 — автоматическое переключение на Ollama.


Связь с другими вопросами

ВопросТема
237Как вы обрабатываете ошибки LLM в агентном пайплайне?
239Как вы реализуете timeout для LLM вызовов?
240Как вы проектируете fallback-стратегии для LLM?
241Как вы мониторите latency и ошибки LLM API?
242Как вы реализуете circuit breaker для внешних API?
243Как вы тестируете устойчивость агента к сбоям?

Навигация