Проблема: хочу голосового ассистента, но ML-инженеров нет
История типична: руководство поставило задачу - сделать голосового ассистента для внутреннего использования. Данные конфиденциальные, облачные API не подходят. Команда из 4 backend-разработчиков на Python. Опыта с нейросетями - ноль. Срок - полгода. Бюджет - одна серверная стойка с парой RTX 4090.
Не повторяйте эту ошибку: сначала изучите основы ML. Мы этого не сделали и потратили месяц впустую.
Решение: локальный стек, который реально работает
После недель экспериментов с кучей моделей, остановились на связке Qwen 8B для генерации ответов и BERT для поиска по документам. Почему? Qwen 8B - одна из немногих моделей, которая адекватно работает на 8 миллиардах параметров без танцев с бубном. BERT - классика для понимания текста, его дообучить проще простого.
Архитектура: 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 для частых запросов. И самое главное - конвейеризация: пока идет генерация ответа, уже начинаем синтез речи для первых слов. Но это сложно, мы не стали делать.
Ошибки, которые чуть не убили проект
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 выпустит устройство без экрана, как предсказывают в новостях, и все такие системы станут ненужными. Но пока - это работает.