Перейти к содержанию
Публикация AiManual

Как построить экспертного RAG-ассистента на CPU VPS: оптимизация 10k книг, выбор архитектуры и модели эмбеддингов

Пошаговый гайд по созданию экспертного RAG-ассистента на CPU VPS с 16GB RAM. Выбор моделей эмбеддингов, настройка Qdrant с mmap, реранкинг BGE и оптимизация под

Коротко

Что будет в материале

  1. 01

    Зачем мучить 10 000 книг на слабом VPS? Потому что можно

  2. 02

    Архитектура, которая не сломается под нагрузкой

  3. 03

    Ошибки, которые убьют вашу систему

  4. 04

    Что в итоге? Система, которая работает

Зачем мучить 10 000 книг на слабом VPS? Потому что можно

Представьте себе задачу: у вас есть коллекция из 10 000 книг - техническая документация, художественная литература, научные труды. Вы хотите задавать вопросы по этому корпусу и получать точные, обоснованные ответы. Облачные API съедят ваш бюджет за неделю, а аренда GPU-сервера тянет на отдельную зарплату. Остается вариант, который все обходят стороной: дешевый CPU VPS с 4-8 ядрами и 16 гигабайтами оперативки.

Звучит как издевательство. Но именно здесь начинается настоящая инженерия. Не бросать железо на задачу, а заставить задачу подчиниться железу. Если вы думаете, что RAG - это только про большие модели и терабайты памяти, вы ошибаетесь. Это про эффективность. Про то, как выжать из каждого мегагерца и каждого мегабайта максимум.

Главный миф: для обработки 10k книг нужен GPU. Неправда. Эмбеддинги и поиск отлично работают на CPU. Проблема не в вычислениях, а в организации данных и памяти. 16 ГБ ОЗУ - это много, если не тратить их впустую.

Архитектура, которая не сломается под нагрузкой

Классический RAG развалится на таком железе. Загрузите все эмбеддинги в память - и 16 ГБ закончатся после первой тысячи книг. Используйте тяжелую модель для эмбеддингов - запрос будет обрабатываться 10 секунд. Забудьте про re-ranking - точность упадет на 30-40%.

Нужна система, где каждый компонент выбран за его эффективность, а не просто потому, что он популярен на GitHub.

Компонент Наш выбор (2026) Почему именно он Потребление RAM
Модель эмбеддингов BGE-M3 (light версия) Поддержка 8192 токена, мультиязычность, оптимизирована для CPU. all-MiniLM-L6-v2 уже устарела для таких объемов. ~500 МБ
Векторная БД Qdrant 1.9.x с mmap Единственная, которая умеет хранить вектора на диске с mmap, почти не занимая RAM. Альтернативы (FAISS, Chroma) сожрут всю память. ~50-100 МБ (зависит от запросов)
Реранкер BGE-Reranker-v3-Mini Специально облегченная версия 2025 года. Точность падает на 2-3% против большой, но скорость в 5 раз выше на CPU. ~300 МБ
LLM для ответов Llama 3.2 3B (4-битная квантизация) Может работать в 4 ГБ ОЗУ, выдает связные ответы. Для экспертного ассистента не нужна 70B модель, нужна точная информация из контекста. ~4 ГБ

Общий подсчет: 500 МБ + 100 МБ + 300 МБ + 4 ГБ = примерно 5 ГБ. Остается 11 ГБ на операционную систему, кэши и буферы. Это реально. Если вы возьмете VPS с 16 ГБ ОЗУ у провайдера вроде Timeweb Cloud, этого хватит с запасом.

1 Подготовка данных: как резать 10 000 книг без потери смысла

Самая скучная и самая важная часть. Если нарезать текст на равные куски по 256 токенов, вы потеряете контекст. Глава книги, разбитая посередине, сделает эмбеддинги бесполезными.

Правило: чанковать по структурным элементам. Главы, разделы, подразделы. Если структуры нет (сплошной текст), используйте алгоритмическое чанкование с перекрытием.

# Пример чанкинга с помощью актуальной библиотеки langchain-text-splitters 2026
from langchain_text_splitters import RecursiveCharacterTextSplitter

# Не делайте так (потеря контекста):
# splitter = RecursiveCharacterTextSplitter(chunk_size=256, chunk_overlap=50)

# Делайте так (сохраняем структуру):
splitter = RecursiveCharacterTextSplitter(
    separators=["\n\n\n", "\n\n", ". ", "? ", "! ", " ", ""],  # Приоритет разделителей
    chunk_size=1024,  # Большие чанки для книг
    chunk_overlap=200,  # Значительное перекрытие
    length_function=len,
    is_separator_regex=False,
)

chunks = splitter.split_text(book_text)
# Каждому чанку добавьте метаданные: название книги, автор, глава, страница
💡
Ошибка новичков: хранить только текст. Обязательно добавляйте метаданные (book_id, chapter, page) в каждый чанк. Потом это спасет при фильтрации запросов. "Найди информацию о квантовой физике только в учебниках 2020-2025 годов" - без метаданных это не сделать.

2 Индексирование: как не сжечь CPU и не ждать неделю

10 000 книг - это примерно 5-6 миллионов чанков. Создание эмбеддингов для каждого - задача на дни, если делать в один поток.

Решение: батчинг и асинхронность. Модель BGE-M3 на CPU обрабатывает примерно 100-200 чанков в секунду (зависит от длины). Вам нужно распараллелить процесс на все ядра VPS.

from sentence_transformers import SentenceTransformer
import numpy as np
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct
import asyncio
from concurrent.futures import ProcessPoolExecutor

model = SentenceTransformer('BAAI/bge-m3-light', device='cpu')

# Создаем батчи
batch_size = 128  # Не больше! Иначе упретесь в память.
chunks = [...]  # Ваш список чанков

points = []
for i in range(0, len(chunks), batch_size):
    batch = chunks[i:i + batch_size]
    # Кодируем батч
    embeddings = model.encode(batch, normalize_embeddings=True, show_progress_bar=False)
    
    for j, (chunk, embedding) in enumerate(zip(batch, embeddings)):
        points.append(
            PointStruct(
                id=i + j,
                vector=embedding.tolist(),
                payload={
                    "text": chunk.text,
                    "book_id": chunk.metadata["book_id"],
                    "title": chunk.metadata["title"],
                    "author": chunk.metadata["author"],
                    "chapter": chunk.metadata.get("chapter", ""),
                }
            )
        )
    # Каждые 10 000 точек сбрасываем в Qdrant, чтобы не держать в памяти
    if len(points) > 10000:
        client.upsert(collection_name="books", points=points)
        points = []
        print(f"Indexed {i + batch_size} chunks")

Это займет время. На 8-ядерном VPS процесс индексирования 5 миллионов чанков растянется на 10-15 часов. Смиритесь. Или используйте гибридный подход: построение индекса на GPU и обслуживание на CPU, если есть временный доступ к мощной машине.

3 Настройка Qdrant: магия mmap и как не дать БД съесть всю память

Qdrant с настройками по умолчанию загрузит все вектора в оперативку. Для 5 миллионов векторов по 768 измерений это примерно 15 ГБ. Прощай, 16 ГБ RAM.

Секрет в режиме mmap. Он позволяет Qdrant читать вектора прямо с диска, почти не используя RAM. Скорость падает, но для RAG, где запросов в секунду не тысячи, это приемлемо.

# config.yaml для Qdrant 1.9.x
storage:
  # Включаем mmap для векторов
  vectors:
    mmap: true
  # Кэш эмбеддингов в памяти для часто запрашиваемых данных (опционально)
  cache:
    size: 1000  # Количество векторов в кэше

service:
  # Лимит памяти для сервиса
  memory_limit: 2048  # Не больше 2 ГБ для самого Qdrant

performance:
  # Увеличиваем количество потоков для поиска
  max_search_threads: 4

Запускаем Qdrant:

docker run -d \
  -p 6333:6333 \
  -v ./qdrant_storage:/qdrant/storage \
  -v ./config.yaml:/qdrant/config/production.yaml \
  qdrant/qdrant:latest

Важно: mmap работает хорошо только с быстрым диском (SSD NVMe). Если ваш VPS использует HDD, поиск будет мучительно медленным. При выборе VPS обращайте внимание на тип диска. На Timeweb Cloud можно выбрать конфигурации с NVMe.

4 Сборка пайплайна запроса: реранкинг не для бедных

Стандартный пайплайн: запрос -> эмбеддинг -> поиск по векторам -> топ-10 результатов -> реранкинг -> топ-3 -> подача в LLM. На CPU каждый этап добавляет задержку. Наша цель - уложиться в 3-5 секунд на ответ.

Хитрость в том, чтобы не отправлять в реранкер все 10 результатов. Сначала делаем гибридный поиск: векторный + лексический (BM25). Это повышает recall. Затем реранкеру отдаем только топ-7. Подробнее про гибридный поиск я писал в отдельном гайде.

from qdrant_client import QdrantClient
from qdrant_client.models import Filter, FieldCondition, MatchValue, SearchRequest
from transformers import AutoModelForSequenceClassification, AutoTokenizer
import torch
import numpy as np

client = QdrantClient(host="localhost", port=6333)

# Реранкер
reranker_tokenizer = AutoTokenizer.from_pretrained("BAAI/bge-reranker-v3-mini")
reranker_model = AutoModelForSequenceClassification.from_pretrained("BAAI/bge-reranker-v3-mini")
reranker_model.eval()

def hybrid_search(query, limit=10):
    # 1. Векторный поиск
    query_embedding = embedder.encode(query, normalize_embeddings=True)
    vector_results = client.search(
        collection_name="books",
        query_vector=query_embedding,
        limit=limit * 2,  # Берем больше для гибридности
        with_payload=True
    )
    
    # 2. Лексический поиск (через Qdrant, если настроен, или через отдельный индекс BM25)
    # Для простоты опустим, но это критически важно для точности
    
    # 3. Объединение и ранжирование (простейший способ)
    combined = vector_results  # Здесь должна быть реальная логика слияния
    return combined[:limit]  # Возвращаем топ-10 для реранкинга

def rerank(query, passages):
    # passages - список текстов чанков
    pairs = [[query, passage] for passage in passages]
    with torch.no_grad():
        inputs = reranker_tokenizer(pairs, padding=True, truncation=True, return_tensors='pt', max_length=512)
        scores = reranker_model(**inputs, return_dict=True).logits.view(-1,).float()
    
    # Сортируем по убыванию score
    ranked_indices = np.argsort(scores.numpy())[::-1]
    return [passages[i] for i in ranked_indices[:3]]  # Топ-3 для LLM

Реранкер - узкое место. Модель v3-Mini быстрая, но все равно добавляет 0.5-1 секунду. Если это слишком много, можно увеличить количество исходных результатов поиска до 20, а в реранкер отдавать только топ-5 по векторному сходству. Теряете в точности, выигрываете в скорости.

Ошибки, которые убьют вашу систему

  • Игнорирование метаданных. Без фильтрации по книге, автору, году, каждый запрос будет искать по всем 10k книг. Это медленно и неточно. Всегда добавляйте фильтры в поиск, если пользователь уточняет.
  • Чанкование по таймеру или фиксированному размеру. Вы получите обрывы предложений и потерянные смыслы. Инвестируйте время в парсинг структуры книг (оглавление, разделы).
  • Запуск всего в одном процессе. Если ваше приложение - один скрипт, который делает эмбеддинг, поиск, реранкинг и генерацию, он будет падать под нагрузкой. Разделите на микросервисы или хотя бы отдельные процессы с очередями.
  • Отказ от кэширования. Повторяющиеся запросы должны кэшироваться. Кэшируйте эмбеддинги запросов и топовые результаты. Redis на том же VPS съест память, но файловый кэш на SSD может спасти.

Что в итоге? Система, которая работает

Вы получите ассистента, который отвечает на вопросы по 10 000 книг за 3-5 секунд, потребляя 10-12 ГБ ОЗУ на пике. Это не магия, это инженерия. Каждый компонент выбран потому, что он решает конкретную проблему в условиях ограничений.

Самое сложное - не написать код, а принять решения. Использовать ли гибридный поиск? Какой размер чанка оптимален для книг? Стоит ли вообще использовать реранкинг на CPU? Ответы зависят от ваших данных и запросов. Начните с простого пайплайна (эмбеддинг -> поиск -> LLM), измеряйте точность, а затем добавляйте сложность.

И помните: RAG на CPU - это не удел бедных. Это демонстрация того, что эффективность важнее мощности. Когда все вокруг бросают на задачи GPU за $10 000, ваша система на $20 VPS будет делать то же самое, просто немного медленнее. А иногда - и точнее, потому что вы были вынуждены думать над каждой деталью.

💡
Прогноз на 2026-2027: модели эмбеддингов станут еще легче и точнее, а векторные БД научатся эффективнее работать с диском. Возможно, необходимость в реранкинге отпадет, потому что поиск будет сразу выдавать идеально релевантные чанки. Но архитектурные принципы останутся: разделение ответственности, эффективное использование памяти и понимание своих данных.