«Мой номер заказа... сейчас, секунду...» — и бодрое «Да, я вас понял, минуточку!» прямо в середину фразы.
Все, кто звонил в поддержку за последние пару лет, это слышали. Выглядит как мелкая недоработка, а на деле это одна из самых неприятных задач в голосовом стеке, и решается она заметно хуже, чем кажется со стороны.
Формулируется она так: понять, что человек в трубке закончил говорить, а не взял паузу. Ниже — про то, почему готовые VAD эту задачу не решают в принципе, что мы перебрали, прежде чем взяться за свое, и где у нас до сих пор не получается.
Дисклеймер: работаю в аутсорсинговом контакт-центре, мы несколько лет делаем свой голосовой стек — телефония, распознавание, ассистенты на LLM. Продукт не называю и ссылок не будет, статья про задачу.
Почему это вообще проблема
Кажется, что все просто: детектируем тишину, ждем N миллисекунд, отдаем накопленный аудиобуфер в ASR, дальше LLM, дальше синтез. Классический VAD, библиотек полно, вопрос закрыт.
Проблема в том, что тишина в человеческой речи означает две принципиально разные вещи.
Первая — реплика закончилась, ход перешел к собеседнику. Вторая — человек взял паузу внутри собственной реплики: вспоминает номер заказа, ищет договор на столе, формулирует претензию, отвлекся на ребенка.
Акустически эти две паузы часто неразличимы. Более того, пауза внутри реплики нередко длиннее, чем пауза на границе хода: попробуйте продиктовать вслух номер карты, который вы ищете в приложении.
Отсюда вилка, в которую упирается любая наивная реализация:
Короткий порог тишины. Ассистент отвечает быстро, но лезет в середину фразы. Пользователь начинает говорить громче и быстрее, чтобы успеть, ассистент перебивает снова. Дальше — либо мат, либо сброс.
Длинный порог. Ассистент не перебивает, но после каждой вашей фразы висит пауза в полторы секунды. Диалог ощущается как разговор по спутниковому телефону.
В литературе это называется endpointing, или end-of-turn detection, и путать это с обычным VAD не стоит. VAD отвечает на вопрос «есть ли сейчас в сигнале речь». Endpointing — на вопрос «имеет ли смысл сейчас брать ход». Второе включает в себя первое, но им не исчерпывается.
Что мы пробовали до того, как начали делать свое
Честно скажу: своя разработка тут не была самоцелью, мы довольно долго пытались обойтись готовым.
Энергетический VAD (webrtcvad и подобное). Дешево, быстро, работает на CPU. На чистой студийной записи прекрасно. На телефонном канале — 8 кГц, кодек, шум улицы, чужие разговоры на фоне, ребенок, телевизор — начинается веселье. Плюс он в принципе не про смысл: он не знает разницы между концом мысли и паузой посреди нее.
Нейросетевые VAD (Silero и аналоги). Заметно устойчивее к шуму, честно решают свою задачу — «речь / не речь». Но задача-то другая. Мы получили меньше ложных срабатываний на шум и ровно ту же проблему с паузами внутри реплики.
Серверный endpointing из облачного ASR. У крупных провайдеров есть встроенная логика определения конца фразы, и она неплохая. Два «но». Первое — она настраивается очень грубо, обычно это один-два параметра таймаута, а нам нужна разная логика для разных сценариев (диктовка номера и свободный рассказ о проблеме — это разные режимы). Второе — сетевой круг наружу, и в бюджете задержки это ощутимая доля.
Просто подкрутить таймаут. Сначала мы, конечно, пошли этим путем. Подобрали значение, стало лучше. Потом посмотрели на разбор конкретных диалогов и увидели, что «лучше» — это в среднем по больнице: на одних сценариях перебиваем, на других тормозим. Одно число на все случаи жизни не существует, потому что распределение длин внутренних пауз зависит от того, что человек в этот момент делает.
Ключевая мысль, до которой мы шли дольше всего
Акустика не знает, закончил ли человек мысль. Это знает текст.
«Мой номер заказа» — с высокой вероятностью незакончено, что бы там ни показывал детектор тишины. «Мой номер заказа 4471» — скорее закончено. «Ну я даже не знаю, как это сказать» — формально законченное предложение, но по контексту человек продолжит.
То есть решение должно смотреть одновременно на два сигнала: акустический (есть ли речь, как долго тишина, что с интонацией к концу реплики) и семантический (выглядит ли уже накопленный транскрипт как завершенная реплика).
Как только мы это сформулировали, архитектура стала очевидной.
Что в итоге получилось
Схема примерно такая:
Быстрый акустический слой. Работает на потоке, определяет речь/не речь и старт паузы. Задача — дешево и с минимальной задержкой сказать «здесь началась тишина». Никаких решений о взятии хода он не принимает.
Инкрементальный ASR. Отдает промежуточные гипотезы, не дожидаясь конца фразы. Это важно: к моменту начала паузы у нас уже есть текст того, что человек сказал.
Классификатор завершенности. Собственная небольшая языковая модель, которая по текущему транскрипту оценивает, выглядит ли реплика законченной. Выход — вероятность, а не бинарное решение.
Адаптивный таймаут. Из этой вероятности считается, сколько ждать. Реплика выглядит явно завершенной — берем ход почти сразу. Реплика оборвана на полуслове — ждем заметно дольше, и это ожидание пользователь воспринимает как нормальное, потому что он сам знает, что не договорил.
Отдельная важная деталь — бэкченнелы. «Ага», «угу», «так-так», «да-да» — это не взятие хода, это сигнал «я тебя слушаю, продолжай». Если ассистент на каждое «угу» замолкает и начинает отвечать, разговор рассыпается. Такие вставки надо распознавать и не считать за перехват.
Второе — barge-in, ситуация, когда человек перебивает уже говорящего ассистента. Тут все зеркально: нужно быстро понять, что это осмысленная речь пользователя, а не эхо собственного синтеза в линии и не кашель, заглушить TTS и переключиться на слушание. Без нормального эхоподавления ассистент бодро перебивает сам себя.
Почему своя модель, а не готовая большая. Две причины. Первая — задержка: этот классификатор дергается очень часто, и он должен отвечать за единицы миллисекунд, поход в чужое облако тут неприемлем. Вторая — данные: у нас есть телефонные разговоры на своем домене, и маленькая модель, обученная на них, обходит общую модель, которая видела в основном чистую студийную речь.
Если конкретно: под классификатор мы взяли rubert-tiny2, 29 млн параметров, и дообучили его на бинарную классификацию «реплика завершена / оборвана». Инференс на CPU укладывается в 6-9 мс. Дергается он не постоянно, а раз в 100 мс с момента, когда акустический слой зафиксировал начало паузы. На вход идет текущая инкрементальная гипотеза ASR и длительность уже накопленной паузы.
Обучали на своих данных: около 12 тысяч телефонных диалогов, из них 40 тысяч размеченных границ ходов.
Про разметку стоит сказать отдельно, потому что это была самая муторная часть. Размечать границы вручную с нуля на таком объеме нереально, поэтому мы пошли от поведения живых операторов. За границу хода принимали момент, когда оператор начинал говорить и клиент не перебивал его в течение следующих двух секунд — то есть человек-оператор своим решением взять ход фактически размечал данные за нас. Логика простая: живой оператор ошибается с определением конца реплики намного реже машины.
Такая эвристика дает шум на быстрых репликах и на диалогах с наложением речи, поэтому спорные случаи, около 5 тысяч, разметили руками.
Метрики: как мы поняли, что стало лучше
Первое, во что мы уперлись — субъективность. «Стало приятнее» это не метрика.
В итоге смотрим на три вещи:
Доля преждевременных перехватов — сколько раз ассистент начал говорить, когда пользователь еще не закончил. Считается по разметке диалогов.
Задержка взятия хода — сколько проходит от реального конца реплики пользователя до начала ответа. Тут важна не только средняя, но и хвост распределения: одно неловкое зависание на четыре секунды портит впечатление сильнее, чем стабильные лишние 200 мс.
Доля диалогов с попыткой пробиться к оператору — косвенная, но самая честная метрика. Когда ассистент бесит, человек начинает давить ноль и кричать «оператор».
Цифры. За бейзлайн взяли то, с чего сами начинали: Silero VAD плюс фиксированный таймаут 900 мс. Замеры на одной и той же выборке диалогов.
Метрика | Бейзлайн | Текущее решение |
|---|---|---|
Преждевременные перехваты | 14% реплик | 3,2% |
Средняя задержка взятия хода | 1200 мс | 480 мс |
95-й перцентиль задержки | 2600 мс | 1100 мс |
Диалоги с попыткой пробиться к оператору | 27% | 16% |
Обратите внимание на предпоследнюю строку. Она интереснее средней задержки: именно хвост распределения отвечает за ощущение «оно тормозит». Пользователь не считает миллисекунды, он запоминает те два раза за разговор, когда повисла неловкая пауза.
По последней метрике честно оговорюсь, что на нее влияет не только endpointing — там и качество ответов, и сценарий. Но динамика после выкатки была заметной и совпала по времени именно с этим изменением.
Чего мы не смогли и где остаются грабли
Пишу этот раздел специально, потому что статьи, где все получилось, читать неинтересно.
Диктовка длинных последовательностей. Номера договоров, адреса, серии документов. Люди диктуют их с рваным ритмом и большими паузами, и на этом сценарии до сих пор приходится переключаться в отдельный режим с более длинными таймаутами, а не полагаться на общую логику.
Речь с сильным фоном. Улица, метро, громкая связь в машине. Акустический слой начинает шуметь, семантический получает битый транскрипт, качество решения падает.
Люди, которые думают вслух. Есть категория собеседников, которые проговаривают процесс: «так, сейчас, у меня тут где-то было, секунду». Формально это законченные конструкции, семантически — тоже, а на деле человек продолжит. Помогает только эвристика по контексту сценария.
Мы все равно слышим, что это машина. Хочу это проговорить прямо, потому что в маркетинговых текстах обычно пишут наоборот. Речь идет не о том, чтобы ассистента нельзя было отличить от человека. Речь о том, чтобы диалог не раздражал. Это разные планки, и вторая достижима, а первая пока нет.
Зачем это все
Короткий ответ: без этого ассистент не выходит в прод.
Мы можем сколько угодно улучшать качество ответов и подключать красивый синтез, но если система лезет в середину фразы, пользователь перестает воспринимать ее как собеседника через минуту разговора. Причем в отчетах это почти не видно — диалог формально завершен, интент распознан, а осадок у человека остался.
Хорошая новость в том, что задача решаемая и не требует гигантских моделей. Плохая — что решается она не одним компонентом, а связкой, и большая часть работы это не «прикрутить LLM», а нудная возня с бюджетом задержки, разметкой границ ходов и разбором конкретных провалившихся диалогов.
Если у кого-то есть опыт с семантическим endpointing — особенно интересно, как вы размечали данные и на чем считаете качество. Мы явно набили не все шишки.

