Урок курса

Barge-in и latency budget: человечный диалог

Voice AI Agents PRO

Barge-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, а не среднее?

Выберите один вариант
Без регистрации · результат не сохраняется

Продолжить с сохранением прогресса

Перейдите к уроку в учебном кабинете.

Перейти к интерактивному уроку