Voice AI Agents PROSTT, TTS и управление диалогомBarge-in и latency budget: человечный диалог

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