RAG с векторной БД на CPU (Chroma/Qdrant)
ТЕХНИЧЕСКОЕ ЗАДАНИЕ: RAG с векторной БД на CPU (Chroma/Qdrant)
1. Цель задачи
Разработать локальную Retrieval-Augmented Generation (RAG) систему на ограниченных ресурсах (8GB RAM, CPU). Необходимо загрузить 100 000 текстовых документов (новости, wiki-статьи или синтезированные данные), построить векторную базу данных (Chroma или Qdrant в in-memory режиме) и реализовать пайплайн поиска + генерации ответа на русском языке. Система должна отвечать на вопросы по корпусу за время не более 500 мс (latency поиск + генерация без учёта первого прогрева). Ключевой результат работающее RAG-приложение (CLI или FastAPI), которое обрабатывает запрос пользователя и возвращает ответ с цитатами из документов при latency <500 мс на CPU.
2. Исходные данные
| Что нужно | Откуда взять |
|---|---|
| Корпус текстов (100k документов) | Скачать датасет IlyaGusev/ru_all_mixed_data (HuggingFace) или сгенерировать синтетически 100k коротких текстов (по 3-5 предложений) с помощью random + шаблоны |
| Модель для эмбеддингов (CPU) | sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2 (~500MB) или intfloat/multilingual-e5-small |
| LLM для генерации (CPU) | Qwen2.5-1.5B-Instruct (GGUF квантование q4_k_m) или TheBloke/Llama-2-7B-Chat-GGUF (если влезет в 4GB) |
| Библиотеки | Chroma (pip install chromadb), Qdrant (pip install qdrant-client[fastembed]), langchain, llama-cpp-python |
Если нет реального инструмента — симулируем:
- Если нет HuggingFace — скачать датасет вручную через
wgetс зеркала или сгенерировать синтетику с помощью numpy и python:import random, json subjects = ["котики", "финансы", "IT", "спорт", "наука"] docs = [] for i in range(100000): s = random.choice(subjects) text = f"Документ {i}: {s}. " + " ".join(random.choices(["факт", "новость", "исследование"], k=5)) docs.append({"id": i, "text": text}) with open("corpus.json", "w") as f: json.dump(docs, f) - Если нет GPU — все шаги выполняются на CPU (только CPU-модели).
3. Технологический стек
| Компонент | Инструменты | Назначение |
|---|---|---|
| Векторная БД | Chroma (persistent) или Qdrant (in-memory) | Хранение и поиск эмбеддингов документов |
| Эмбеддинги | sentence-transformers (multilingual-e5-small) | Преобразование текста в векторы |
| LLM инференс | llama-cpp-python (gguf) или HuggingFace pipeline (device="cpu") | Генерация ответа на основе найденных документов |
| Оркестрация | LangChain / LlamaIndex или чистый код | Сборка пайплайна RAG |
| API | FastAPI (опционально) | Web-интерфейс для запросов |
| Хранение данных | JSON / Parquet | Исходный корпус и метаданные |
4. Этапы выполнения
Этап 1: Подготовка корпуса и окружения (2 часа)
Действия
- Создать виртуальное окружение Python 3.10+ и установить зависимости:
pip install chromadb sentence-transformers langchain langchain-community llama-cpp-python fastapi uvicorn - Загрузить или сгенерировать 100k документов (пункт 2). Сохранить в corpus.json.
- Загрузить модель эмбеддингов:
from sentence_transformers import SentenceTransformer model = SentenceTransformer('intfloat/multilingual-e5-small', device='cpu') - Скачать LLM в GGUF (например, Qwen2.5-1.5B-Instruct-q4_k_m.gguf) в папку
models/.
Ожидаемый результат этапа Рабочее окружение, файл corpus.json, модель эмбеддингов и LLM готовы к использованию.
Этап 2: Индексация документов в векторную БД (3 часа)
Действия
- Разбить корпус на батчи по 500 документов для избежания переполнения RAM.
- Для каждого документа вычислить эмбеддинг с помощью model.encode(doc["text"]).
- Загрузить векторы в Chroma:
import chromadb client = chromadb.PersistentClient(path="./chroma_db") collection = client.create_collection(name="docs", metadata={"hnsw:space": "cosine"}) for batch in batches: ids = [str(d["id"]) for d in batch] embeddings = [d["embedding"] for d in batch] # предварительно закэшировать metadatas = [{"source": d["text"][:50]} for d in batch] collection.add(ids=ids, embeddings=embeddings, metadatas=metadatas) - Для Qdrant (in-memory): запустить qdrant_client.QdrantClient(":memory:") и загрузить через upsert.
- Выполнить тестовый поиск: collection.query(query_embeddings=[...], n_results=5) — проверить скорость (ожидаем <50ms на CPU).
Ожидаемый результат этапа Хранящаяся на диске векторная БД (Chroma) с 100k записей, эмбеддинги кэшированы.
Этап 3: Построение RAG пайплайна (2 часа)
Действия
- Реализовать функцию поиска:
def retrieve(query: str, k=5): emb = model.encode(query).tolist() return collection.query(query_embeddings=[emb], n_results=k) - Загрузить LLM через llama-cpp-python:
from llama_cpp import Llama llm = Llama(model_path="models/qwen-1.5b.gguf", n_ctx=2048, n_threads=4) - Сформировать промпт с контекстом:
def generate(query, docs): context = "\n\n".join([doc["text"] for doc in docs]) prompt = f"На основе документов:\n{context}\n\nВопрос: {query}\nОтвет:" return llm(prompt, max_tokens=256, temperature=0.2)["choices"][0]["text"] - Объединить в одну функцию rag_pipeline(query).
- Написать CLI-скрипт
rag_cli.pyс циклом ввода.
Ожидаемый результат этапа Рабочий RAG пайплайн, принимающий вопрос и выводящий ответ.
Этап 4: Оптимизация производительности и latency (2 часа)
Действия
- Измерить latency каждого этапа: эмбеддинг запроса, поиск, генерация.
- Оптимизировать:
- Если latency >500 мс — переключиться на более лёгкую LLM (TinyLlama-1.1B GGUF) или уменьшить контекст (512 токенов).
- Запустить нагрузочный тест: 20 последовательных запросов, среднее время.
Ожидаемый результат этапа Замеры latency (цель <500 мс), применённые оптимизации.
Этап 5: Создание API (опционально, 1 час)
Действия
- Реализовать FastAPI endpoint
/ask:@app.post("/ask") async def ask(query: str): result = rag_pipeline(query) return {"answer": result} - Запустить сервер:
uvicorn main:app --host 0.0.0.0 --port 8000. - Протестировать через
curl -X POST -d "query=Какой-то вопрос" http://localhost:8000/ask.
Ожидаемый результат этапа FastAPI endpoint, готовый к демонстрации.
5. Критерии приемки (Definition of Done)
- Корпус из 100 000 документов загружен и проиндексирован в векторную БД (Chroma или Qdrant).
- Среднее время ответа (latency) на CPU не превышает 500 мс на 10 различных запросах (измерено после прогрева).
- Система корректно возвращает ответ на русском языке, содержащий информацию из корпуса (оценка экспертом 2 примеров).
- Код пайплайна без GPU-зависимостей, использование только CPU.
- Наличие скрипта
rag_cli.pyили FastAPI сервера с рабочим примером. - Присутствует файл с метриками производительности (latency_table.csv).
- Модель эмбеддингов и LLM загружены локально без обращений к внешним API.
- Возможность добавить новые документы без переиндексации всех данных (Chroma поддерживает incremental).
- Использование не более 8GB RAM в процессе работы (мониторинг через
psutil). - Документация (README.md) с инструкцией по запуску.
6. Ожидаемый результат
- Основной артефакт папка проекта с файлами:
rag_cli.py– CLI-интерфейс.rag_api.py– FastAPI сервер (опционально).indexer.py– скрипт индексации.corpus.json– исходный корпус.chroma_db/– persistent хранилище.latency_report.csv– таблица с замерами.README.md– описание, установка, примеры.
- Содержание результата программа, которая принимает вопрос на русском, выводит ответ и цитаты из документов.
- Дополнительно результаты нагрузочного теста, код оптимизации.
7. Возможные сложности и их решение
| Сложность | Решение |
|---|---|
| Нехватка RAM при индексации 100k документов | Использовать батчи по 500 документов, освобождать память через del и gc.collect() |
| LLM-генерация слишком медленная (2-3 секунды) | Использовать квантование 4-bit GGUF; уменьшить контекст; выбрать TinyLlama; установить n_predict=128 |
| Поиск в Chroma занимает >100 мс | Включить HNSW с параметрами ef_construction=200, ef_search=50 |
| Модель эмбеддингов не влазит в 8GB | Использовать multilingual-e5-small (80MB), не загружать все модели одновременно |
| Запросы к LLM содержат слишком длинный контекст | Ограничить количество документов до 3 и обрезать текст до 500 токенов |
8. Бюджет времени (оценка)
| Этап | Время |
|---|---|
| Этап 1: Подготовка корпуса и окружения | 2 ч |
| Этап 2: Индексация документов в векторную БД | 3 ч |
| Этап 3: Построение RAG пайплайна | 2 ч |
| Этап 4: Оптимизация производительности | 2 ч |
| Этап 5: Создание API (опционально) | 1 ч |
| Итого | 10 ч |
Примечание При первом выполнении время может увеличиться в 1.5 раза из-за необходимости настройки окружения и отладки.
9. Связанные вопросы из базы знаний
| Вопрос | Тема |
|---|---|
| 12 | Векторные БД: сравнение FAISS, Chroma, Qdrant |
| 45 | Использование sentence-transformers для эмбеддингов |
| 78 | Квантование LLM с GGUF и llama.cpp |
| 112 | Оценка качества RAG: метрики |
| 189 | Оптимизация HNSW параметров |
| 234 | Ограничения CPU для инференса моделей |
| 301 | Инкрементальное обновление индекса |
| 415 | Обработка длинных документов: чанкинг |
| 567 | Пайплайны LangChain для RAG |
| 689 | Запуск FastAPI сервера в production |
10. Чек-лист самопроверки
- Я проверил, что после запуска
rag_cli.pyи ввода тестового вопроса получаю осмысленный ответ за <500 мс. - Я убедился, что использовал только CPU-модели и никакие данные не отправляются в облако.
- Я проверил, что при повторном запуске сервера или CLI индекс не перестраивается заново (persistent mode).
- Я измерил RAM потребление при индексации (не превышает 7.5GB) и при генерации (не более 6GB).
- Я подготовил README с примерами команд и требованиями к системе (Python 3.10, 8GB RAM, без GPU).