Как вы реализуете 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) |
|---|---|
| 1 | 1 с |
| 2 | 2 с |
| 3 | 4 с |
| 4 | 8 с |
| 5 | 16 с |
| 6 | 16 с (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_delay | 1–2 с | Слишком маленькая (0.1 с) — не даст серверу восстановиться; большая (10 с) — замедлит пользователя. |
| multiplier | 2 | Классический компромисс между скоростью и вежливостью. |
| max_delay | 16–60 с | Для публичных API (OpenAI) 16 с достаточно; для локальных моделей можно меньше. |
| jitter | 0–1 с (или 10% от задержки) | Снижает вероятность одновременных retry у многих клиентов. |
| max_attempts | 3–5 | Больше 5 — пользователь ждёт слишком долго; меньше 3 — не хватает для временных сбоев. |
6. Обработка ошибок: какие исключения ловить
Для OpenAI:
- openai.RateLimitError (HTTP 429)
- openai.APITimeoutError (таймаут)
- openai.APIConnectionError (сетевая ошибка)
- openai.InternalServerError (HTTP 500, редко)
Для других провайдеров (Anthropic, Cohere) — аналогичные классы. Важно не повторять ошибки, которые не имеют смысла повторять (например, openai.AuthenticationError — неверный ключ).
7. Fallback-стратегии
Когда все попытки исчерпаны, нужно решить, что делать:
- Пробросить ошибку — просто выбросить исключение, пусть вызывающий код решает.
- Вернуть кэшированный ответ — если запрос похож на предыдущий, можно отдать старый результат (риск устаревания).
- Переключиться на другую модель — например, с GPT-4 на GPT-3.5-turbo (дешевле, быстрее, но менее качественно).
- Использовать локальную модель — если есть локальный LLM (например, Llama 3), можно отправить запрос туда.
- Сообщить пользователю — вернуть сообщение «Сервис временно недоступен, попробуйте позже».
Пример 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)tenacityasyncioollama(локальный LLM)
Шаги:
- Создать асинхронную функцию
call_llm(prompt, model="gpt-4")с декоратором tenacity (3 попытки, backoff 1–8 с, jitter 0.5 с). - Создать fallback-функцию
call_ollama(prompt). - Написать агента, который в цикле отправляет 10 запросов, используя
asyncio.gather(параллельно). - Добавить логирование каждой попытки.
- Протестировать: искусственно ограничить скорость через
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 | Как вы тестируете устойчивость агента к сбоям? |
Навигация
- Предыдущий: 237
- Следующий: 239
- Индекс: 00. Индекс разборов