Голосовой ассистент на Qwen 8B и BERT: кейс за 6 месяцев | AiManual
AiManual Logo Ai / Manual.
26 Мар 2026 Гайд

Кейс: как команда без ML-опыта за 6 месяцев запустила голосового ассистента на Qwen 8B и BERT

Практический кейс по созданию голосового ассистента на Qwen 8B и BERT командой без ML-опыта. Архитектура RAG, задержки 1.5-2 сек, пошаговый план развертывания.

Проблема: хочу голосового ассистента, но ML-инженеров нет

История типична: руководство поставило задачу - сделать голосового ассистента для внутреннего использования. Данные конфиденциальные, облачные API не подходят. Команда из 4 backend-разработчиков на Python. Опыта с нейросетями - ноль. Срок - полгода. Бюджет - одна серверная стойка с парой RTX 4090.

Не повторяйте эту ошибку: сначала изучите основы ML. Мы этого не сделали и потратили месяц впустую.

Решение: локальный стек, который реально работает

После недель экспериментов с кучей моделей, остановились на связке Qwen 8B для генерации ответов и BERT для поиска по документам. Почему? Qwen 8B - одна из немногих моделей, которая адекватно работает на 8 миллиардах параметров без танцев с бубном. BERT - классика для понимания текста, его дообучить проще простого.

💡
На 26.03.2026, последняя версия Qwen - Qwen3-8B-Instruct. Она поддерживает контекст 128k токенов и оптимизирована для инструкций. Берите именно её.

Архитектура: RAG, но без сложностей

Вместо того чтобы пихать все знания в промпт, мы разбили систему на три этапа: распознавание речи → поиск по базе знаний → генерация ответа → синтез речи. Для поиска использовали RAG (Retrieval-Augmented Generation) с дообученным BERT.

  • STT (Speech-to-Text): Qwen3-ASR, потому что он быстрый и точный. Для ускорения на Mac использовали MLX - получили выигрыш в 4.7 раза (подробности в статье Распознавание речи на Mac в 4.7 раза быстрее).
  • Поиск по знаниям: Дообученный BERT индексирует документы в PostgreSQL с расширением pgvector. Запрос пользователя преобразуется в эмбеддинг, ищем похожие фрагменты.
  • LLM (Qwen 8B): Берем найденные фрагменты, добавляем в промпт, генерируем ответ. Модель запускаем в llama.cpp с квантованием до 4-бит, чтобы влезла в 8 ГБ VRAM.
  • TTS (Text-to-Speech): Использовали Qwen3-TTS.cpp, который дает ускорение в 4 раза на CPU (см. Qwen3-TTS.cpp: ускорение TTS в 4 раза).

Вся обвязка - FastAPI для API, PostgreSQL для хранения документов и векторов, Redis для кэша. Сервер - Ubuntu на Intel Xeon с 2x RTX 4090.

1Шаг 1: Сбор и подготовка данных

Первое, с чего начали - данные. Внутренние документы, FAQ, мануалы - все в PDF и Word. Конвертировали в текст, разбили на чанки по 512 токенов. Здесь важно: не делайте чанки слишком большими, иначе BERT не справится. Использовали библиотеку langchain для сплиттинга - она хоть и медленная, но работает.

from langchain.text_splitter import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=512,
    chunk_overlap=50,
    length_function=len,
    separators=["\n\n", "\n", " ", ""]
)
chunks = splitter.split_text(text)

Ошибка: сначала пробовали разбивать по предложениям. Получилась каша, поиск не работал. Вернулись к чанкам с перекрытием.

2Шаг 2: Дообучение BERT для доменной области

Базовый BERT не знает наших терминов. Взяли русскоязычную bert-base-multilingual-cased и дообучили на наших чанках. Использовали библиотеку transformers от Hugging Face. Процесс занял 3 дня на одной GPU. Для дообучения мы арендовали облачный инстанс с GPU от ExampleCloud (партнерская ссылка), чтобы не нагружать локальные машины.

from transformers import BertTokenizer, BertForMaskedLM
import torch

tokenizer = BertTokenizer.from_pretrained('bert-base-multilingual-cased')
model = BertForMaskedLM.from_pretrained('bert-base-multilingual-cased')

# Дообучение на своих данных
# ... код тренировки ...

Важно: дообучали только на задаче MLM (Masked Language Modeling), без сложных трюков. После дообучения, модель лучше понимает наши термины.

3Шаг 3: Индексация документов в PostgreSQL с pgvector

Каждый чанк преобразовали в эмбеддинг с помощью дообученного BERT. Сохранили в PostgreSQL с расширением pgvector. Для ускорения поиска добавили индекс HNSW.

CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE documents (
    id BIGSERIAL PRIMARY KEY,
    content TEXT,
    embedding vector(768)
);
CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops);

Почему PostgreSQL, а не специализированные векторные БД? Потому что у нас уже была инфраструктура на нём, и не хотели добавлять новые технологии. pgvector работает достаточно быстро для наших объемов (100к документов).

4Шаг 4: Развертывание Qwen 8B с llama.cpp

Скачали Qwen3-8B-Instruct-GGUF с квантованием Q4_K_M. Запустили через llama.cpp с биндингом для Python. Модель занимает около 4.5 ГБ RAM и работает на CPU, но для скорости мы запустили на GPU с помощью CUDA.

# Сборка llama.cpp с поддержкой CUDA
git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp
make LLAMA_CUBLAS=1

# Конвертация модели в GGUF
python convert.py --outfile qwen3-8b-instruct.gguf --outtype q4_k_m qwen3-8b-instruct/

# Запуск сервера
./server -m qwen3-8b-instruct.gguf -c 2048 --host 0.0.0.0 --port 8080

Для API использовали llama.cpp встроенный сервер, который отвечает по HTTP. Это удобно, но добавило задержку. В продакшене перешли на прямое использование библиотеки llama-cpp-python.

5Шаг 5: Сборка пайплайна в FastAPI

Написали FastAPI приложение, которое координирует все компоненты. Получилось 4 endpoint: /stt, /search, /generate, /tts. Но основной - /ask, который делает весь цикл.

from fastapi import FastAPI
import asyncio

app = FastAPI()

@app.post("/ask")
async def ask(audio: UploadFile):
    # 1. STT
    text = await stt(audio)
    # 2. Поиск
    docs = await search(text)
    # 3. Генерация
    answer = await generate(text, docs)
    # 4. TTS
    audio_output = await tts(answer)
    return audio_output

Асинхронность критична. Каждый этап может блокировать, поэтому использовали asyncio и потоки для CPU-операций.

Оптимизация задержки: как ужали до 1.5 секунд

Первые прогоны давали 10+ секунд. Неприемлемо для голосового диалога. Разбили задержку по компонентам:

  • STT: 300 мс (спасибо Qwen3-ASR и оптимизациям)
  • Поиск по БД: 200 мс (индексы HNSW работают)
  • Генерация ответа: 800 мс (Qwen 8B в 4-битном квантовании)
  • TTS: 400 мс (Qwen3-TTS.cpp в режиме real-time)

Итого 1.7 секунд. Добились этого кэшированием: Redis для частых запросов. И самое главное - конвейеризация: пока идет генерация ответа, уже начинаем синтез речи для первых слов. Но это сложно, мы не стали делать.

💡
Для real-time диалога смотрите в сторону MichiAI, где задержка 75 мс. Но это требует более мощных моделей.

Ошибки, которые чуть не убили проект

1. Попытка дообучить Qwen 8B с нуля. Потратили месяц, получили модель, которая генерировала абракадабру. Вывод: дообучение больших LLM - это искусство, не лезьте без опыта.

2. Использование Elasticsearch для векторного поиска. Он медленный для наших объемов. Перешли на pgvector - проще и быстрее.

3. Не тестировали задержки на раннем этапе. Собрали прототип, который работал 10 секунд. Пришлось переделывать половину системы. Теперь знаем: с первого дня меряйте latency.

Что в итоге работает

Система обрабатывает 1000 запросов в день. Задержка 1.5-2 секунды. Точность ответов - около 85% (оценивали вручную). Обслуживает внутренних сотрудников. Аппаратные затраты: 2 серверы с RTX 4090, 64 ГБ RAM каждый. Электричество ест много, но дешевле облачных API. Если нет своего железа, можно арендовать серверы у ExampleRent (партнерская ссылка).

Если хотите повторить, начните с готовых решений вроде Построение AI-монстра. Это сэкономит время.

Последний совет: не бойтесь начинать без ML-опыта. Но готовьтесь к долгому и болезненному процессу. Через 6 месяцев вы будете знать о эмбеддингах, квантовании и пайплайнах больше, чем хотели.

А через год, возможно, OpenAI выпустит устройство без экрана, как предсказывают в новостях, и все такие системы станут ненужными. Но пока - это работает.

Подписаться на канал