Урок курса
Barge-in и latency budget: человечный диалог
Voice AI Agents PROBarge-in: три стратегии и проблема поддакиваний
В первом модуле мы уже разбирали этот механизм: VAD обнаруживает речь клиента, пока агент говорит, — пайплайн останавливает TTS, сбрасывает очередь синтеза, отменяет генерацию LLM и начинает новый цикл распознавания. Здесь разберём то, что за этим стоит: когда именно останавливаться, а когда нет — и как это выглядит в коде.
Три стратегии
Поведение агента при перебивании сводится к одному из трёх вариантов:
- Без перебивания — событие
startво время речи агента игнорируется. Агент договаривает реплику и лишь затем слушает. Допустимо для коротких подтверждений в одну фразу, но на длинных репликах ломает разговор. - Акустическое перебивание — любое
startнемедленно останавливает агента. Просто в реализации, но срабатывает на любой звук со стороны клиента, включая короткие поддакивания. - Семантическое (адаптивное) перебивание — агент останавливается, только если клиент действительно перехватывает слово, и не реагирует на короткие подтверждения.
Почему «любой звук прерывает» — плохо
Пока агент говорит, клиент часто вставляет короткие реплики, подтверждая, что слушает: «угу», «ага», «да-да», «понятно». Это backchannel — обратная связь, а не попытка перехватить слово. Акустический barge-in принимает их за перебивание и обрывает агента на полуслове, после чего тот сбрасывает свою реплику и начинает слушать пустоту. Диалог рвётся на ровном месте: клиент всего лишь подтвердил, что слышит.
Адаптивное перебивание добавляет к событию start классификатор: это намерение перехватить слово или короткое поддакивание? Решение опирается на длительность реплики, её энергию и первые распознанные слова. Современные фреймворки голосовых агентов уже включают такой фильтр backchannel из коробки — но понимать, что именно он решает, нужно независимо от инструмента: именно от качества этого решения зависит, рвётся диалог или нет.
Реализация: один обработчик события
Каждый раз, когда событие start приходит, пока агент говорит у него есть 2 варианта решения:
Левая ветка — настоящее перебивание. TTS останавливается в пределах текущего аудиочанка, не дольше 100 мс: клиент не должен слышать наложение двух голосов. Затем из очереди выбрасывается уже синтезированное, но ещё не воспроизведённое аудио, и отменяется генерация LLM. Только после этого стартует новый STT-цикл — с момента события start, иначе первые слова клиента потеряются.
Правая ветка — то, чего не было в простом акустическом barge-in: если речь классифицирована как поддакивание, агент просто продолжает. Именно эта развилка превращает акустический триггер в адаптивный.
В коде оба пути — один обработчик:
async def on_speech_start(event):
if not agent_is_speaking():
return # агент молчал — обычный цикл распознавания
if not is_real_interruption(event):
return # backchannel «угу» — правая ветка: агент продолжает
await tts.stop() # левая ветка: остановить воспроизведение в текущем чанке
audio_out_queue.clear() # выкинуть синтезированное, но не сказанное
llm_task.cancel() # прервать генерацию ответа, если она ещё идёт
start_new_stt_turn() # слушать с момента start, не теряя первых слов
Рабочий критерий хорошего barge-in держится на двух числах:
- Реакция до 200 мс — от начала речи клиента до остановки агента. Если дольше, клиент уже говорит, а агент ещё не замолчал: оба звучат одновременно, клиент сбивается.
- Поддакивания не обрывают — «ага» и «угу» агент игнорирует и продолжает. Если нет — разговор рассыпается на обрывки при каждом «угу» клиента.
Latency budget: из чего складывается задержка
Каждый компонент пайплайна, который мы собирали в этом модуле, добавляет свою задержку. По отдельности цифры уже встречались: TTFR у STT, TTFT у LLM, TTFA у TTS. Latency budget — это их сумма от момента, когда клиент закончил говорить, до момента, когда он услышал первый звук ответа. Именно эту величину клиент воспринимает как «быстро» или «тормозит».
Первая статья — пауза end-of-turn. Её механику разбирал прошлый урок; здесь важно другое: она входит в бюджет и часто оказывается крупнейшей статьёй — 300–600 мс уходит ещё до того, как пайплайн начал работать над ответом. Сократить её сетью или железом нельзя, это «честное» ожидание.
Дальше — цепочка обработки: STT финализирует транскрипт (150–300 мс), LLM отдаёт первый токен (200–800 мс — самый изменчивый компонент), TTS выдаёт первый звук синтеза (100–400 мс), плюс сеть — около 50 мс на каждый вызов. Стадии перекрываются при потоковой обработке, поэтому реальная задержка меньше арифметической суммы. Доминируют обычно два слагаемых: пауза end-of-turn и первый токен LLM.
Целевые ориентиры
Разумные цели для облачного каскада по компонентам: финал STT — до 200 мс, первый токен LLM — до 400 мс, первый звук TTS — до 150 мс. На больших выборках реальных звонков каскадная архитектура даёт: P50 — около 1,5 секунды, P95 — около 5 секунд. Если цифры заметно хуже — ищите узкое место по таблице ниже.
Куда смотреть, если медленно
- Реакция тормозит на каждой реплике одинаково — велика пауза end-of-turn. Проверьте таймаут тишины и семантический порог из прошлого урока.
- Задержка плавает от реплики к реплике — узкое место в LLM: длинный контекст, тяжёлая модель, очередь на стороне провайдера. Смотрите на TTFT.
- Агент стабильно «думает» лишнюю секунду перед каждым ответом — нет стриминга: пайплайн ждёт весь ответ LLM целиком, прежде чем отдать его в TTS.
- Первый звук приходит с задержкой только в начале разговора — холодный старт соединения или сессии TTS/STT.
Оптимизация: как уложиться в бюджет
Бюджет из прошлого шага показывает, где задержка велика. Теперь — как её сократить. Приёмы идут от самого крупного выигрыша к точечной борьбе за последние сотни миллисекунд.
Стриминг насквозь
Самая дорогая ошибка — ждать готовности целиком на каждом стыке. Если пайплайн дожидается всего ответа LLM, прежде чем отдать его в синтез, к задержке LLM прибавляется полное время генерации фразы. Решение — отдавать ответ по частям: как только LLM сформировала первое предложение, оно сразу уходит в TTS, пока модель генерирует продолжение. TTS, в свою очередь, отдаёт первый аудиочанк, не дожидаясь конца фразы.
async for sentence in llm_stream(prompt): # предложения по мере генерации
await tts.synthesize(sentence) # синтез начинается с первой фразы
Это приём с наибольшим эффектом — с него стоит начинать.
Кэш частых фраз
Часть реплик агента повторяется в каждом звонке: приветствие, «секунду, проверяю», «не расслышал, повторите, пожалуйста». Их незачем синтезировать заново — достаточно один раз получить аудио и держать готовым. На таких репликах первый звук приходит мгновенно, а самые заметные для клиента моменты (начало разговора) перестают тормозить.
Тёплые соединения
Открыть соединение и установить сессию — само по себе время: TLS-рукопожатие, аутентификация, прогрев. Если делать это на каждую реплику, задержка всплывает в начале каждого хода. Постоянный gRPC-стрим к SpeechKit или SaluteSpeech, который мы держали открытым в уроках по STT и TTS, — это и есть оптимизация: соединение живёт весь звонок, и за рукопожатие платишь один раз.
Спекулятивная генерация
Более тонкий приём: начать запрос к LLM по предварительному (partial) транскрипту, не дожидаясь, пока end-of-turn окончательно подтвердит конец реплики. Если клиент договорил так, как и предполагалось, ответ уже почти готов. Если иначе — спекулятивный запрос отбрасывается, и генерация идёт заново. Это срезает задержку, но увеличивает расход токенов и усложняет логику, поэтому применяется выборочно, когда борьба идёт за последние сотни миллисекунд.
Когда оптимизировать уже нечего
Все приёмы выше сокращают бюджет каскада, но не убирают его природу: три отдельные модели, между которыми данные меняют формат. Радикальная альтернатива — не складывать STT, LLM и TTS, а взять единую речевую модель (speech-to-speech), которая слышит и отвечает голосом сама, с встроенным перебиванием: GPT-Realtime-2 или российский Yandex Realtime API. Это убирает большую часть бюджета разом; Оба варианта разбираются в курсе отдельно.
Порядок, в котором стоит подступаться к оптимизации каскада: сначала стриминг насквозь (наибольший эффект), затем кэш частых фраз (мгновенный первый звук на типовых репликах), затем тёплые соединения (убирают разовые всплески), и в последнюю очередь спекулятивная генерация (сложный приём ради последних миллисекунд).
Диагностика задержки в продакшне
Бюджет из второго шага — это план. В продакшне нужно видеть реальную задержку каждого звонка, иначе оптимизация идёт вслепую: непонятно, какой компонент тормозит и помог ли вообще внесённый приём.
Таймстемпы по компонентам
Достаточно зафиксировать момент каждой передачи между блоками пайплайна. Минимальный набор на одну реплику клиента:
speech_end— end-of-turn зафиксировал конец реплики клиентаstt_final— финальный транскрипт готовllm_first_token— пришёл первый токен ответаtts_first_audio— синтез выдал первый аудиочанкplayback_start— клиент услышал первый звук ответа
Разности между соседними таймстемпами и есть бюджет, измеренный на живом звонке, а не оценочный:
t = {}
t["speech_end"] = now() # end-of-turn сработал
t["stt_final"] = now() # транскрипт финализирован
t["llm_first_token"] = now() # первый токен ответа
t["tts_first_audio"] = now() # первый чанк синтеза
t["playback_start"] = now() # клиент услышал ответ
log_latency(call_id, {
"stt_ms": t["stt_final"] - t["speech_end"],
"llm_ms": t["llm_first_token"] - t["stt_final"],
"tts_ms": t["tts_first_audio"] - t["llm_first_token"],
"total_ms": t["playback_start"] - t["speech_end"],
})
Почему среднее обманывает
По одному числу — среднему — задержку оценивать нельзя: оно прячет хвосты. Холодный старт, сетевой всплеск, неожиданно длинный ответ LLM бьют по отдельным звонкам, и именно эти звонки портят впечатление, а в среднем растворяются. Поэтому задержку смотрят по перцентилям: p50 (медиана — типичный звонок), p95 и p99 (хвост — худшие случаи).
Если p50 равен 700 мс, а p95 — 3 секунды, значит каждый двадцатый клиент ждёт три секунды и думает, что его не услышали. Оптимизировать нужно именно p95, а не среднее: подтянуть хвост важнее, чем сдвинуть и без того приемлемую медиану.
Задача 1
Какая характеристика отличает акустическое перебивание от адаптивного?
Задача 2
Почему акустический barge-in рвёт диалог при коротких репликах клиента?
Задача 3
Сопоставьте стратегию barge-in с описанием её поведения.
Сопоставьте элементы
Задача 4
Какова типичная задержка финализации транскрипта STT в каскадной архитектуре голосового агента?
Задача 5
Какие компоненты входят в latency budget голосового агента? (выберите все верные)
Задача 6
О чём говорит плавающая задержка агента — разная от реплики к реплике?
Задача 7
Сопоставьте таймстемп с тем, что он фиксирует в диагностике задержки.
Сопоставьте элементы
Задача 8
С какого приёма оптимизации latency стоит начинать в первую очередь?
Задача 9
Какие приёмы снижают задержку каскадного агента? (выберите все верные)
Задача 10
Почему при оптимизации задержки важнее улучшать p95, а не среднее?
Продолжить с сохранением прогресса
Перейдите к уроку в учебном кабинете.
