Повторные запросы к API LLM: как не удвоить стоимость и не потерять результат

Разбираем, когда повторять запрос к API языковой модели, как настроить экспоненциальную задержку и почему тайм-аут не всегда означает, что генерация не состоялась.

Схема повторного запроса к API языковой модели при тайм-ауте и ограничении частоты
Схема повторного запроса к API языковой модели при тайм-ауте и ограничении частоты
Visit of Ursula von der Leyen, President of the European Commission, to India (P-065755-00-36).jpg | by Europäische Kommission — Audiovisueller Dienst, CE — Service audiovisuel, EC — Audiovisual Service, Dati Bendo | wikimedia_commons | CC BY 4.0

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

Ниже — практический разбор для разработчика, который вызывает модели через HTTP API. Схема подходит для обычной генерации текста; для инструментальных вызовов и действий с побочными эффектами понадобятся дополнительные меры.

Повторные запросы к API LLM: сначала классифицируйте сбой

Не всякий неуспешный ответ означает, что стоит отправить тот же запрос снова. В документации OpenAI ошибки разделены по классам: например, `429` может указывать на ограничение частоты запросов или исчерпание квоты, а `401` — на проблему аутентификации. У Anthropic также различаются ошибки ограничения частоты, перегрузки и некорректного запроса. Gemini API документирует отдельные ответы, включая `429`, `500` и `503`.

Практическая классификация выглядит так:

  • Повторяемые временные сбои: перегрузка сервиса, временная недоступность, некоторые сетевые ошибки. Обычно это `5xx`, но конкретное поведение зависит от провайдера.
  • Ограничение частоты: `429`. Повтор может быть уместен после ожидания, но немедленная повторная отправка усугубит перегрузку.
  • Ошибки запроса: некорректные параметры, неподдерживаемая функция или превышение допустимого размера. Повтор без изменений обычно бесполезен.
  • Проблемы доступа и квоты: неверный ключ, отсутствие разрешения либо исчерпанный лимит расходов. Нужно исправить настройку или дождаться восстановления квоты, а не запускать цикл ретраев.
  • Тайм-аут без ответа: результат неоднозначен. Клиенту неизвестно, успел ли сервер выполнить генерацию.

Ориентируйтесь на код ответа и тело ошибки конкретного API, а не только на текст исключения в SDK. Клиентская библиотека может преобразовать сетевую ошибку в собственный тип исключения; кроме того, провайдеры не обязаны одинаково трактовать все коды.

Почему тайм-аут не доказывает, что запрос не выполнен

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

Это принципиально отличается от ситуации, когда запрос не дошёл до сервера. В обоих случаях клиент может увидеть сетевую ошибку, но последствия для стоимости и состояния различны. Не считайте отсутствие ответа подтверждением отсутствия выполнения.

Для обычного текстового запроса повтор чаще всего создаёт дублирующий результат и потенциальный дополнительный расход. Для tool calling риск выше: если модель предложила вызвать инструмент, а приложение выполнило действие, повторный цикл может привести к повторному действию, если обработчик не защищён от дубликатов.

Общее правило из практик распределённых систем — считать сетевые сбои неоднозначными и проектировать повторы с учётом возможной повторной обработки. AWS отдельно разбирает тайм-ауты, повторы и backoff; эти принципы полезны и при работе с модельными API, хотя они не гарантируют одинаковой семантики у разных поставщиков.

Экспоненциальная задержка и случайный разброс

Если сотни клиентов повторяют запрос ровно через секунду после ошибки, они могут одновременно создать новую волну нагрузки. Чтобы этого избежать, используют экспоненциальную задержку: интервал между попытками растёт. Случайный разброс, или jitter, распределяет повторы по времени.

Упрощённая формула для задержки перед попыткой:

text
delay = случайное_число(0, min(максимум, база × 2^номер_попытки))

Например, при базе 0,5 секунды и максимуме 8 секунд верхняя граница задержки последовательно возрастает: 0,5; 1; 2; 4; затем остаётся 8 секунд. Случайное значение внутри каждого диапазона помогает избежать синхронных повторов.

Это не универсальные значения: их нужно подобрать под ограничения API, допустимую задержку интерфейса и тип задачи. Если сервер присылает `Retry-After`, проверьте документацию провайдера и учитывайте это значение, а не игнорируйте его в пользу собственного короткого таймера. Также задайте общий срок операции: пять попыток с длинными паузами могут быть неприемлемы для интерактивного чата, но допустимы для фоновой очереди.

Ограничьте попытки, время и бюджет

Политика повторов должна иметь несколько пределов:

Максимум попыток. Не позволяйте запросу повторяться бесконечно.

Общий deadline. Прекратите операцию, когда истёк срок, приемлемый для пользователя или фоновой задачи.
3. Ограничение параллелизма. При перегрузке новые запросы не должны бесконтрольно накапливаться.
4. Бюджет повторов. Считайте число дополнительных вызовов и их стоимость отдельно от исходных.
5. Условие остановки. Не повторяйте ошибки, которые требуют исправить запрос, ключ или конфигурацию.

Для пользовательского интерфейса полезно вернуть понятное состояние: «временно не удалось получить ответ» — вместо того, чтобы скрыто ждать неопределённое время. Для фоновой задачи можно поставить повтор в очередь, но с ограничением срока жизни и ясным статусом выполнения.

Особенно внимательно проверьте SDK: некоторые клиенты повторяют отдельные ошибки автоматически. Если поверх этого реализован собственный цикл, фактическое число запросов может оказаться выше ожидаемого. Уточните настройки повторов в используемой версии библиотеки и проверьте их тестом или журналами.

Сделайте повторяемые действия идемпотентными

Идемпотентность означает, что повтор одного и того же действия не создаёт повторного эффекта. Для чистой генерации текста она не предотвращает повторную оплату, но для приложения с базой данных или внешними инструментами помогает не выполнить действие дважды.

Пример: модель предлагает создать тикет. Вместо немедленного вызова внешнего сервиса сохраните операцию с собственным идентификатором, например `operation_id`. Перед созданием тикета проверьте, обрабатывался ли этот идентификатор. Если запрос повторился после сетевого сбоя, обработчик сможет вернуть уже созданный результат, а не создать второй тикет.

Важно различать идентификатор вашей бизнес-операции и поддержку idempotency key со стороны провайдера модели. Не предполагайте, что любой API гарантирует дедупликацию вызовов: проверяйте официальную документацию конкретного сервиса. Даже если модельный запрос можно безопасно повторить технически, его повтор всё равно может означать новую генерацию и новые расходы.

Для инструментальных сценариев полезно отделять получение предложения модели от исполнения действия: сохранить ответ и параметры вызова, проверить их, затем выполнить операцию через защищённый от дубликатов обработчик.

Минимальная проверка политики ретраев

Перед внедрением проверьте поведение на трёх типах ситуации: временный `5xx`, ответ `429` и тайм-аут после отправки запроса. В тестовой среде или через подменённый HTTP-клиент можно имитировать ответы и записывать, сколько раз вызывается транспорт.

Проверка должна подтвердить, что:

  • запрос с ошибкой параметров не повторяется без изменения данных;
  • временная ошибка вызывает ограниченное число попыток с растущими интервалами;
  • `429` не приводит к немедленной лавине запросов;
  • по истечении общего deadline задача прекращается;
  • при неоднозначном тайм-ауте операция помечается как неизвестная, если провайдер не даёт надёжного способа проверить результат;
  • инструментальное действие не исполняется повторно при повторной доставке одной операции.

В журналах фиксируйте провайдера, модель, код ответа, номер попытки, задержку, длительность и итоговый статус. Не записывайте ключи API и чувствительное содержимое промпта. Такая телеметрия помогает отличить реальную нестабильность сервиса от ошибок конфигурации и увидеть, сколько вызовов добавляют ретраи.

Когда лучше не повторять запрос

Если пользователь получил частичный результат, повторная генерация может заменить полезный ответ другим. Если модель уже предложила действие, повтор способен породить новый набор аргументов. А при исчерпанной квоте дополнительная попытка не исправит ситуацию.

Поэтому при неопределённом исходе выбирайте явное состояние, например «результат не подтверждён», и решайте, что делать дальше: проверить состояние операции, запросить подтверждение пользователя или предложить повтор вручную. Перед запуском в продакшене сверьте актуальные коды ошибок, правила `Retry-After` и настройки повторов именно для используемой версии API и SDK.

Источники