Снижение задержки (Latency) и метрика Time-to-First-Token
Урок: Снижение задержки (Latency) и метрика Time-to-First-Token
Добро пожаловать в седьмой модуль курса по Gemini 3 API. Мы переходим от функциональной разработки к тому, что отличает «домашний» прототип от высоконагруженного сервиса в продакшене — к инженерной оптимизации производительности.
В мире генеративного ИИ скорость — это не просто техническая характеристика, это фундаментальная часть пользовательского опыта (UX). Пользователи привыкли к мгновенной реакции интерфейсов. Когда ваш AI-агент «думает» 10 секунд перед ответом, магия разрушается, а пользователь начинает сомневаться в работоспособности системы.
В этом уроке мы глубоко погрузимся в анатомию сетевых запросов к LLM, разберем критически важные метрики Latency и TTFT, и научимся их оптимизировать, используя архитектурные особенности Gemini 3.
Анатомия задержки: Что происходит, когда вы нажимаете «Отправить»?
Чтобы бороться с задержкой, врага нужно знать в лицо. Давайте деконструируем жизненный цикл запросу к LLM. Общее время ожидания (Total Latency) складывается из нескольких этапов:
- Network RTT (Round Trip Time): Время, за которое ваш запрос долетает от клиента (или вашего бэкенда) до серверов Google и возвращается обратно.
- Queueing (Очередь): Время ожидания свободного слота GPU/TPU в кластере провайдера. В моменты пиковой нагрузки это может стать узким местом.
- Prefill (Обработка контекста): Модель должна «прочитать» и токенизировать весь ваш промпт, системные инструкции и историю чата. Это фаза чтения. Она параллелится, но зависит от длины входных данных.
- Decoding (Генерация): Фаза написания ответа. Это последовательный процесс: модель генерирует токен за токеном. Этот этап самый медленный, так как каждый следующий токен зависит от предыдущего.
Понимание этой структуры помогает выбрать правильную стратегию оптимизации. Если у вас медленный старт — проблема в Prefill. Если медленно пишется текст — проблема в Decoding.
Король метрик: Time-to-First-Token (TTFT)
В традиционных веб-сервисах мы смотрим на общее время ответа. В стриминговых AI-приложениях эта метрика вторична. Главный герой здесь — TTFT.
Time-to-First-Token (TTFT) — это время, прошедшее с момента отправки запроса до момента, когда пользователь увидел на экране первый символ ответа.
Почему это так важно?
- Психология восприятия: Исследования UX показывают, что задержка до 200-400 мс воспринимается как мгновенная. Если пользователь видит, что текст начал печататься через 0.5 секунды, он готов ждать окончания генерации хоть 10 секунд, потому что видит прогресс.
- Ощущение «живого» диалога: Низкий TTFT создает иллюзию, что собеседник слушает и отвечает сразу, а не уходит в библиотеку за ответом.
Вторая важная метрика — Time-Between-Tokens (TBT) или скорость генерации (токены в секунду). Она определяет, насколько быстро «бежит» текст. Для комфортного чтения скорость должна быть выше скорости чтения человека (примерно 5-10 токенов/сек).
import time
import google.genai as genai
# Настройка клиента (предполагаем, что API_KEY уже в окружении)
client = genai.Client()
def measure_performance(prompt, model_id='gemini-3-flash'):
print(f"--- Тестирование модели: {model_id} ---")
start_time = time.time()
ttft = 0
token_count = 0
first_token_received = False
# Используем стриминг для измерения TTFT
response = client.models.generate_content_stream(
model=model_id,
contents=prompt
)
for chunk in response:
if not first_token_received:
ttft = time.time() - start_time
first_token_received = True
print(f"⚡ TTFT (Time-to-First-Token): {ttft:.4f} сек.")
# В реальном коде здесь была бы логика обработки токенов
if chunk.text:
token_count += 1 # Упрощенный подсчет чанков как токенов для примера
total_time = time.time() - start_time
print(f"⏱️ Общее время: {total_time:.4f} сек.")
# Расчет средней скорости генерации (исключая время на prefill/TTFT)
generation_time = total_time - ttft
if generation_time > 0:
tps = token_count / generation_time
print(f"🚀 Скорость генерации: ~{tps:.2f} chunks/sec")
measure_performance("Напиши короткое стихотворение про скорость света.")
Стратегия 1: Выбор модели (Flash vs Pro vs Ultra)
Самый простой рычаг управления задержкой — выбор правильной модели. Семейство Gemini 3 спроектировано с учетом градации скорости и интеллекта.
- Gemini 3 Flash: Чемпион по низкому TTFT. Оптимизирована для высокой пропускной способности и мгновенных ответов. Идеальна для чат-ботов, простых задач классификации и извлечения данных.
- Gemini 3 Pro: Баланс. Используйте, когда нужны сложные рассуждения, но время ответа всё ещё критично.
- Gemini 3 Ultra: Тяжелая артиллерия. Самая высокая задержка. Используйте для оффлайн-задач, сложной аналитики или там, где качество ответа важнее скорости.
Практический совет: Используйте паттерн «Waterfall» (Водопад). Сначала попробуйте решить задачу моделью Flash. Если она не справляется (например, оценка уверенности низкая), делайте фолбэк на Pro. Это сохранит низкую среднюю задержку для 80% запросов.
Стратегия 2: Context Caching (Кэширование контекста)
Это «киллер-фича» Gemini для снижения задержки на этапе Prefill. Представьте, что вы загружаете в модель книгу на 500 страниц и задаете по ней вопросы. Без кэширования модель каждый раз заново «читает» эти 500 страниц (токенизирует и обрабатывает слои внимания).
Как это работает: Вы один раз отправляете большой контекст (документы, системные инструкции, примеры few-shot). Gemini сохраняет промежуточное состояние (KV-кэш) на своих серверах. При следующих запросах вы ссылаетесь на этот кэш. Фаза Prefill сокращается практически до нуля.
Когда применять:
1. Долгоживущие чат-боты с огромными системными промптами.
2. RAG-системы, где контекст документов не меняется часто.
3. Анализ длинных видео или аудио.
import google.genai as genai
import datetime
# Пример создания кэша для большого документа
# 1. Загружаем большой файл (условно)
# big_file = client.files.upload(path='handbook.pdf')
# 2. Создаем кэш
cache = client.caches.create(
model='models/gemini-1.5-flash-001', # Убедитесь в поддержке версии
config={
'display_name': 'Company Handbook Cache',
'ttl': '3600s', # Время жизни кэша (1 час)
'contents': [
# Ссылка на загруженный файл или большой текст
# {'role': 'user', 'parts': [{'file_data': {'file_uri': big_file.uri}}]}
{'role': 'user', 'parts': [{'text': '...ОЧЕНЬ ДЛИННЫЙ ТЕКСТ ИНСТРУКЦИЙ...'}]}
]
}
)
print(f"Кэш создан: {cache.name}")
# 3. Использование кэша в запросе
# Обратите внимание: Prefill time будет минимальным
start = time.time()
response = client.models.generate_content(
model='models/gemini-1.5-flash-001',
contents="Какая политика по отпускам?",
config={'cached_content': cache.name} # Ссылаемся на кэш
)
print(f"Ответ получен за {time.time() - start:.4f} сек: {response.text[:50]}...")
Стратегия 3: Оптимизация Промпта (Prompt Engineering)
Длина промпта напрямую влияет на TTFT (фаза Prefill), а длина желаемого ответа — на общее время (фаза Decoding). Тут работает простая математика.
- Убирайте «воду»: Инструкция «Будь вежлив и профессионален, отвечай, используя деловой стиль» занимает токены. Если модель по умолчанию вежлива, уберите это.
- Ограничивайте вывод (`max_output_tokens`): Если вам нужно просто «Да» или «Нет», жестко ограничьте генерацию 5 токенами. Это предотвратит случаи, когда модель начинает «рассуждать» после ответа, тратя время и деньги.
- Синтаксис имеет значение: Для структурированных данных (JSON) используйте режим `response_mime_type="application/json"` или `response_schema`. Модели Gemini 3 оптимизированы для генерации JSON и делают это быстрее, чем при попытке сформировать JSON через текстовые уговоры.
Стратегия 4: Semantic Caching (Ваш уровень)
Самый быстрый запрос — тот, который не дошел до LLM. Если пользователь спрашивает «Как сбросить пароль?», и вы уже отвечали на это 100 раз, нет смысла дергать Gemini.
Используйте векторную базу данных (например, Chroma, Pinecone) для хранения пар «Вопрос — Ответ».
1. Пришел запрос.
2. Векторизуем его.
3. Ищем похожий вопрос в базе (similarity > 0.9).
4. Если нашли — отдаем сохраненный ответ мгновенно (50-100 мс).
5. Если нет — идем к Gemini.
Создайте скрипт 'Latency Benchmark', который сравнивает два сценария для одной и той же задачи (например, саммаризация текста): 1. Обычный запрос (без стриминга). 2. Стриминговый запрос (с измерением TTFT). Выведите разницу во времени до появления первого полезного контента.
Вы разрабатываете чат-бота для техподдержки, который использует огромную базу знаний (500 страниц документации) в системном промпте. Какой метод будет НАИБОЛЕЕ эффективен для снижения задержки первого токена (TTFT) при повторных запросах?