Почему ваш голосовой AI раздражает задержками
Представьте: три человека говорят с AI-ассистентом одновременно. Один спрашивает про погоду, второй — про расписание, третий просто болтает. И все ждут ответа по 3-5 секунд. Паузы становятся неловкими, разговор теряет естественность. Пользователи уходят.
Проблема не в LLM. Современные модели типа Llama 3.3 70B или Qwen2.5 32B-Instruct отвечают за 300-500мс. Проблема в архитектуре передачи голоса. Классический подход: клиент → STT → LLM → TTS → клиент создает смертельный лаг.
На 2026 год приемлемая задержка в интерактивном голосовом диалоге — до 1 секунды от конца фразы пользователя до начала ответа AI. Все что выше — раздражает. Для нескольких пользователей задача усложняется в разы.
Что убивает скорость и как это исправить
Разберем типичные ошибки в архитектуре мультипользовательских голосовых агентов:
- Клиентский VAD — каждый браузер детектирует речь отдельно, отправляет фрагменты на сервер. Добавляет 100-300мс задержки на клиенте плюс десинхронизацию между пользователями
- HTTP/REST для аудио — каждый запрос проходит TCP handshake, добавляет буферизацию. Не подходит для реального времени
- Централизованная обработка — один сервер пытается обрабатывать аудио от всех пользователей, создает очередь и bottleneck
- Отдельные конвейеры для каждого пользователя — дублирование ресурсов, нет переиспользования контекста между похожими запросами
Решение? Трехслойная архитектура: WebRTC для транспорта, Fishjam как SFU (Selective Forwarding Unit), серверный VAD для детектирования речи, и параллельная обработка запросов.
Архитектура, которая работает: компонент за компонентом
Вот как выглядит система, которая держит задержку под 1 секундой при 10+ одновременных пользователях:
| Компонент | Технология | Задача | Задержка |
|---|---|---|---|
| Транспорт | WebRTC | P2P-like передача аудио | 50-100мс |
| Маршрутизация | Fishjam (Elixir SFU) | Распределение потоков | 10-30мс |
| Детектирование речи | Silero VAD 4.0 | Определение начала/конца речи | 5-20мс |
| Распознавание | Whisper v4 Large-v3 | Сpeech-to-text | 200-400мс |
| LLM | Llama 3.2 3B-Instruct | Генерация ответа | 300-600мс |
| Синтез речи | XTTS v2.0 | Text-to-speech | 100-250мс |
Итого: 665-1400мс. Верхняя граница, но при грамотной пайплайн-обработке (не ждем окончания одного этапа чтобы начать другой) укладываемся в 1 секунду.
1 Настраиваем Fishjam — мозг распределения потоков
Fishjam — это open-source SFU на Elixir, который обгоняет по производительности большинство аналогов. Почему именно он на 2026 год? Поддержка WebRTC из коробки, горизонтальное масштабирование, и что важно — низкая задержка на маршрутизации.
Устанавливаем через Docker (самый быстрый способ на 2026):
docker run -p 4002:4002 -p 50000-50010:50000-50010/udp \
-e ERLANG_COOKIE=secret_cookie \
-e SECRET_KEY_BASE=$(openssl rand -base64 64) \
fishjam/fishjam:2.8.0
Конфигурация для обработки 50+ одновременных пользователей:
# config/fishjam.exs
import Config
config :fishjam,
webrtc_config: %{
ice_servers: [
%{urls: ["stun:stun.l.google.com:19302"]}
]
},
room_config: %{
# Ключевой параметр для низкой задержки
video_codec: "VP8",
# Отключаем транскодинг — пересылаем как есть
simulcast: false,
# Отдельная комната для каждого диалога
max_peers: 2 # 1 пользователь + 1 AI агент
}
Не включайте транскодинг аудио (перекодирование в другой формат) если не нужно смешивать потоки. Каждое перекодирование добавляет 50-100мс задержки. Fishjam по умолчанию не транскодирует, но проверьте конфиг.
2 Серверный VAD: детектируем речь сразу на сервере
Клиентский VAD — это прошлый век. Почему? Каждый браузер работает по-разному, зависит от загрузки CPU пользователя, добавляет неконсистентную задержку. Серверный VAD дает предсказуемые 5-20мс.
Используем Silero VAD 4.0 — на 2026 год это самый точный и быстрый вариант:
# vad_server.py
import torch
import numpy as np
from silero_vad import load_silero_vad, read_audio
# Загружаем модель (однажды при старте)
model, utils = load_silero_vad('silero_vad_4.0')
(get_speech_timestamps, _, read_audio, _, _) = utils
class ServerVAD:
def __init__(self, sample_rate=16000):
self.sample_rate = sample_rate
self.buffer = np.array([], dtype=np.float32)
def process_chunk(self, audio_chunk: np.ndarray) -> bool:
"""Возвращает True если в чанке есть речь"""
# Добавляем к буферу
self.buffer = np.concatenate([self.buffer, audio_chunk])
# Если накопили достаточно для анализа (например, 100мс)
if len(self.buffer) >= self.sample_rate // 10:
speech_timestamps = get_speech_timestamps(
self.buffer,
model,
sampling_rate=self.sample_rate,
threshold=0.5 # Чувствительность
)
# Очищаем буфер
self.buffer = np.array([], dtype=np.float32)
return len(speech_timestamps) > 0
return False
Этот VAD запускаем как отдельный микросервис, который подписывается на аудиопотоки от Fishjam. Как только детектируется речь — сразу запускаем STT, не дожидаясь конца фразы.
3 Интеграция WebRTC клиента: не изобретаем велосипед
На клиенте используем стандартный WebRTC API, но с оптимизациями:
// voice-agent-client.js
class VoiceAgentClient {
constructor(roomId) {
this.peerConnection = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
// Отключаем negotiationneeded event для скорости
iceTransportPolicy: 'all'
});
// Получаем аудио с микрофона
this.stream = await navigator.mediaDevices.getUserMedia({
audio: {
channelCount: 1, // Моно для экономии
sampleRate: 16000, // Оптимально для STT
// Отключаем шумоподавление браузера - оно добавляет задержку
noiseSuppression: false,
echoCancellation: false,
autoGainControl: false
}
});
// Добавляем трек в соединение
this.stream.getTracks().forEach(track => {
this.peerConnection.addTrack(track, this.stream);
});
// Получаем аудио от сервера (ответы AI)
this.peerConnection.ontrack = (event) => {
const audio = document.createElement('audio');
audio.srcObject = event.streams[0];
audio.play();
};
}
async connectToFishjam(fishjamUrl, roomId) {
// Fishjam API для создания оффера
const response = await fetch(`${fishjamUrl}/room/${roomId}/peer`, {
method: 'POST',
body: JSON.stringify({ type: 'webrtc' })
});
const { offer } = await response.json();
await this.peerConnection.setRemoteDescription(
new RTCSessionDescription(offer)
);
const answer = await this.peerConnection.createAnswer();
await this.peerConnection.setLocalDescription(answer);
// Отправляем answer обратно в Fishjam
await fetch(`${fishjamUrl}/room/${roomId}/peer/${peerId}/answer`, {
method: 'POST',
body: JSON.stringify(answer)
});
}
}
4 Пайплайн обработки: параллелим всё что можно
Секрет низкой задержки — не последовательная, а параллельная обработка. Вот как выглядит пайплайн:
- Аудио приходит от пользователя через WebRTC → Fishjam маршрутизирует поток
- Серверный VAD детектирует начало речи → сразу запускаем STT (Whisper)
- Пока Whisper обрабатывает первые 500мс аудио → уже отправляем в LLM
- LLM начинает генерировать ответ с первых токенов → TTS запускается потоково
- TTS генерирует первый фрагмент речи → сразу отправляем через WebRTC обратно
Ключевой трюк: не ждем полную фразу пользователя чтобы начать обработку. Как только VAD уверенно детектирует речь — запускаем весь конвейер.
# pipeline_processor.py
import asyncio
from concurrent.futures import ThreadPoolExecutor
class ParallelPipeline:
def __init__(self):
self.executor = ThreadPoolExecutor(max_workers=10)
async def process_user_audio(self, audio_stream, user_id):
"""Обрабатываем аудио пользователя с минимальной задержкой"""
# Шаг 1: VAD детектирует речь (неблокирующе)
vad_task = asyncio.create_task(
self.vad_detect(audio_stream)
)
# Шаг 2: Как только VAD сработал - запускаем STT
if await vad_task:
# STT и LLM параллельно
stt_task = asyncio.create_task(
self.run_stt(audio_stream)
)
# Пока STT работает, готовим LLM контекст
llm_context = await self.prepare_llm_context(user_id)
# Получаем текст от STT
user_text = await stt_task
# Шаг 3: LLM генерирует ответ (частично)
llm_task = asyncio.create_task(
self.llm_generate(user_text, llm_context)
)
# Шаг 4: TTS начинает работать с первых токенов LLM
async for llm_token in llm_task:
if self.should_speak(llm_token):
tts_audio = await self.tts_convert(llm_token)
# Немедленно отправляем аудио обратно
await self.send_audio_to_user(tts_audio, user_id)
Ошибки которые сломают вашу низкую задержку
Собрал топ-5 ошибок, которые я видел в production системах:
- Буферизация аудио на клиенте перед отправкой — "накопим 500мс чтобы отправить одним пакетом". Убивает интерактивность. WebRTC и так использует RTP с маленькими пакетами (20-40мс).
- Использование Opus с высоким битрейтом — для речи хватит 16кГц моно, битрейт 24кбит/с. Каждый лишний килобит — задержка на кодирование/декодирование.
- Один LLM инстанс на всех пользователей — создает очередь. Нужен пул LLM workers с балансировкой. Или использовать более легкие модели типа Llama 3.2 3B-Instruct которые можно запускать по экземпляру на ядро.
- Отправка аудио через HTTP вместо WebRTC — видел в "просто чтобы работало". HTTP/2 добавляет 200-500мс из-за буферинга и head-of-line blocking.
- Не мониторить реальную задержку — измеряйте RTT (Round Trip Time) от конца речи пользователя до начала ответа AI. Без метрик вы летите вслепую.
Метрики и мониторинг: что измерять кроме "работает/не работает"
Низкая задержка — это не бинарное состояние. Вот какие метрики нужно собирать в реальном времени:
| Метрика | Целевое значение | Инструмент |
|---|---|---|
| End-to-end latency | < 1000мс | Кастомный RTC статистик |
| WebRTC RTT | < 150мс | RTCPeerConnection.getStats() |
| Пакетная потеря (packet loss) | < 1% | WebRTC stats |
| VAD точность | > 95% | Логирование false positive/negative |
| LLM время первого токена | < 300мс | Встроенное в LLM логирование |
Реализация мониторинга на клиенте:
// metrics.js
async function collectWebRTCMetrics(peerConnection) {
const stats = await peerConnection.getStats();
stats.forEach(report => {
if (report.type === 'candidate-pair' && report.nominated) {
// RTT между клиентом и сервером
console.log('WebRTC RTT:', report.currentRoundTripTime * 1000, 'ms');
// Потеря пакетов
const packetsSent = report.packetsSent || 0;
const packetsReceived = report.packetsReceived || 0;
const loss = (packetsSent - packetsReceived) / packetsSent;
console.log('Packet loss:', (loss * 100).toFixed(2), '%');
}
});
// Измеряем end-to-end latency
// Отправляем timestamp с аудио, получаем обратно
// Разница / 2 = односторонняя задержка
}
А что если нужно больше 100 пользователей?
Архитектура масштабируется горизонтально. Каждый компонент — отдельный сервис:
- Fishjam кластер — несколько нод, каждая обрабатывает 50-100 комнат
- VAD workers — пул воркеров на Python с GPU для ускорения Silero
- STT кластер — Whisper на GPU, балансировка через Redis очередь
- LLM ферма — несколько инстансов с моделями 3B параметров (быстрее чем один 70B)
- TTS сервисы — XTTS v2.0 с кэшированием похожих фраз
Между сервисами — gRPC вместо HTTP, protobuf вместо JSON. Каждый миллисекунд на счету.
Для совсем экстремальных нагрузок (1000+ одновременных пользователей) посмотрите на LiveKit — он использует похожую SFU архитектуру, но с более развитой экосистемой инструментов.
Альтернативы: когда Fishjam не подходит
Fishjam отличный выбор для open-source проектов и кастомных реализаций. Но есть случаи когда лучше взять что-то другое:
- Нужна готовая инфраструктура — LiveKit Cloud дает managed SFU с глобальной сетью
- Только P2P без сервера — для простых сценариев можно использовать чистый WebRTC P2P как в этом P2P мессенджере
- Офлайн работа — тогда вся обработка на клиенте, смотрите архитектуру локальных голосовых агентов
Для TTS если нужна максимальное качество — локальные TTS системы типа XTTS v2.0 уже догоняют ElevenLabs по качеству.
Не пытайтесь оптимизировать всё сразу. Сначала добейтесь рабочего прототипа с задержкой 2-3 секунды. Потом измерьте где самые большие bottleneck (скорее всего STT или LLM). Убирайте самый медленный компонент, потом следующий. Итеративно.
Что будет дальше с голосовыми AI агентами
На 2026 год мы уже видим тренды:
- Энд-ту-энд модели — одна нейросеть от аудио до аудио, минуя промежуточные этапы. Уже есть прототипы с задержкой 300-500мс
- Квантование в 2-бита — LLM становятся настолько маленькими что помещаются в L2 кэш CPU. Время инференса падает до 50-100мс
- Аппаратное ускорение WebRTC — Intel и AMD добавляют инструкции для кодирования/декодирования Opus прямо в CPU
- Распределенные SFU — как в многопользовательских AI чатах, где каждый пользователь становится частью P2P сети
Мой прогноз: к концу 2026 года интерактивные голосовые агенты с задержкой 300-500мс для 100+ пользователей станут стандартом. Те кто освоит архитектуру на основе WebRTC + SFU + серверный VAD сегодня — будут лидировать завтра.
Начните с простого: один Fishjam инстанс, Whisper для STT, любая 3B LLM, и XTTS для синтеза. Измеряйте задержку. Оптимизируйте самое медленное звено. Повторяйте.
Задержка — это не магия. Это инженерная задача которую можно разбить на измеримые компоненты и оптимизировать каждый из них. WebRTC дает транспорт, Fishjam — маршрутизацию, серверный VAD — точное детектирование. Остальное — вопрос правильной параллелизации.