Как бы вы спроектировали multi-tenant RAG (разные компании, изолированные данные)?
Краткий тезис
Multi-tenant RAG — это система, в которой одна инфраструктура обслуживает несколько клиентов (tenant'ов), полностью изолируя их данные. Основные подходы: отдельные инстансы (максимальная изоляция, но высокая стоимость), collection per tenant (золотая середина: одна векторная БД с отдельными коллекциями на каждого tenant'а) и metadata filtering (единая коллекция с фильтрацией по tenant_id — дешево, но рискованно). Рекомендую collection per tenant + рейт-лимиты: это даёт хорошую изоляцию, управляемость и масштабирование.
1. Термин: Multi-tenant RAG
Multi-tenant (мультитенантность) — архитектура, при которой один экземпляр приложения обслуживает множество клиентов (tenant'ов), при этом данные каждого tenant'а логически изолированы. В контексте RAG — это значит, что поиск и генерация выполняются только по документам, принадлежащим конкретной компании.
Основные проблемы:
- Изоляция данных — tenant не должен видеть чужие документы.
- Производительность — запросы одного tenant не должны замедлять другие.
- Управление ресурсами — необходимо справедливо распределять вычислительную мощность и память.
2. Ключевые требования при проектировании
| Требование | Описание | Влияние на архитектуру |
|---|---|---|
| Изоляция данных | Документы компании-клиента недоступны другим | Определяет выбор подхода (коллекции, фильтрация) |
| Производительность | Запросы одного tenant не блокируют других | Требует рейт-лимитов и выделения ресурсов |
| Стоимость | Инфраструктура должна быть экономически оправдана | Отдельные инстансы дороги, общие коллекции — дёшево |
| Масштабируемость | Добавление новых tenant'ов без простоев | Collection per tenant легко масштабируется |
| Безопасность | Защита от случайного доступа (например, ошибок в фильтрах) | Metadata filtering менее безопасна, чем физическая изоляция |
3. Подход 1: Отдельные инстансы (высокая изоляция, высокая стоимость)
Каждый tenant получает полностью самостоятельную инфраструктуру: свою векторную БД (например, Qdrant, Pinecone), свой LLM endpoint (если нужно), свой API-шлюз.
Плюсы
- Максимальная изоляция — ни один tenant не может навредить другому.
- Простая отладка и настройка под конкретного клиента.
- Возможность индивидуальных fine-tuning моделей.
Минусы
- Высокие затраты на инфраструктуру и администрирование.
- Сложное развёртывание новых tenant'ов.
- Неэффективное использование ресурсов — каждый инстанс простаивает большую часть времени.
Когда использовать Очень крупные клиенты с жёсткими требованиями к безопасности и производительности, готовые платить за полную изоляцию.
4. Подход 2: Collection per tenant (золотая середина)
Одна общая инстанция векторной БД, но каждому tenant'у выделяется отдельная коллекция (collection). В Qdrant, Weaviate, Milvus это нативная возможность.
# Пример настройки в Qdrant (псевдокод)
from qdrant_client import QdrantClient
client = QdrantClient("localhost", port=6333)
# Создаём коллекцию для tenant_42
client.recreate_collection(
collection_name="tenant_42",
vectors_config=VectorParams(size=768, distance=Distance.COSINE),
)
# Индексация документов tenant_42
client.upsert(
collection_name="tenant_42",
points=[
PointStruct(id=1, vector=[0.1, 0.2, ...], payload={"text": "..."}),
]
)
# Поиск только по своей коллекции
results = client.search(
collection_name="tenant_42",
query_vector=[0.3, 0.1, ...],
limit=5
)
Плюсы
- Хорошая изоляция на уровне БД — коллекции не пересекаются.
- Умеренные затраты — один сервер, много коллекций.
- Простое добавление tenant'ов — создать новую коллекцию.
- Можно настраивать ресурсные лимиты на коллекцию (число векторов, скорость поиска).
Минусы
- Один tenant может перегрузить общий сервер (если не внедрить рейт-лимиты).
- Требуется управление «горячими» и «холодными» tenant'ами (например, архивные коллекции отключать).
5. Подход 3: Metadata filtering (единая коллекция с фильтром)
Все документы всех tenant'ов хранятся в одной коллекции. Каждый документ имеет поле tenant_id (например, в payload). При поиске добавляется обязательный фильтр по этому полю.
# Пример в Qdrant с фильтром
results = client.search(
collection_name="all_tenants",
query_vector=[0.3, 0.1, ...],
query_filter=Filter(
must=[FieldCondition(key="tenant_id", match=MatchValue(value="company_42"))]
),
limit=5
)
Плюсы
- Минимальные издержки — один сервер, одна коллекция.
- Простая админка — нет раздельных коллекций.
- Хорошо для малого числа tenant'ов с малым объёмом данных.
Минусы
- Риск утечки данных — ошибка в фильтре (например, пропуск условия) покажет чужие документы.
- Производительность — фильтры по тегам могут замедлить поиск, особенно при большом количестве tenant'ов и документов.
- Сложность управления — нельзя разделить ресурсы или приоритеты между tenant'ами.
- Отсутствие изоляции — один tenant может случайно повлиять на индексацию всех данных.
6. Сравнительная таблица подходов
| Характеристика | Отдельные инстансы | Collection per tenant | Metadata filtering |
|---|---|---|---|
| Изоляция данных | Полная | Высокая (логическая) | Низкая (зависит от кода) |
| Стоимость | Высокая | Средняя | Низкая |
| Сложность администрирования | Высокая | Средняя | Низкая |
| Производительность per tenant | Выделенная | Зависит от общей нагрузки | Общая, могут быть конфликты |
| Масштабирование (добавление tenant'ов) | Сложное (новый сервер) | Простое (новая коллекция) | Очень простое (просто добавить документы) |
| Безопасность (ошибки в коде) | Невозможно скомпрометировать | Маловероятно (разные коллекции) | Высокий риск утечки |
| Возможность индивидуальных настроек | Да | Частично (настройки коллекции) | Нет |
7. Рекомендация: collection per tenant + resource limits
Оптимальное решение для большинства B2B-сценариев — collection per tenant в общей векторной БД с добавлением рейт-лимитов (rate limits) на каждого tenant'а.
Реализация рейт-лимитов
- С помощью API gateway (например, NGINX + Lua, Kong) или библиотеки token bucket на стороне бэкенда.
- Ограничить количество запросов в секунду и количество документов, которые tenant может индексировать.
# Пример простого rate limiter на FastAPI
from fastapi import FastAPI, HTTPException
import time
from collections import defaultdict
app = FastAPI()
limits = defaultdict(lambda: {"tokens": 10, "last_time": time.time()}) # 10 req/s
def check_rate_limit(tenant_id: str):
now = time.time()
info = limits[tenant_id]
elapsed = now - info["last_time"]
info["tokens"] = min(info["tokens"] + elapsed * 10, 10) # восстановление
info["last_time"] = now
if info["tokens"] < 1:
raise HTTPException(status_code=429, detail="Too Many Requests")
info["tokens"] -= 1
@app.post("/search/{tenant_id}")
async def search(tenant_id: str, query: dict):
check_rate_limit(tenant_id)
# поиск по коллекции tenant_id
Дополнительные рекомендации
- Использовать слой кэширования (например, Redis) для часто повторяющихся запросов — снижает нагрузку на векторную БД.
- Для очень крупных tenant'ов (с большим объёмом данных) можно выделить отдельный сервер или коллекцию с повышенными ресурсами.
- Мониторить использование коллекций — при превышении порогов автоматически создавать новые шарды или увеличивать мощность.
8. Embedding модели: одна для всех или fine-tune?
- Одна общая модель эмбеддингов для всех tenant'ов. Это проще, дешевле, и часто достаточно, если домены близки.
- Fine-tune для отдельных tenant'ов — если у компании свой специфичный домен (например, юридические документы). Тогда для этого tenant'а можно развернуть отдельный embedding endpoint (например, через Hugging Face TGI или Ray Serve), но хранить векторы в той же векторной БД, только в другой коллекции.
- Абляционные тесты — проверять, улучшает ли fine-tune качество retrieval'а для данного tenant'а. Если нет, оставлять общую модель.
# Пример выбора модели в рантайме
MODEL_MAP = {
"default": "intfloat/multilingual-e5-small",
"tenant_42": "finetuned-e5-legal",
}
def get_embedding(text: str, tenant_id: str) -> List[float]:
model_name = MODEL_MAP.get(tenant_id, MODEL_MAP["default"])
# загрузка модели или вызов кэшированного эндоинта
return embed_with_model(text, model_name)
9. Рейт-лимиты и квоты per tenant
Рейт-лимиты — ключевой механизм защиты от «шумного соседа». Должны контролировать:
- Количество поисковых запросов в секунду/минуту/день.
- Количество индексируемых документов в день (чтобы один tenant не забил всё место).
- Максимальный размер документа / количество векторов на коллекцию.
Реализация
- Хранить лимиты в конфиге (YAML/JSON) или в БД (например, Redis с ключом
tenant:{id}:limits). - На уровне API gateway выставлять заголовки
X-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Reset(по аналогии с GitHub API).
Пример конфигурации (YAML):
tenants:
default:
search_rate: 10 # запросов в секунду
index_rate: 100 # документов в час
max_vectors: 1_000_000
premium:
search_rate: 100
index_rate: 1000
max_vectors: 10_000_000
10. Дополнительные соображения
- Изоляция LLM Если используется внешняя LLM (API), запросы разных tenant'ов могут перемешиваться в контексте. Лучше передавать tenant_id в системном сообщении или через metadata в вызове – это не гарантирует изоляцию, но LLM может игнорировать чужие данные. Для полной изоляции – отдельные модели или инстансы.
- Кэширование Кэш должен быть tenant-aware: ключ включает
tenant_id. Иначе один tenant может получить ответы, сгенерированные на основе данных другого. - Мониторинг: Собирать метрики по каждому tenant’у: латентность, hit rate, количество ошибок. Использовать Prometheus + Grafana.
- Обработка ошибок При сбое в одной коллекции другие не должны страдать (graceful degradation).
Пет-проект для закрепления
Задача Разработать прототип multi-tenant RAG с двумя tenant'ами на Qdrant и FastAPI.
Инструменты
Шаги:
- Запустите Qdrant в Docker.
- Создайте две коллекции:
tenant_aиtenant_b. - Напишите скрипт индексации: сгенерируйте синтетические документы (по 100 на tenant) с разными темами.
- Реализуйте API:
POST /index/{tenant_id}— добавление документа.POST /search/{tenant_id}— поиск с заданным числом результатов (top-k).
- Добавьте rate limiter: 5 запросов в секунду на tenant (используйте token bucket из стандартной библиотеки или стороннюю).
- Протестируйте: отправьте 10 параллельных запросов от одного tenant — после пятого должен вернуться 429.
- Убедитесь, что поиск по
tenant_aне возвращает документыtenant_b.
Ожидаемый результат Работающий сервис, где каждый tenant имеет изолированную коллекцию, rate limiting работает корректно, документация (README) описывает архитектуру.
Связь с другими вопросами
| Вопрос | Тема |
|---|---|
| 1 | Проектирование RAG для 10 000 документов |
| 7 | Уменьшение latency в RAG-системе |
| 9 | Обновление документов в RAG |
| 15 | Обеспечение безопасности и конфиденциальности в RAG |
| 20 | Асинхронная обработка запросов |
| 31 | Выбор векторной БД |
Навигация
- Предыдущий: 83
- Следующий: 85
- Индекс: 00. Индекс разборов