Как вы реализуете retry с exponential backoff для LLM API с rate limit?
Краткий тезис
При вызове LLM API через агента или RAG-пайплайн неизбежны ошибки rate limit (превышение лимита запросов). Retry с exponential backoff — это стратегия повторных попыток с увеличивающейся задержкой, которая снижает нагрузку на API и повышает надёжность системы. Ключевые элементы: jitter (случайное отклонение) для предотвращения thundering herd, настройка максимального числа попыток и fallback-логика (кэш, очередь, сообщение об ошибке). В Python удобно использовать библиотеку tenacity.
1. Термин: Rate limit (лимит запросов)
Rate limit — ограничение на количество запросов к API в единицу времени (например, 60 запросов в минуту). LLM-провайдеры (OpenAI, Anthropic, Google) устанавливают лимиты для защиты инфраструктуры и равномерного распределения ресурсов.
При превышении лимита API возвращает HTTP-статус 429 Too Many Requests или 503 Service Unavailable с сообщением "rate_limit" в теле ответа. Без обработки таких ошибок агент или RAG-система упадёт с исключением, что критично для production.
2. Exponential backoff: концепция и математика
Exponential backoff — алгоритм, при котором задержка между повторными попытками растёт экспоненциально. Формула:
delay = base * multiplier^(attempt - 1)
Где:
base— начальная задержка (например, 1 секунда)multiplier— множитель (обычно 2)attempt— номер попытки (начиная с 1)
Пример с base=1, multiplier=2:
- Попытка 1: 1 сек
- Попытка 2: 2 сек
- Попытка 3: 4 сек
- Попытка 4: 8 сек
Зачем экспонента Линейная задержка (1, 2, 3, 4…) не даёт API времени восстановиться при пиковой нагрузке. Экспонента даёт резерв, позволяя серверу сбросить очередь.
3. Jitter и проблема thundering herd
Thundering herd (топочущее стадо) — ситуация, когда множество клиентов одновременно получают rate limit, ждут одинаковую задержку и одновременно повторяют запрос, снова вызывая rate limit. Это создаёт лавину.
Jitter — случайное отклонение задержки, чтобы разнести повторные попытки во времени. Два подхода:
- Full jitter:
delay = random(0, exponential_delay) - Equal jitter:
delay = exponential_delay / 2 + random(0, exponential_delay / 2)
На практике чаще используют full jitter с ограничением максимума (cap). Пример: min(cap, base * 2^attempt) + random(0, 1).
Библиотека tenacity поддерживает jitter через параметр jitter.
4. Реализация на Python с tenacity
Tenacity — популярная библиотека для retry с гибкой настройкой. Установка: pip install tenacity.
Базовый пример:
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception
import openai
client = openai.OpenAI(api_key="...")
def is_rate_limit_error(exception):
"""Проверяем, что ошибка связана с rate limit."""
if isinstance(exception, openai.RateLimitError):
return True
# Альтернатива: проверка текста ошибки
if "rate_limit" in str(exception).lower():
return True
return False
@retry(
stop=stop_after_attempt(3), # максимум 3 попытки
wait=wait_exponential(multiplier=1, min=1, max=16), # задержка: 1, 2, 4, 8, 16 сек
retry=retry_if_exception(is_rate_limit_error), # ретрай только при rate limit
reraise=True # если все попытки исчерпаны, поднять исключение
)
def call_llm(prompt: str) -> str:
response = client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": prompt}]
)
return response.choices[0].message.content
Разбор параметров
| Параметр | Описание | Рекомендация |
|---|---|---|
stop | Условие остановки попыток | stop_after_attempt(3) или stop_after_delay(60) |
wait | Стратегия ожидания | wait_exponential(multiplier=1, min=1, max=16) |
retry | Условие для повторной попытки | retry_if_exception(lambda e: ...) |
jitter | Добавление случайности | jitter=wait_random(min=0, max=1) |
reraise | Пробрасывать последнее исключение | True |
Добавление jitter
from tenacity import wait_random
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=1, max=16) + wait_random(0, 1),
retry=retry_if_exception(is_rate_limit_error),
reraise=True
)
def call_llm(prompt):
...
wait_exponential(...) + wait_random(0, 1) — сумма экспоненциальной задержки и случайного числа от 0 до 1 секунды.
5. Альтернативные библиотеки и подходы
| Библиотека | Особенности |
|---|---|
backoff | Декораторы @backoff.on_exception, встроенный jitter, поддержка asyncio |
retry | Простая, но меньше опций |
| Собственная реализация | Полный контроль, но больше кода |
Пример с backoff
import backoff
import openai
@backoff.on_exception(
backoff.expo,
openai.RateLimitError,
max_tries=3,
jitter=backoff.full_jitter
)
def call_llm(prompt):
...
Асинхронный вариант (asyncio):
import asyncio
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=1, max=16),
retry=retry_if_exception(is_rate_limit_error)
)
async def call_llm_async(prompt: str) -> str:
async with openai.AsyncOpenAI() as client:
response = await client.chat.completions.create(...)
return response.choices[0].message.content
6. Fallback-стратегии
Если все попытки исчерпаны, система не должна молча падать. Fallback:
- Кэш — если запрос повторяется, вернуть ранее закэшированный ответ (если допустимо).
- Очередь — поместить запрос в очередь (например, Redis или RabbitMQ) и обработать позже.
- Сообщение об ошибке — вернуть пользователю понятное сообщение: «Сервис временно недоступен, попробуйте позже».
- Понижение модели — переключиться на более дешёвую модель с меньшим rate limit (например, с GPT-4 на GPT-3.5).
Пример с fallback:
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception
def call_llm_with_fallback(prompt: str) -> str:
try:
return call_llm(prompt)
except Exception:
# Fallback: вернуть заглушку или записать в очередь
log_error(prompt)
return "Извините, сервис временно недоступен."
7. Мониторинг и логирование retry
Важно отслеживать количество ретраев и ошибок. tenacity позволяет добавить колбэки:
from tenacity import before_log, after_log
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=1, max=16),
retry=retry_if_exception(is_rate_limit_error),
before=before_log(logger, logging.WARNING),
after=after_log(logger, logging.INFO),
reraise=True
)
def call_llm(prompt):
...
Метрики для сбора (Prometheus, Grafana):
llm_retry_count— количество ретраевllm_rate_limit_errors— количество ошибок rate limitllm_request_duration— время выполнения с учётом ретраев
8. Best practices
| Практика | Почему важна |
|---|---|
| Ограничить число попыток (3–5) | Бесконечные ретраи могут заблокировать поток |
| Использовать jitter | Предотвращает thundering herd |
| Разделять ошибки: ретрай только на rate limit, не на 400 Bad Request | Не тратить ресурсы на неисправимые ошибки |
| Настроить максимальную задержку (cap) | Не ждать слишком долго (например, 60 сек) |
| Логировать каждый ретрай | Для отладки и мониторинга |
| Тестировать с эмуляцией rate limit | Убедиться, что логика работает |
Пример обработки разных ошибок
def is_retryable(exception):
if isinstance(exception, openai.RateLimitError):
return True
if isinstance(exception, openai.APITimeoutError):
return True
# Не ретраим ошибки аутентификации или неверного запроса
return False
Пет-проект для закрепления
Задача Реализовать симулятор LLM API с rate limit и клиент с retry + exponential backoff + jitter.
Инструменты Python, tenacity, flask (для симуляции API), pytest.
Шаги:
-
Создать симулятор API на Flask:
- Эндпоинт
/chatпринимает POST сprompt. - Счётчик запросов за последние 10 секунд.
- Если превышен лимит (например, 5 запросов за 10 сек), возвращать 429 с JSON
{"error": "rate_limit"}. - Иначе возвращать фиктивный ответ.
- Эндпоинт
-
Написать клиент с
tenacity:- Использовать
wait_exponential(multiplier=1, min=1, max=8). - Добавить
jitterчерезwait_random(0, 0.5). stop_after_attempt(3).retry_if_exception(lambda e: "rate_limit" in str(e)).
- Использовать
-
Написать тесты:
- Отправить 10 запросов подряд — проверить, что часть ретраится.
- Проверить, что после 3 неудачных попыток выбрасывается исключение.
- Проверить, что jitter приводит к разным задержкам.
-
Добавить мониторинг:
- Логировать каждый ретрай с временем ожидания.
Ожидаемый результат Клиент успешно обрабатывает rate limit, не падает, а повторяет запросы с увеличивающейся задержкой. Тесты подтверждают корректность.
Связь с другими вопросами
| Вопрос | Тема |
|---|---|
| 401 | Как спроектировать агента с RAG? |
| 402 | Обработка ошибок в агентных системах |
| 403 | Управление контекстом и памятью агента |
| 406 | Как реализовать кэширование ответов LLM? |
| 410 | Мониторинг и логирование агентов |
| 736 | Общие принципы надёжности ML-систем |
Навигация
- Предыдущий: 404
- Следующий: 406
- Индекс: 00. Индекс разборов