Barge-in и latency budget: человечный диалог
Уроки курсаBarge-in и latency budget: человечный диалог
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 мс — от начала речи клиента до остановки агента. Если дольше, клиент уже говорит, а агент ещё не замолчал: оба звучат одновременно, клиент сбивается.
- Поддакивания не обрывают — «ага» и «угу» агент игнорирует и продолжает. Если нет — разговор рассыпается на обрывки при каждом «угу» клиента.
