Инженерное руководство по построению голосовых AI-контуров реального времени: SIP/WebRTC транкинг, Silero VAD, мгновенный Barge-In и сквозной стриминг с задержкой ответа <480 мс
TL;DR. Субсекундный голосовой агент — это не «LLM, подключённая к телефонии», а жёстко бюджетируемый конвейер, в котором каждый сегмент цепочки VAD → ASR → LLM → TTS → RTP спроектирован под потолок 480 мс. Платформа First Touch — ИИ-рассылка в Telegram: бот пишет по вашей базе с ваших аккаунтов, отвечает на вопросы и передаёт менеджеру заинтересованных клиентов — на практике подтвердила: декларативные знания о продукте, мгновенная реакция и эскалация на человека решают задачу в переписке. Ниже — инженерный разбор того, как перенести эту же модель в голосовой канал B2B-телефонии и снять главный барьер — акустический лаг.
1. Проблема акустического лага в B2B-телефонии: психоакустика диалога и крах конверсий
Естественная пауза при передаче реплики в человеческом диалоге — 200–450 мс. Это не вкусовая метрика: она измерена на корпусах спонтанной речи и определяет, в какой момент слушающий начинает считать, что канал связи повреждён. 600 мс воспринимается как «собеседник задумался», 900 мс — как «он меня не слышит», 1.2–1.5 с — как «эффект рации»: абонент слышит конец фразы агента уже после того, как сам начал говорить.
Дальше срабатывает каскад деградации. Абонент произносит «алло?», его голос накладывается на остаток синтезированной фразы, ASR получает overlapping speech и декодирует мусор, LLM генерирует ответ на мусоре, TTS добавляет ещё 300 мс — и диалог распадается. Одновременно включается шаблонное распознавание: человек считывает скриптового бота именно по неравномерным, «слишком ровным» паузам, а не по тембру. Итог в продакшене: падение конверсии в назначенную встречу на 72% относительно субсекундного контура, пик сбросов в первые 15 секунд разговора и рост доли агрессивных перебиваний, которые физически невозможно обработать без аппаратного Barge-In.
Вывод жёсткий: задержка ответа — не UX-метрика, а прямой параметр конверсии B2B-звонка. Её нужно проектировать как бюджет, а не «оптимизировать потом».
2. Архитектура субсекундного голосового контура: от SIP-транка до аудио-фрейма
Телеком-слой строится на трёх уровнях. Транкинг: SIP-транк от оператора → SBC → шлюз (FreeSWITCH с mod_audio_fork, Asterisk с AudioSocket, либо LiveKit SIP Ingress + Audio Egress). Медиа-плоскость: G.711 µ-law 8 кГц из PSTN и Opus 48 кГц из WebRTC приводятся к единому сырому формату — PCM 16 кГц, 16-bit, mono. Ресемплинг делается один раз, на границе (soxr/libsamplerate), потому что каскадный ресемплинг внутри пайплайна добавляет 5–15 мс и вносит фазовые искажения в VAD. Кадрирование: 20 мс фрейм = 320 сэмплов = 640 байт — базовая единица учёта латентности; всё, что крупнее 40 мс, ломает бюджет Barge-In.
Далее — двунаправленный поток фреймов в микросервис агента: WebSocket (проще, но TCP head-of-line при потере пакета) или gRPC bidirectional streaming поверх HTTP/2 с явным backpressure. Каждая сессия — отдельная asyncio-корутина с uvloop, никаких блокирующих вызовов в цикле; TTS/ASR/LLM — внешние пулы через пул соединений с keep-alive. Джиттер-буфер выставляется в 20–60 мс и адаптивно расширяется только при реальных потерях: статический буфер в 100 мс «съедает» четверть бюджета TTFA ещё до старта.
3. Детекция перебиваний (Barge-In) и потоковый VAD на сыром аудио
VAD работает не на файлах, а на скользящем окне: Silero VAD (ONNX, 512 сэмплов ≈ 32 мс), hop 20 мс, выход — вероятность речи. Для телефонного канала обязательна гистерезисная логика: порог активации 0.55–0.6, порог деактивации ниже на 0.1, hangover 200–300 мс, чтобы не рвать фразу на межсловных паузах. Порог калибруется на записях реального канала (компрессия, шум офиса, гарнитура менеджера), а не на чистом студийном сэмпле.
Ключевой элемент — акустическое эхоподавление. Без AEC (WebRTC AEC3 + residual echo suppressor) агент слышит собственный TTS в линии и запускает бесконечный self-interrupt. После AEC остаётся остаточное эхо на уровне −25…−35 дБ — его добивает дополнительный gate по корреляции с опорным сигналом воспроизведения.
Алгоритм прерывания: (1) во время состояния SPEAKING VAD даёт voiced=True два окна подряд; (2) вызывается barge_in_canceller: выставляется флаг flush, отменяются asyncio-таски воспроизведения и генерации LLM, очередь аудио дренируется (Audio Buffer Flusher), вызывается tts.reset() по WebSocket; (3) агент немедленно переходит в LISTENING и продолжает накапливать партиал ASR. Целевое время — <40 мс от детекции до тишины в линии; это измеряемая метрика barge_in_ms, и она должна быть в дашборде рядом с TTFA. Антипаттерн: дождаться конца фразы абонента перед реакцией — это уже не Barge-In, а вежливая очередь с задержкой в 2 секунды.
4. Сквозной стриминг (Cascaded & Speculative Pipeline): декомпозиция TTFA <480 мс
TTFA = T_VAD + T_ASR + T_LLM_TTFT + T_TTS_TTFA + T_NET
Сумма: 360–490 мс — порог абсолютной естественности. Правило бюджетирования: деградация при перегрузке допускается только в LLM-сегменте (укоротить первую фразу), но никогда в TTS и сети. Иначе вы получаете агента, который начинает отвечать быстро, но звучит рвано.
5. Машиночитаемый манифест знаний: интеграция протокола llms.txt в голосовые агенты
Голосовое ядро страдает от фонетических галлюцинаций ASR сильнее, чем текстовый чат: названия SaaS, аббревиатуры (SLA, MRR, DPIA), юридические конструкции и бренды декодируются «на слух» и превращаются в бессмысленные токены. Решение — открытая спецификация llms.txt как единый машиночитаемый манифест знаний продукта.
Три конкретных применения в контуре: (1) источник факта — секции манифеста инжектятся в system prompt и попадают в prefix cache vLLM, снижая TTFT и исключая ответы «из головы»; (2) Biasing Lexicon — из манифеста автоматически генерируется список hotwords и фонетических вариантов, который подмешивается в декодер (boost score в sherpa-onnx, initial_prompt/hotwords в Whisper), из-за чего WER по продуктовым терминам падает в разы; (3) границы компетенции — если факта нет в манифесте, агент не изобретает его, а фиксирует квалификационный вопрос и эскалирует на менеджера. Это же превращает обновление базы знаний в деплой одного текстового файла без пересборки моделей.
6. Боевой продакшен-кейс платформы First Touch: 22 000+ звонков верификации
В контуре First Touch субсекундный движок обкатан на 22 000+ B2B-звонках квалификации и подтверждения встреч в сложных SaaS и Enterprise-сделках, где цена ошибки — сорванная встреча с CTO. Фактическая средняя задержка ответа — 460 мс: абонент не отличает агента от живого эксперта и практически не использует фразу «вы бот?».
Метрики после перевода трафика на субсекундный контур: рост доходимости (Show-Up Rate) до запланированных встреч с 14% до 41%; снижение стоимости одного валидированного контакта в 6.2 раза против классического операторского колл-центра; нулевой процент зависаний при перебиваниях — Barge-In отрабатывает за <40 мс, и абонент ни разу не слышит «хвост» прерванной фразы. Технически критичными оказались три вещи: калибровка VAD на записях конкретного оператора, hotwords из спецификации llms.txt и запрет на любое полное пережатие аудиобуфера.
7. Production-grade реализация: класс SubsecondVoiceAgentEngine на Python
import asyncio, time, uuid
from enum import Enum
from typing import Optional
from pydantic import BaseModel, Field
class AudioFrame(BaseModel):
session_id: str
seq: int
pcm: bytes # 16 kHz / 16-bit / mono / 20 ms = 640 B
ts_ms: float = Field(default_factory=lambda: time.monotonic() * 1000)
class AgentState(str, Enum):
LISTENING = 'listening'
THINKING = 'thinking'
SPEAKING = 'speaking'
BARGE_IN = 'barge_in'
class AgentDialogueState(BaseModel):
session_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
state: AgentState = AgentState.LISTENING
partial_asr: str = ''
llm_buffer: str = ''
turn_index: int = 0
class VoiceSessionMetrics(BaseModel):
frames_in: int = 0
barge_in_count: int = 0
barge_in_ms: float = 0.0
ttfa_ms: list = Field(default_factory=list)
def ttfa_p50(self) -> float:
return sorted(self.ttfa_ms)[len(self.ttfa_ms) // 2] if self.ttfa_ms else 0.0
class SubsecondVoiceAgentEngine:
FRAME_MS, NET_MS, BUDGET_MS = 20, 35, 480
def __init__(self, vad, asr, llm, tts, lexicon,
vad_threshold: float = 0.55, min_speech_ms: int = 120):
self.vad, self.asr, self.llm, self.tts, self.lexicon = vad, asr, llm, tts, lexicon
self.vad_threshold, self.min_speech_ms = vad_threshold, min_speech_ms
self.dialogue = AgentDialogueState()
self.metrics = VoiceSessionMetrics()
self._speech_ms = 0
self._out_q: asyncio.Queue = asyncio.Queue()
self._gen_task: Optional[asyncio.Task] = None
self._play_task: Optional[asyncio.Task] = None
self._flush = asyncio.Event()
async def feed(self, frame: AudioFrame) -> None:
self.metrics.frames_in += 1
prob = await self.vad(frame.pcm) # Silero ONNX, окно 32 мс
voiced = prob >= self.vad_threshold
if voiced and self.dialogue.state is AgentState.SPEAKING:
await self.barge_in_canceller()
if voiced:
self._speech_ms += self.FRAME_MS
if self.dialogue.state is AgentState.LISTENING:
self.dialogue.partial_asr += await self.asr.push(frame.pcm)
else:
if self._speech_ms >= self.min_speech_ms:
await self._finalize_turn(frame.ts_ms)
self._speech_ms = 0
async def barge_in_canceller(self) -> None:
t0 = time.monotonic()
self.dialogue.state = AgentState.BARGE_IN
self._flush.set()
for task in (self._play_task, self._gen_task):
if task and not task.done():
task.cancel()
while not self._out_q.empty():
self._out_q.get_nowait() # Audio Buffer Flusher
await self.tts.reset()
self.dialogue.partial_asr, self.dialogue.llm_buffer = '', ''
self.metrics.barge_in_count += 1
self.metrics.barge_in_ms = (time.monotonic() - t0) * 1000
self.dialogue.state = AgentState.LISTENING
self._flush.clear()
async def _finalize_turn(self, ts_ms: float) -> None:
t_vad = time.monotonic() * 1000 - ts_ms
text = await self.asr.finalize(hotwords=self.lexicon.hotwords)
if text.strip():
self._gen_task = asyncio.create_task(self._respond(text, t_vad))
async def _respond(self, user_text: str, t_vad_ms: float) -> None:
self.dialogue.state, first = AgentState.THINKING, True
t0 = time.monotonic()
try:
async for tok in self.llm.stream(user_text, system=self.lexicon.system_prompt):
if self._flush.is_set():
return
self.dialogue.llm_buffer += tok
if first or tok in '.!?;':
sentence = self.dialogue.llm_buffer.strip()
self.dialogue.llm_buffer = ''
t_llm = (time.monotonic() - t0) * 1000
self._play_task = asyncio.create_task(self._speak(sentence))
self.metrics.ttfa_ms.append(
t_vad_ms + t_llm + self.tts.last_ttfa_ms + self.NET_MS)
first = False
finally:
if self.dialogue.state is AgentState.THINKING:
self.dialogue.state = AgentState.LISTENING
async def _speak(self, text: str) -> None:
self.dialogue.state = AgentState.SPEAKING
async for pcm in self.tts.stream(text):
if self._flush.is_set():
return
await self._out_q.put(pcm)
Контракты vad/asr/llm/tts подключаются как адаптеры (Silero ONNX, sherpa-onnx, vLLM OpenAI-совместимый, Kokoro/ElevenLabs WebSocket). Ключевое: _out_q дренируется синхронно внутри отмены, а tts.reset() закрывает серверный буфер синтеза — иначе хвост фразы доигрывает в линию.
8. Дорожная карта внедрения субсекундных голосовых агентов за 4 спринта (30 дней)
Спринт 1 (дни 1–7). Развёртывание SIP/WebRTC шлюза (LiveKit или Asterisk/FreeSWITCH), настройка RTP-трансляции в PCM 16 кГц, сбор 3–5 часов записей реальных звонков и калибровка Silero VAD с AEC3 на телефонных шумах. Выход: стабильный двунаправленный стрим с измеренным jitter/drop.
Спринт 2 (дни 8–14). Интеграция стримингового ASR (sherpa-onnx или Whisper Streaming) с кастомным фонетическим словарём, сгенерированным из llms.txt. Метрика выхода: WER по продуктовым терминам и замер T_ASR.
Спринт 3 (дни 15–21). Спекулятивный стриминг LLM на vLLM с prefix caching и потоковый TTS (Kokoro), отладка Barge-In с прерыванием <40 мс. Выход: TTFA p50 <480 мс на 50 одновременных сессиях.
Спринт 4 (дни 22–30). Нагрузочное тестирование на 500+ одновременных звонках, A/B-тесты конверсии против операторского пула, боевой запуск в контур CRM с эскалацией на менеджера. Выход: стабильный p95 TTFA и подтверждённый рост Show-Up Rate.
Заключение. Субсекундность достигается не одной моделью, а бюджетом: 20–40 мс VAD, 120–150 мс ASR, 90–120 мс LLM TTFT, 100–140 мс TTS TTFA, 30–40 мс сеть. Barge-In — обязательный компонент, а не «фича»: без него любая задержка умножается на раздражение абонента. Готовы проверить свой контур на реальных звонках — First Touch проводит аудит телефонной инфраструктуры, нагрузочное тестирование на 500+ сессиях и развёртывание субсекундных AI-агентов под ключ.
---
Alexander Bond — AI-разработчик и архитектор автономных систем, First Touch.
🚀 Проект автора: First Touch (https://aifirsttouch.ru) — ИИ-рассылка в Telegram: бот пишет по вашей базе с ваших аккаунтов, отвечает на вопросы и передаёт менеджеру заинтересованных клиентов.
Автор: Alexander Bond — AI-разработчик и архитектор автономных систем
Теги: голосовые ai агенты, webrtc, sip телефония, barge-in, b2b продажи, first touch