中文翻译暂不可用,显示俄语原文。
Multi-tenant RAG с изоляцией данных
ТЕХНИЧЕСКОЕ ЗАДАНИЕ: Multi-tenant RAG с изоляцией данных
1. Цель задачи
Спроектировать и реализовать мультиарендную архитектуру для RAG-системы, в которой данные каждого тенанта (арендатора) полностью изолированы. Применяется подход collection per tenant (отдельная коллекция в векторной БД для каждого клиента) и/или фильтрация по полю tenant_id в общей коллекции. Система должна гарантировать, что запрос от одного тенанта не вернёт документы другого даже при ошибках в коде приложения.
Ключевой результат Рабочий прототип Multi-tenant RAG API, прошедший тесты на изоляцию (тенант A не видит данные тенанта B).
2. Исходные данные
| Что нужно | Откуда взять |
|---|---|
| Документы для минимум 2 тенантов (5–10 документов на тенант) | Создать самостоятельно (например, текстовые файлы с разными темами: «Финансы_тенантА», «Медицина_тенантБ») |
| Векторная БД (Qdrant / Weaviate / Chroma) | Docker-образ (официальный) или облачная инстанция |
| Python 3.10+ окружение | Локальная установка / venv |
| Каркас RAG-приложения | FastAPI + LangChain (или LlamaIndex) |
| Эмбеддинг-модель | Sentence-transformers (all-MiniLM-L6-v2) или OpenAI API |
Если нет реального инструмента — симулируем:
- Если нет возможности поднять Qdrant в Docker — используем Chroma in-memory (но с потерей некоторых функций фильтрации).
- Если нет реальных документов — генерируем 10 синтетических текстов с помощью faker + случайные тематики (финансы, спорт, IT), помечаем каждый tenant_id в метаданных.
- Если нет OpenAI API — используем локальную модель через Ollama или отключаем генерацию, оставляем только retrieval.
3. Технологический стек
| Компонент | Инструменты | Назначение |
|---|---|---|
| Backend | Python 3.10+ / FastAPI | Веб-сервер для обработки запросов |
| Векторная БД | Qdrant (Docker) | Хранение и поиск эмбеддингов с фильтрацией |
| Embedding | sentence-transformers/all-MiniLM-L6-v2 | Генерация векторных представлений |
| RAG-фреймворк | LangChain (или LlamaIndex) | Пайплайн retrieval + generation |
| LLM | OpenAI GPT-4 (опционально) или Ollama | Генерация ответа на основе извлечённых документов |
| Тестирование | Pytest + httpx | Unit и интеграционные тесты изоляции |
| Контейнеризация | Docker / Docker Compose | Локальный запуск Qdrant + приложения |
4. Этапы выполнения
Этап 1: Проектирование архитектуры и подготовка окружения (30 минут)
Действия
-
Выбрать модель изоляции — collection per tenant (рекомендуется для production). Каждый тенант получает отдельную коллекцию с именем
tenant_{id}. Если тенантов > 1000 — рассмотреть shared collection с фильтром. Для данного pet-проекта используем collection per tenant. -
Создать структуру директорий проекта:
multi-tenant-rag/ ├── app/ │ ├── __init__.py │ ├── main.py │ ├── api.py │ ├── vector_store.py │ ├── auth.py (заглушка) │ └── models.py ├── data/ │ ├── tenant_a/ │ └── tenant_b/ ├── tests/ │ ├── test_isolation.py │ └── test_retrieval.py ├── docker-compose.yml └── requirements.txt -
Подготовить Docker Compose для Qdrant и, опционально, для приложения:
version: '3.8' services: qdrant: image: qdrant/qdrant:v1.12.1 ports: - "6333:6333" volumes: - ./qdrant_storage:/qdrant/storage
Ожидаемый результат этапа Поднятый Qdrant, проект с пустыми модулями, requirements.txt.
Этап 2: Реализация per-tenant коллекций и загрузка данных (1.5 часа)
Действия
-
Написать модуль
vector_store.py:- Функция
get_client()— инициализация QdrantClient (подключение к localhost:6333). - Функция create_tenant_collection(tenant_id: str) — создаёт коллекцию с именем tenant_{tenant_id} с размерностью эмбеддинга (384) и конфигурацией HNSW.
- Функция upsert_documents(tenant_id: str, documents: List[Document]) — разбивает документы на чанки (Chunk size=512, overlap=50), генерирует эмбеддинги через sentence-transformers, загружает в соответствующую коллекцию с payload, содержащим tenant_id и doc_id.
- Функция
-
Реализовать загрузку данных:
# load_data.py (скрипт) from app.vector_store import create_tenant_collection, upsert_documents from langchain.text_splitter import RecursiveCharacterTextSplitter splitter = RecursiveCharacterTextSplitter(chunk_size=512, chunk_overlap=50) for tenant in ["tenant_a", "tenant_b"]: create_tenant_collection(tenant) # читать файлы из data/{tenant}/ files = glob.glob(f"data/{tenant}/*.txt") for filepath in files: with open(filepath) as f: text = f.read() chunks = splitter.split_text(text) # создать Document objects docs = [Document(page_content=chunk, metadata={"tenant_id": tenant}) for chunk in chunks] upsert_documents(tenant, docs) -
Добавить тестовые данные:
data/tenant_a/finance.txt: «Отчёт по доходам за Q1 2025 компании Альфа. Прибыль составила 1.2 млн долларов.»data/tenant_b/medical.txt: «История болезни пациента Петрова И.С. Диагноз: гипертония 2 степени.»
-
Запустить скрипт загрузки и убедиться, что обе коллекции созданы и содержат векторы.
Ожидаемый результат этапа Скрипт загрузки данных успешно заполнил две коллекции; проверка через Qdrant UI (/dashboard) показывает наличие точек.
Этап 3: Реализация API с изоляцией по tenant_id (1 час)
Действия
-
Создать модель запроса в
models.py:from pydantic import BaseModel, Field class QueryRequest(BaseModel): tenant_id: str = Field(..., description="Идентификатор тенанта") question: str = Field(..., min_length=1, max_length=1000) -
Реализовать endpoint
/retrieveв api.py:- Принимает
QueryRequest. - Извлекает tenant_id из запроса.
- Использует коллекцию tenant_{tenant_id} для поиска (через QdrantClient.search).
- Возвращает top_k документов (например, 3) с их содержимым и метаданными.
- Важно: никогда не разрешать выбор коллекции из пользовательского ввода без валидации — проверять, что tenant_id существует в предопределённом списке.
- Принимает
-
Включить аутентификацию (упрощённую) — проверять заголовок X-Tenant-ID, который должен совпадать с tenant_id в теле запроса. Это моделирует реальную проверку токена.
-
Написать основное приложение
main.py:from fastapi import FastAPI, Depends, HTTPException, Header from app.api import router app = FastAPI() app.include_router(router) -
Запустить и протестировать вручную:
curl -X POST http://localhost:8000/retrieve \ -H "X-Tenant-ID: tenant_a" \ -H "Content-Type: application/json" \ -d '{"tenant_id": "tenant_a", "question": "Какая прибыль компании Альфа?"}'Убедиться, что возвращаются только документы из коллекции
tenant_a.
Ожидаемый результат этапа Рабочий endpoint /retrieve, корректно фильтрующий по tenant.
Этап 4: Тестирование изоляции и безопасности (1 час)
Действия
-
Написать интеграционные тесты в
tests/test_isolation.py:import pytest from httpx import AsyncClient from app.main import app @pytest.mark.asyncio async def test_tenant_a_cannot_see_tenant_b_data(): async with AsyncClient(app=app, base_url="http://test") as client: # tenant_a просит данные по медицинской теме, которая есть только у tenant_b response = await client.post("/retrieve", json={"tenant_id": "tenant_a", "question": "История болезни пациента Петрова"}, headers={"X-Tenant-ID": "tenant_a"}) data = response.json() # Ни один документ не должен содержать слово "гипертония" assert not any("гипертония" in doc["content"] for doc in data["documents"]) -
Написать тест на инъекцию tenant_id:
- Попытаться передать "tenant_id": "tenant_b'" OR '1'='1" — должно отрабатывать безопасно (только буквы, цифры, подчёркивание).
- Проверить, что доступ к несуществующей коллекции возвращает 404.
-
Проверить безопасность на уровне API:
- Запрос без заголовка
X-Tenant-ID— 403. - Запрос с несовпадающим
tenant_idи заголовком — 403.
- Запрос без заголовка
-
Запустить тесты и зафиксировать результаты.
Ожидаемый результат этапа Все тесты проходят, изоляция подтверждена.
5. Критерии приемки (Definition of Done)
- Код размещён в git-репозитории с Readme и инструкцией по запуску.
- Приложение запускается одной командой (
docker-compose up). - Существуют две коллекции (или одна с фильтрами) с разными данными для двух тенантов.
- Endpoint
/retrieveвозвращает только документы запрашиваемого тенанта. - Написаны и проходят минимум 3 теста на изоляцию (разные тенанты, инъекция, несовпадение заголовка).
- При попытке запроса с несуществующим
tenant_idвозвращается HTTP 404. - Документация API сгенерирована (FastAPI auto-docs).
- Нет хардкода секретов (API-ключи вынесены в
.env).
6. Ожидаемый результат
Основной артефакт Репозиторий с проектом, содержащий:
app/— исходный код FastAPI-приложения.data/— примеры документов для двух тенантов.tests/— автоматические тесты изоляции.docker-compose.yml— конфигурация для запуска Qdrant.requirements.txt— зависимости.README.md— инструкция по запуску и демонстрация.
Дополнительные артефакты (опционально):
- Скрипт для генерации синтетических документов.
- Grafana-дашборд для мониторинга запросов (число запросов на тенант, задержки).
- Пример Postman-коллекции для ручного тестирования.
7. Возможные сложности и их решение
| Сложность | Решение |
|---|---|
| Случайное смешивание tenant_id при batch-загрузке | В payload каждого вектора всегда записывать tenant_id; перед upsert проверять соответствие названия коллекции и tenant_id. |
| Забыли создать коллекцию для нового тенанта | Реализовать lazy-создание: при первом запросе на retrieve, если коллекция не существует, возвращать 404 (а не создавать автоматически — это может быть использовано для DoS). |
| Некорректные фильтры в Qdrant (например, опечатка в имени поля) | В тестах явно проверять, что фильтр применяется: запрос с точным must фильтром и сравнение результата с запросом без фильтра. |
Передача tenant_id через URL (например, /tenant_a/retrieve) — риск path traversal | Использовать только заголовок и тело запроса, валидировать tenant_id regex: ^[a-zA-Z0-9_]+$. |
| Асинхронные race conditions при одновременной загрузке данных разными тенантами | Использовать синхронные блокировки на уровне приложения или очередь задач (celery). Для пет-проекта достаточно одного скрипта загрузки. |
8. Бюджет времени (оценка)
| Этап | Время |
|---|---|
| Этап 1: Архитектура и окружение | 30 мин |
| Этап 2: Коллекции и загрузка данных | 1 час 30 мин |
| Этап 3: API с изоляцией | 1 час |
| Этап 4: Тестирование | 1 час |
| Итого | 4 часа |
Примечание Для первого раза время может увеличиться до 6–8 часов из-за отладки.
9. Связанные вопросы из базы знаний
| Вопрос | Тема | Номер |
|---|---|---|
| Как реализовать multi-tenant RAG? | Архитектура RAG | 45 |
| Векторные БД: Qdrant vs Chroma vs Weaviate | Выбор инструмента | 112 |
| Фильтрация в Qdrant (filtered search) | Query API | 201 |
| Безопасность tenant-изоляции в приложениях ИИ | Security | 378 |
| LangChain: работа с несколькими векторными сторами | LangChain | 89 |
| Тестирование RAG-систем | QA | 567 |
| Инъекция в tenant_id (OWASP) | Security | 450 |
| Разбивка документов на чанки (chunking strategies) | Preprocessing | 33 |
| Мониторинг RAG: метрики для multi-tenancy | Observability | 721 |
| Использование эмбеддингов с метаданными | Embedding | 155 |
10. Чек-лист самопроверки
- Я спроектировал архитектуру с учётом изоляции данных (collection per tenant или shared + filter) и задокументировал выбор.
- Все эндпоинты API проверяют tenant_id из заголовка и не доверяют только телу запроса.
- Я написал тесты, которые явно проверяют, что тенант A не получает данные тенанта B, даже если вопрос явно относится к теме B.
- Я протестировал граничные случаи: несуществующий tenant_id, специальные символы в tenant_id, отсутствие заголовка.
- В README описан процесс запуска и ссылка на тесты; код можно запустить и проверить изоляцию.