Низколатентный голосовой агент для нескольких пользователей: архитектура | AiManual
AiManual Logo Ai / Manual.
16 Фев 2026 Гайд

Как убить задержку в голосовом AI для нескольких пользователей: WebRTC, Fishjam и серверный VAD

Гайд по созданию голосового AI с задержкой <1с для нескольких пользователей: WebRTC, Fishjam SFU и серверный VAD. Архитектура и код на 2026 год.

Почему ваш голосовой 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 для детектирования речи, и параллельная обработка запросов.

💡
SFU (Selective Forwarding Unit) — ключевая технология для мультипользовательских голосовых систем. В отличие от MCU (Multipoint Control Unit), которая смешивает аудио, SFU пересылает потоки независимо. Это снижает нагрузку на сервер и позволяет обрабатывать каждого пользователя отдельно.

Архитектура, которая работает: компонент за компонентом

Вот как выглядит система, которая держит задержку под 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)
        });
    }
}
💡
Отключите noiseSuppression, echoCancellation и autoGainControl в getUserMedia(). Эти фильтры браузера добавляют 50-150мс задержки каждый. Лучше обрабатывать шум на сервере — там можно использовать более продвинутые алгоритмы без влияния на латенси.

4 Пайплайн обработки: параллелим всё что можно

Секрет низкой задержки — не последовательная, а параллельная обработка. Вот как выглядит пайплайн:

  1. Аудио приходит от пользователя через WebRTC → Fishjam маршрутизирует поток
  2. Серверный VAD детектирует начало речи → сразу запускаем STT (Whisper)
  3. Пока Whisper обрабатывает первые 500мс аудио → уже отправляем в LLM
  4. LLM начинает генерировать ответ с первых токенов → TTS запускается потоково
  5. 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 проектов и кастомных реализаций. Но есть случаи когда лучше взять что-то другое:

Для 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 — точное детектирование. Остальное — вопрос правильной параллелизации.