ГлавнаяБлогОптимизация LLM-пайплайна: с 3.2с до 1.1с на первый токен
AI / Нейросети

Оптимизация LLM-пайплайна: с 3.2с до 1.1с на первый токен

Узнайте, как сократить задержку LLM-пайплайна с 3.2 до 1.1 секунды: VAD-фильтрация, удаление классификатора, кэширование промптов и выбор модели. Примените советы сейчас!

Al
Редакция Algolitalgolit.ru
8 мин чтения15 августа 2026 г.

Почему ваш LLM-пайплайн тормозит: разбор задержек

Представьте: вы в видеозвонке, собеседник задаёт вопрос, а вы ждёте ответ от ИИ-ассистента. Три секунды — это вечность, когда человек ждёт вашей реакции. В этой статье я разберу, как я оптимизировал свой десктопный оверлей для интервью, сократив время до первого токена с 3.2 до 1.1 секунды. Вы узнаете, какие компоненты пайплайна на самом деле критичны, и как применить эти приёмы в своих проектах.

Наивный пайплайн: где теряются миллисекунды

Исходная архитектура выглядела так: захват аудио (loopback) → PCM → WebSocket STT → транскрипт → классификатор «вопрос ли это?» → LLM → потоковый ответ. Итого ~3.2 секунды до первого токена. Четыре места для оптимизации, но только два из них оказались решающими.

Оптимизация 1: VAD-фильтрация тишины на клиенте

Первая победа — не про带宽, а про эндпоинтинг на стороне STT-сервера. Если вы передаёте непрерывный аудиопоток, сервер не видит чёткой паузы и задерживает фиксацию сегмента. Решение — детектор речи (VAD) в AudioWorklet, работающий на сыром сигнале до нормализации:

this._vadThresh = 0.005;   // RMS: выше — речь
this._hangoverSec = 1.0;   // продолжать отправку после падения уровня
this._prerollMax = 14;     // ~600ms @48k буферизованной тишины

Три параметра, каждый — из-за конкретной проблемы:

  • Порог на сыром аудио. Если сначала нормализовать, то фоновый шум усиливается и превращается в «речь». Детектируйте на сыром сигнале, нормализуйте после.
  • Hangover 1.0с. Если обрезать поток сразу при падении RMS, вы срежете хвост каждого предложения. Значение должно превышать порог тишины сервера (0.6с) с запасом, иначе сервер не получит завершающую тишину для фиксации сегмента.
  • Pre-roll ~600мс. Тихие начала предложений могут быть ниже порога VAD. К моменту обнаружения речи первый слог потерян. Поэтому держите скользящий буфер последних 14 чанков и сбрасывайте его при срабатывании VAD.

Последний пункт — разница между «что такое индекс базы данных» и «то такое индекс базы данных». И LLM уверенно отвечает на второй вариант неправильно.

Оптимизация 2: серверный эндпоинтинг STT

ElevenLabs Scribe v2 Realtime поддерживает commit_strategy: 'vad', что позволяет серверу фиксировать сегмент после паузы, не дожидаясь вашего запроса:

commit_strategy: 'vad',
vad_silence_threshold_secs: '0.6',

В сочетании с клиентским VAD транскрипт приходит, пока собеседник ещё делает вдох. Один острый момент: лимит на keyterm biasing — 50. Если отправить 51, сокет закроется с кодом 1008 и сообщением, которое вы не увидите, если не логируете причины закрытия. Я потерял вечер на этом.

Проблема переподключений и «молчащего сокета»

Долгие звонки означают обрывы соединения. Переподключиться легко, а вот правильно — нет. Типичный сбой: происходит reconnect, и пока он выполняется, пользователь меняет язык, что вызывает ещё один reconnect. Теперь существуют два сокета. Старый всё ещё доставляет события, новый — настоящее соединение, и транскрипт перемешивается с мусором от обоих. Или хуже: устаревший побеждает, а новый молча игнорируется — аудио идёт, но ничего не появляется, и нет ни одной ошибки.

Решение — счётчик поколений:

const myGen = ++this.gen;      // это соединение
// ...позже, в каждом обработчике:
if (myGen !== this.gen) return;  // новое соединение заменило нас

Каждое асинхронное продолжение проверяет, актуально ли оно. Всё, что приходит от устаревшего сокета, отбрасывается. Четыре строки, которые устранили целый класс проблем «просто перестало работать через 20 минут».

Удаление классификатора: неожиданное решение

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

«Что такое SOLID?» — ответ — «А вторая буква?»

«А вторая буква?» классификатор оценивает как не-вопрос. Он молчал именно тогда, когда контекст делал намерение очевидным. Точность была высокой, но ошибки были катастрофическими и происходили в самые ценные моменты.

Я удалил классификатор. Основная модель теперь видит каждое завершённое высказывание плюс недавний контекст и решает сама — у неё есть контекст, которого у классификатора не было. Это убрало 200мс и исправило проблему уточнений. Две победы от удаления кода.

Вместо классификатора я поставил простой клиентский гейт:

const AUTO_SILENCE_MS = 900;    // нет «?» — пауза в середине предложения
const AUTO_UTTEREND_MS = 120;   // заканчивается на «?» — реагировать почти сразу

Если буфер заканчивается вопросительным знаком, предложение завершено — запускаем через 120мс. Иначе ждём 900мс на случай, если собеседник думает. Никаких вызовов моделей, никаких сетевых запросов.

Кэширование промптов: почему TTL важнее hit rate

Системный промпт содержит сжатую карточку резюме, поэтому он немаленький. Кэширование промптов в Anthropic решает эту проблему, но стандартный TTL — 5 минут, а в интервью бывают паузы длиннее. Интервьюер говорит, кандидат думает, и кэш тихо истекает. Вы платите полную цену как раз за вопросы, идущие после долгой паузы.

Два блока, оба с TTL 1 час:

const CACHE_1H = { type: "ephemeral", ttl: "1h" } as const;
// блок 1: статическая персона — общая для всех сессий и пользователей
// блок 2: данные сессии (CV-карта, роль) — стабильны для одного интервью

Разделение статического и сессионного важно: блок 1 одинаков для всех, поэтому он «тёплый» ещё до первого вопроса пользователя. Если жадно интерполировать переменные (например, язык ответа) в этот блок, вы разобьёте одну запись кэша на шестнадцать.

Параллелизация проверок доступа

Аутентификация, rate limit, проверка квоты, парсинг тела — всё это независимые операции, но выполнялись последовательно. Я объединил их в Promise.all:

const [, accessRes] = await Promise.all([
  enforceRateLimit(db, userId, "ask", 15, 60),
  assertAccess(db, userId, "ask"),
  assertDailyCap(db, userId),
  req.json().then((b) => { body = b; }),
]);

Экономия ~92мс без изменения гарантий. Не гламурно, но лучшее соотношение усилий и результата во всём списке.

Выбор модели на основе измерений, а не предпочтений

Я предполагал, что нужна самая мощная модель. Измерив на 10 технических вопросах, получил:

  • Sonnet-класс: $3.54 за 500 запросов, 8.6с полного ответа
  • Haiku-класс: $1.03, 5.2с

В 3.4 раза дешевле и на 3.4 секунды быстрее. Цена — примерно одна фактическая ошибка на 5–6 вопросов и чуть более грубый текст. Для реального времени, где поздний ответ бесполезен, выбор очевиден.

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

Итоговые результаты: 1.1с вместо 3.2с

Время от конца предложения до первого токена сократилось с ~3.2с до ~1.1с. Разбивка по источникам:

  • Удаление классификатора: −200мс и исправление ошибок
  • VAD-гейтинг + серверный эндпоинтинг: −1.2с
  • Параллельная проверка доступа: −92мс
  • Более дешёвая и быстрая модель: −3.4с на полный ответ

Два самых больших выигрыша пришли от удаления компонента и от параметров в 40-строчном аудио-ворклете. Ноль — от самого вызова LLM, на который я потратил первую неделю.

Что бы я сделал иначе

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

Относитесь к «молчаливой остановке» как к стандартному сбою. Каждый тихий путь — устаревший сокет, истёкший кэш, проглоченный catch — выглядит как «работает» снаружи. Счётчик поколений и сторожевой таймер на потоке существуют потому, что сначала оба отказали молча.

Измеряйте, прежде чем считать модель узким местом. Она составляла 200мс из 3.2с.

Практический вывод: что делать прямо сейчас

Возьмите свой пайплайн и проверьте четыре вещи: есть ли VAD-гейтинг на клиенте? Используете ли вы серверный эндпоинтинг STT? Нужен ли вам отдельный классификатор или основная модель справится сама? Какая модель реально нужна по метрикам, а не по ощущениям? Начните с VAD — это самый большой выигрыш без изменения логики приложения.

#LLM-пайплайн#оптимизация задержки#VAD#кэширование промптов#выбор модели
Al
Редакция Algolit

Пишем про алгоритмы, подготовку к собеседованиям и карьеру в IT — так, чтобы было понятно и полезно.

Хочешь закрепить знания на практике?

Решай задачи на Algolit — интерактивная платформа для обучения

Начать бесплатно →