Перейти к содержимому
7 мин чтения

Как оценить задержку LLM-шлюза для голосового ассистента

Разбираем, как измеряется задержка LLM-шлюза, сколько миллисекунд отдать сети, STT, генерации и TTS, какие p95 закрепить в пилоте и SLA.

Как оценить задержку LLM-шлюза для голосового ассистента

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

Я бы ставил для пилота цель p50 <= 900 мс и p95 <= 1 400 мс от конца речи до первого слышимого фрагмента. Это не мировой стандарт, а рабочая инженерная граница для диалога, в котором люди не начинают переспрашивать и перебивать систему. Самому шлюзу, без времени модели, стоит отдать не больше 50 мс на p50 и 120 мс на p95. Если шлюз тратит полсекунды на авторизацию, маршрутизацию и установку нового соединения, никакая быстрая модель его не спасет.

Какой момент считать началом ответа

Измеряйте задержку от подтвержденного конца пользовательской речи до первого аудиосэмпла ответа, который реально вышел на устройство. Эта метрика называется speech_end_to_first_audio; она ближе всего к тому, что слышит человек. Таймер от отправки HTTP-запроса до полного JSON-ответа скрывает распознавание, детектор конца реплики, синтез и буфер воспроизведения.

У начала интервала есть неприятная неоднозначность. Физический конец речи известен только задним числом, а endpointing-компонент решает, была ли пауза окончанием фразы. Сохраняйте две отметки: last_voice_sample_at, которую дает VAD, и utterance_committed_at, когда оркестратор принял решение отправить финальный текст. Разница между ними показывает цену осторожности детектора. Если оставить только вторую отметку, 500 мс ожидания тишины исчезнут из графика, хотя пользователь их прекрасно слышит.

Конец интервала тоже нельзя ставить на событии «TTS вернул первый байт». Между байтом сервера и звуком остаются декодирование, jitter buffer, очередь аудиоустройства и иногда пауза в самом контейнере. Клиент должен отметить момент, когда положил первый ненулевой PCM-фрейм в воспроизведение. Для телефонии полезно дополнительно измерять границу медиашлюза, но продуктовая SLO должна заканчиваться на клиенте или на максимально близкой к нему точке наблюдения.

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

Отдельно помечайте служебную реплику и содержательный ответ. Фраза «одну минуту» улучшает время до первого звука, но не сокращает ожидание результата. Если продукт регулярно вставляет такую реплику, добавьте speech_end_to_first_content_audio и ограничьте ее собственным p95. Иначе команда научится выигрывать метрику звуковой заглушкой.

Бюджет отклика складывается последовательно

Разложите один пользовательский ход на интервалы с едиными часами и сложите их в критический путь. Для типичного каскада STT, LLM и TTS бюджет p50 на первый звук может выглядеть так:

УчастокБюджет p50Что входит
Решение о конце реплики220 мсVAD, endpointing, короткое ожидание тишины
Финализация STT120 мсПоследний аудиофрейм, стабильный текст
Сеть до оркестратора и обратно80 мсРеальный маршрут клиента, а не ping из ЦОДа
Политики и маршрутизация шлюза50 мсАвторизация, лимиты, выбор провайдера
LLM до первого пригодного фрагмента300 мсОчередь, prefill, первые токены
TTS до первого аудиофрейма90 мсНакопление текста и начало синтеза
Клиентский буфер и запуск40 мсДекодирование и воспроизведение
Итого900 мсОт конца речи до первого звука

Эти числа служат стартовой гипотезой, а не обещанием конкретного провайдера. В колл-центре с телефонией и 8 кГц распределение будет одним, в мобильном приложении с нестабильной сетью другим. Сначала закрепите общий предел, затем меняйте доли по трассам. Команда, которая начинает с отдельных SLA компонентов, часто получает пять «быстрых» сервисов и медленный разговор.

ITU-T G.114 регулярно цитируют как доказательство, что 150 мс якобы достаточно или допустимо для ассистента. Рекомендация говорит об односторонней передаче разговорной речи: до 150 мс приемлемо для большинства приложений, а выше 400 мс уже плохо для общего сетевого планирования. Это полезная опора для сетевой части, но не норматив на полный ход машины. В нашем пути есть распознавание, принятие решения и генерация, которых в обычном голосовом канале между двумя людьми нет.

Резервируйте 10-15 процентов общего бюджета на неучтенные переходы, но не прячьте их под словом «сеть». DNS, TLS, очередь пула, сериализация и холодный контейнер должны получить собственные события. Иначе резерв быстро превращается в постоянные 300 мс, за которые никто не отвечает.

Складывать отдельные p95 арифметически для прогноза общего p95 нельзя. Хвосты компонентов могут совпадать или возникать на разных запросах. Соберите сквозное распределение на одних и тех же трассах, а бюджет по компонентам используйте как ограничение и средство поиска причины. Для планирования емкости полезно смоделировать совместные пики, но приемку все равно проводите по наблюдаемой сквозной метрике.

Детектор конца речи часто дороже шлюза

Самая большая управляемая пауза нередко возникает до LLM-запроса. Endpointing должен отличить завершенную фразу от короткой паузы внутри нее. Если порог тишины равен 700 мс, система уже проиграла большую часть бюджета еще до маршрутизации.

Слишком короткий порог тоже вреден. Ассистент отправит «переведи мне» до того, как пользователь добавит «платеж за март», модель начнет отвечать, а оркестратору придется отменять генерацию. На графике TTFT все будет быстро, а в записи разговора появятся обрывы и ложные старты. Поэтому измеряйте не только миллисекунды, но и долю ошибочных завершений реплики, долю отмененных ответов и число исправлений финального STT.

Для русского языка фиксированный порог особенно груб при числах, фамилиях и диктовке реквизитов. Практичнее сочетать VAD, стабильность частичной расшифровки и признаки законченной фразы. При низкой уверенности можно подождать дополнительные 150-250 мс, но только для таких ходов. Одинаковые 600 мс на «да» и на длинный адрес дают простую конфигурацию ценой плохого опыта.

Barge-in, то есть перебивание ассистента, требует отдельного пути. От первого голоса пользователя до приглушения TTS я ставлю цель p95 не выше 200 мс. Здесь нельзя ждать финального STT или решения LLM: VAD на клиенте либо медиасервере сразу останавливает воспроизведение, а оркестратор затем отменяет генерацию и синтез. Если счетчик токенов продолжает работать после отмены, это уже проблема стоимости и корректности отмены, а не только задержки.

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

Потоковый STT нужно судить по стабильности текста

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

Документация Amazon Transcribe прямо разделяет IsPartial и завершенный сегмент, а стабилизация помечает отдельные элементы полем Stable. Высокая стабилизация ускоряет появление закрепленных слов, но может снизить точность. Это хороший пример неизбежного обмена, а не переключатель «ускорить бесплатно». Оркестратор может заранее подготовить контекст на стабильном префиксе, но выполнять инструменты и формировать окончательный ответ следует после commit-события.

Размер аудиофрейма входит в задержку дважды: отправитель ждет накопления фрейма, а распознаватель получает менее частые порции. Google Cloud рекомендует 100-миллисекундный фрейм как компромисс между эффективностью и задержкой. Amazon указывает диапазон 50-200 мс и равномерный размер чанков. Я начинаю тест с 80-100 мс, затем проверяю сеть: при большом числе мелких пакетов мобильный канал может дать больше джиттера, чем мы выиграли на накоплении.

Для STT заведите три гистограммы: audio_frame_to_partial, last_voice_to_stable_text и last_voice_to_final_text. Первая ловит транспорт и обработку потока, вторая показывает, когда можно спекулятивно готовить LLM, третья входит в честный критический путь. Среднее значение здесь почти бесполезно. Один длинный хвост на шумных звонках испортит разговоры именно тем пользователям, которым распознавание и без того дается хуже.

Спекулятивный запуск допустим, если его можно дешево отменить и он не вызывает внешнего действия. Нельзя по частичной фразе отправлять перевод, менять тариф или зачитывать баланс. Можно прогреть маршрут, собрать неизменяемую часть системного промпта и начать prefill стабильного префикса, если стек это поддерживает.

Шлюзу нужен собственный лимит накладных расходов

Смените только адрес API
Ваш OpenAI-совместимый SDK, код и промпты продолжат работать после замены base_url.

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

Для пилота разумна такая градация внутреннего overhead при теплом соединении:

  • до 50 мс p50 и до 120 мс p95: нормальный запас для политик, выбора маршрута и журналирования;
  • 120-200 мс p95: терпимо на старте, если трасса показывает конкретную дорогую проверку;
  • выше 200 мс p95: шлюз уже заметно забирает бюджет голоса;
  • выше 400 мс p99: нужен отдельный разбор очередей, повторов и холодных путей.

Порог нельзя применять к первому запросу после разрыва соединения так же, как к устойчивому потоку. Измеряйте режимы warm, new_connection и failover раздельно. Для голоса держите HTTP/2, WebSocket или другой поддерживаемый поток открытым на время сессии. Если каждый ход заново платит за DNS, TCP и TLS, архитектура создает задержку своими руками.

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

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

При работе через RU LLM можно сохранить OpenAI-совместимый клиент и сменить base_url, но задержку все равно нужно снимать по каждому маршруту и модели, включая варианты на собственной GPU-инфраструктуре в российских ЦОДах. Совместимый API упрощает переключение, а физику очередей и расстояния он не отменяет.

Первый токен еще нельзя произнести

TTFT модели заканчивается на первом токене, а TTS нужен произносимый фрагмент. Если первые токены содержат пробел, служебную конструкцию или начало длинного слова, метрика провайдера выглядит хорошо, но звук не начинается. Введите time_to_first_speakable_chunk: от отправки LLM-запроса до фрагмента, который TTS принял без последующей правки.

NVIDIA в методике NIM определяет TTFT как ожидание первого непустого токена и предупреждает, что в него обычно входят очередь, prefill и сеть. Там же ITL определена как средний интервал между последующими токенами без TTFT. Это полезные определения, но разные движки ставят таймеры в разных точках. В договоре и телеметрии надо описать события, а не полагаться на одинаковую аббревиатуру.

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

Скорость после первого токена влияет на звук не меньше TTFT. Если LLM выдает текст медленнее, чем TTS его произносит, аудиобуфер опустеет. Следите за audio_buffer_ms и минимальным значением за ход. Средняя ITL может скрыть отдельные паузы, поэтому полезны p95 интервала между токенами и максимальный разрыв внутри каждой генерации.

Модели с рассуждением могут долго молчать до видимого ответа. Для голосового канала либо выбирайте режим с коротким prefill и быстрым началом, либо разделяйте задачу: компактная модель мгновенно подтверждает намерение, а более тяжелая готовит содержательный результат. Подтверждение должно сообщать правду, например «проверяю статус», и не повторяться одинаковой заглушкой в каждом ходе.

Одна трасса должна объяснять всю паузу

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

Инструментируйте клиент, медиасервер, STT, оркестратор, шлюз, LLM и TTS одним trace_id, а интервалы пишите как события монотонных часов. Абсолютное время между машинами пригодно для корреляции, но длительность на одном узле считайте монотонным таймером. Синхронизация NTP не исправляет прыжок системных часов внутри запроса.

Минимальная запись завершенного хода может выглядеть так:

{
  "trace_id": "voice-01842",
  "route": "model-a/provider-b",
  "timestamps_ms": {
    "last_voice": 0,
    "utterance_commit": 238,
    "stt_final": 351,
    "gateway_in": 374,
    "provider_send": 411,
    "first_token": 702,
    "tts_send": 756,
    "first_audio_received": 832,
    "first_audio_played": 887
  },
  "cancelled": false,
  "failover": false
}

Из такой записи видно, что пользователь ждал 887 мс, внутренние действия шлюза до отправки заняли 37 мс, а первый токен пришел через 291 мс после отправки провайдеру. Значение tts_send также показывает 54 мс между первым токеном и пригодным для синтеза фрагментом. Ни один общий request_latency не дал бы такого разбора.

Сохраняйте рядом длину входа и выхода в токенах, длительность аудио, кодек, тип сети, регион клиента, модель, провайдера, статус кэша, число повторов и причину остановки. Текст и аудио для анализа задержки не обязательны. В российском контуре отдельно решите, какие метаданные могут связать трассу с человеком, задайте срок хранения и маскируйте PII до записи. Быстрая система не получает освобождения от требований 152-ФЗ.

Клиентские и серверные события удобно соединять через traceparent, но идентификатор не должен содержать номер телефона, логин или внутренний номер договора. Случайного идентификатора достаточно. В выборку для долгого хранения можно положить только интервалы и технические теги, а доступ к исходной записи разговора оставить в другом, более строгом контуре.

Проверьте полноту: сумма известных интервалов должна сходиться с speech_end_to_first_audio с небольшой погрешностью. Если остаются необъясненные 180 мс, выпускать красивый дашборд рано. Сначала найдите пропавший участок.

Пилот должен пройти хвосты и нагрузку

Пилот принимают по p95 и доле нарушений, а не по впечатляющей медиане на десяти запросах. Я использую три воротца: качество диалога, задержка компонентов и поведение при деградации. Проход по среднему при провале любого воротца не считается успехом.

Для первого пилота на целевом трафике можно закрепить такие пороги:

МетрикаЦельКрасная граница
Конец речи до первого звука, p50<= 900 мс> 1 100 мс
Конец речи до первого звука, p95<= 1 400 мс> 1 800 мс
Конец речи до первого звука, p99<= 2 000 мс> 2 500 мс
Внутренний overhead шлюза, p95<= 120 мс> 200 мс
Прерывание TTS после начала речи, p95<= 200 мс> 300 мс
Ходы с разрывом аудиобуфера< 0,5%> 1%
Ошибочное завершение реплики< 2%> 4%

Проценты в таблице не универсальны. Их надо подтвердить пользовательскими сессиями и ценой ошибки для конкретного сценария. Банковское подтверждение операции терпит чуть более долгую проверку, но не терпит преждевременного commit. Справочный ассистент может ответить быстрее с меньшей моделью и принять небольшую потерю полноты.

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

Сегментируйте результат по модели, провайдеру, региону, типу сети и длине промпта. Общая p95 может пройти, пока один маршрут стабильно дает три секунды. На пилоте такой маршрут либо получает меньший вес, либо исключается из голосового пула, даже если он дешевле и хорошо отвечает в пакетных задачах.

Устройте отдельный прогон отказов: оборвите поток STT, задержите первые байты модели, верните ошибку TTS и закройте клиентское соединение во время ответа. У каждого случая должен быть ограниченный дедлайн и слышимое поведение, которое не вводит человека в заблуждение. Пилот без таких проверок подтверждает только счастливый путь, а большую часть длинного хвоста создают именно повторы и ожидание уже недоступной зависимости.

SLA поставщика должен описывать измерение

Проверьте open-weight модели для голоса
Прогоните Llama, Qwen, Gemma или DeepSeek на собственной инфраструктуре шлюза.

SLA на «задержку API 300 мс» ничего не значит без начальной точки, конечного события, перцентиля, нагрузки и исключений. Для шлюза запишите как минимум SLO внутренней обработки, доступность потокового соединения, время обнаружения отказа маршрута и предел failover. Для модели отдельно задайте TTFT и темп последующих токенов.

В приложении к договору должны быть указаны:

  • точка измерения и регион генератора нагрузки;
  • распределение длины промптов и ответов, а также число одновременных сессий;
  • p50, p95 и p99 за короткие окна, плюс допустимая доля нарушений;
  • правила учета тайм-аутов, повторов, отмен и ответов с ошибкой;
  • доступ к идентификатору трассы для разбора спорного запроса.

Не принимайте исключение «зависит от нижестоящего провайдера» для всей метрики. Шлюз действительно не управляет чужой GPU, зато он управляет выбором маршрута, тайм-аутом, пулом соединений, повтором и моментом переключения. Разделите ответственность численно: собственный overhead поставщика, наблюдаемый RTT, provider TTFT и время failover.

Среднемесячный p95 тоже слишком груб для голоса. Двадцать минут тяжелой очереди могут испортить тысячи разговоров и раствориться в месяце. Считайте короткие окна, например пять минут, и задайте долю окон, которые обязаны пройти. Для коммерческого SLA добавьте сервисные последствия, но техническая команда прежде всего должна получить сырые события и возможность сопоставить их со своей трассой.

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

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

Сокращать нужно критический путь, а не отчет

Начните оптимизацию с самого длинного участка p95, который находится под вашим контролем. Часто это endpointing, очередь модели или создание соединения, а не код маршрутизатора. Сокращение 20 мс в шлюзе бесполезно, если детектор каждый раз ждет 700 мс тишины.

После базовой трассировки обычно работают четыре приема: постоянное потоковое соединение на сессию, ранняя подготовка неизменяемого контекста, выбор модели по голосовому SLO и потоковая передача законченных фраз в TTS. Prompt caching может сократить prefill повторяющегося системного префикса, но промах кэша обязан оставаться в нагрузочном наборе. Иначе пилот измерит идеальный повтор, а продакшен получит разнообразные диалоги.

Не гонитесь за минимальным TTFT ценой неверных ответов и ложных действий. Голос делает паузу заметной, но ошибка в сумме платежа заметнее. Разделите ходы по риску: справочные ответы получают агрессивный маршрут и короткий endpointing, операции с последствиями ждут финальный текст и проходят нужные проверки. Пользователь примет объяснимые дополнительные 300 мс перед подтверждением лучше, чем мгновенную ошибку.

Решение о допуске простое, когда измерения честные. Пилот проходит, если p95 первого звука укладывается в 1,4 секунды на целевой нагрузке, внутренний p95 шлюза остается ниже 120 мс, перебивание останавливает звук за 200 мс, а качество commit не падает ниже принятого порога. Если один из пунктов не проходит, трасса уже показывает, кому возвращать бюджет. Именно такой договор между компонентами превращает субъективное «ассистент тормозит» в исправимую инженерную задачу.

Часто задаваемые вопросы

Какая задержка голосового ассистента считается нормальной?

Для пилота разумно целиться в p50 до 900 мс и p95 до 1,4 секунды от конца речи пользователя до первого звука. Граница зависит от сценария, поэтому ее проверяют на целевой аудитории, а не объявляют универсальным стандартом.

Сколько задержки можно отдать LLM-шлюзу?

На внутреннюю обработку шлюза я закладываю до 50 мс на p50 и до 120 мс на p95 при теплом соединении. Время сети до провайдера, очередь модели и ее TTFT нужно показывать отдельно.

Нужно ли включать STT в общую задержку ответа?

Да, пользователь слышит всю паузу, включая ожидание конца реплики и финализацию распознавания. Отдельные метрики STT нужны для диагностики, но продуктовая SLO начинается с конца речи.

Почему нельзя измерять только TTFT модели?

Первый токен может оказаться непригодным для синтеза, а после него возможны медленные разрывы между токенами. Измеряйте также время до первого произносимого фрагмента, первый звук и заполнение аудиобуфера.

Какой перцентиль задержки использовать в SLA?

Минимум p95, а для диагностики редких тяжелых пауз нужен p99. Медиана показывает обычный ход, но не защищает разговор от очередей и холодных маршрутов.

Можно ли ускорить ответ с помощью частичного результата STT?

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

Как измерять задержку при переключении на резервную модель?

Помечайте такие ходы как failover и считайте время от дедлайна основного маршрута до первого токена резервного. Не смешивайте их с теплым обычным путем, иначе SLA ничего не скажет о восстановлении.

Что важнее для голоса, TTFT или скорость генерации токенов?

Обе метрики ограничивают разные части разговора. TTFT определяет начальную паузу, а медленная или неровная выдача токенов опустошает буфер TTS и создает провалы уже внутри ответа.

Как тестировать задержку LLM-шлюза под нагрузкой?

Повторите профиль реальных разговоров: несколько ходов на соединение, разные длины реплик, всплески, отмены и промахи кэша. Затем сравнивайте p95 по каждому маршруту, модели, региону и типу сети.

Что делать, если p95 ответа превышает две секунды?

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