Управление квотами и оптимизация задержки (Latency)
Введение: Почему "работает на моем ноутбуке" недостаточно
Добро пожаловать в седьмой модуль курса. Мы переходим от написания кода к LLMOps — операционной деятельности по поддержке LLM в продакшене. Два самых страшных врага любого инженера, внедряющего AI в корпоративную среду, — это Rate Limits (ограничения частоты запросов) и Latency (задержка ответа).
Представьте ситуацию: вы создали гениального ассистента на базе Gemini 3. На демо-презентации всё летало. Но как только вы открыли доступ для 500 сотрудников компании, начался хаос. Половина запросов падает с ошибкой 429 Resource Exhausted, а те, что проходят, заставляют пользователей ждать по 15 секунд, глядя на пустой экран. Результат? Пользователи возвращаются к старым инструментам, а бизнес считает проект провальным.
В этом уроке мы разберем:
- Как элегантно обрабатывать квоты (RPM/TPM) и не терять запросы.
- Что такое Exponential Backoff и Jitter, и почему без них нельзя идти в прод.
- Анатомию задержки: TTFT (Time To First Token) против общей генерации.
- Техники оптимизации скорости: от кэширования контекста до стриминга.
Часть 1: Управление квотами (Rate Limiting)
Google Cloud, как и любой провайдер API, накладывает ограничения, чтобы защитить свою инфраструктуру. В Gemini API существуют три основные метрики квот:
- RPM (Requests Per Minute): Количество запросов в минуту. Это защита от DDOS и спама.
- TPM (Tokens Per Minute): Количество токенов (входных + выходных) в минуту. Это ограничение вычислительной мощности.
- RPD (Requests Per Day): Общее количество запросов в сутки (чаще встречается на бесплатных тарифах).
Ошибка 429: Resource Exhausted
Когда вы превышаете любой из этих лимитов, API возвращает статус 429. Новичок просто покажет ошибку пользователю. Профессионал реализует стратегию повторных попыток (Retry Strategy).
Почему нельзя просто делать `while True: try`?
Если ваш сервис мгновенно повторяет запрос после ошибки, вы создаете эффект Thundering Herd (несущегося стада). Если API перегружено, ваши мгновенные повторы только ухудшают ситуацию, и Google может временно заблокировать ваш ключ.
Стратегия: Exponential Backoff и Jitter
Золотой стандарт обработки ошибок 429 в распределенных системах состоит из двух компонентов:
- Exponential Backoff (Экспоненциальная задержка): Время ожидания между попытками увеличивается экспоненциально. Например: 1 сек, 2 сек, 4 сек, 8 сек, 16 сек. Это дает системе время на восстановление.
- Jitter (Джиттер / Случайный шум): Добавление небольшой случайной величины к времени ожидания. Если 100 ваших микросервисов получили ошибку одновременно и все они подождут ровно 2 секунды, они снова ударят по API одновременно через 2 секунды. Джиттер (например, 2 сек + 0.13 сек) «размазывает» нагрузку по времени.
Формула идеальной задержки выглядит так:sleep_time = min(cap, base * 2 ** attempt) + random_between(0, 1)
import time
import random
import google.generativeai as genai
from google.api_core import exceptions
# Настройка модели (предполагается, что API ключ уже задан)
model = genai.GenerativeModel('gemini-1.5-pro-latest')
def generate_with_backoff(prompt, max_retries=5, base_delay=1.0):
"""
Выполняет запрос к Gemini с экспоненциальной задержкой и джиттером.
"""
for attempt in range(max_retries):
try:
# Попытка выполнить запрос
response = model.generate_content(prompt)
return response.text
except exceptions.ResourceExhausted as e:
# Если это 429 ошибка (лимиты)
if attempt == max_retries - 1:
print(f"Достигнут лимит попыток. Ошибка: {e}")
raise
# Вычисляем время задержки
# Формула: 2^attempt + случайный шум (Jitter)
sleep_time = (base_delay * (2 ** attempt)) + random.uniform(0, 1)
print(f"Лимит исчерпан (429). Попытка {attempt + 1} не удалась.")
print(f"Ждем {sleep_time:.2f} секунд перед повтором...")
time.sleep(sleep_time)
except Exception as e:
# Другие ошибки (например, 500) можно обрабатывать иначе
print(f"Произошла непредвиденная ошибка: {e}")
raise
# Пример использования
try:
result = generate_with_backoff("Объясни квантовую запутанность для пятиклассника.")
print("Результат:", result[:100], "...")
except Exception:
print("Не удалось получить ответ от модели.")
Продвинутый уровень: Клиентский Rate Limiting
Выше мы рассмотрели реактивный подход (ждем ошибку, потом реагируем). В Enterprise системах часто используют проактивный подход. Вы сами считаете токены и запросы, не отправляя их в Google, если лимит близок.
Алгоритм Token Bucket (Маркерная корзина):
Представьте ведро, в которое каждую секунду капает вода (ваша квота). Когда вы делаете запрос, вы зачерпываете воду. Если ведро пустое — вы ждете, не отправляя запрос в API. Для реализации в распределенных системах (когда у вас много серверов делают запросы) обычно используют Redis для хранения общего счетчика токенов.
Часть 2: Оптимизация Latency (Задержки)
Скорость — это деньги. В LLM задержка делится на две критические метрики:
- TTFT (Time To First Token): Время от нажатия кнопки "Отправить" до появления первого символа ответа. Это самая важная метрика для восприятия пользователя. Если TTFT > 2 секунд, пользователь начинает думать, что приложение зависло.
- Total Latency (Общее время): Время до полного завершения генерации ответа.
Зависимость простая: Входные токены влияют на TTFT. Выходные токены влияют на Total Latency.
Методы снижения задержки
1. Стриминг (Streaming)
Стриминг не делает модель быстрее технически, но он делает её мгновенной психологически. Вместо ожидания всего ответа (например, 5 секунд), пользователь видит первый токен через 0.5 секунды. В Gemini API это реализуется параметром stream=True.
2. Context Caching (Кэширование контекста)
Уникальная фича Gemini. Если у вас есть огромный системный промпт (например, книга правил компании на 500 страниц) и вы отправляете его с каждым запросом:
- Вы платите за обработку этих токенов каждый раз.
- Модель тратит время на их "чтение" каждый раз (увеличивая TTFT).
С Context Caching вы загружаете контекст один раз, получаете ID и ссылаетесь на него. Это драматически снижает TTFT для больших входных данных (Input Latency).
3. Выбор правильной модели
Gemini 1.5 Flash значительно быстрее (и дешевле) версии Pro. Для 80% задач (классификация, саммаризация простых текстов, извлечение сущностей) Flash справляется отлично. Используйте Pro только там, где нужны сложные рассуждения.
# Пример использования Streaming для улучшения UX
import time
def stream_response_example(prompt):
print(f"Запрос: {prompt}")
print("Ответ: ", end="", flush=True)
start_time = time.time()
first_token_received = False
# stream=True возвращает генератор
response = model.generate_content(prompt, stream=True)
for chunk in response:
# Замер TTFT
if not first_token_received:
ttft = time.time() - start_time
first_token_received = True
# В реальном приложении мы бы логировали это в систему мониторинга
# print(f"\n[DEBUG] TTFT: {ttft:.4f}s\n")
# Выводим части ответа по мере поступления
print(chunk.text, end="", flush=True)
total_time = time.time() - start_time
print(f"\n\n[DEBUG] Всего времени: {total_time:.4f}s")
# Сравните это визуально с обычным запросом, где вы ждете 3-5 секунд тишины
stream_response_example("Напиши короткое эссе о будущем искусственного интеллекта (3 абзаца).")
Оптимизация RAG-пайплайна. У вас есть система поддержки, которая перед ответом на вопрос пользователя всегда добавляет в контекст 'Базу Знаний Компании' (файл manual.txt размером 500,000 токенов). <br><br>Задача:<br>1. Опишите словами, как изменится TTFT (время до первого токена) и стоимость, если вы перейдете с обычной отправки текста на Context Caching.<br>2. Напишите псевдокод (или Python код с использованием `genai.caching`), который создает кэш для этого контента с временем жизни (TTL) 1 час.
Ваше приложение получает ошибку 429 (Resource Exhausted) при 50 запросах в минуту, хотя ваш лимит — 60 RPM. В чем, скорее всего, причина?
Резюме и лучшие практики
Управление LLM в продакшене — это баланс между стабильностью и скоростью.
- Никогда не доверяйте сети: Ошибки 429 будут случаться. Ваш код должен ожидать их и обрабатывать через Backoff.
- Следите за TPM, а не только RPM: Длинные промпты съедают квоту быстрее, чем частые короткие запросы.
- Кэшируйте всё, что статично: Если контекст не меняется между пользователями — используйте Context Caching.
- Стриминг — лучший друг UX: Пользователи готовы ждать завершения ответа 10 секунд, если первый токен появился через 0.5 секунды.
- Метрики: Обязательно логируйте время TTFT и Total Latency для каждого запроса. Вы не можете улучшить то, что не измеряете.