Как бы вы спроектировали 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 tenantMetadata 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.

Инструменты

Шаги:

  1. Запустите Qdrant в Docker.
  2. Создайте две коллекции: tenant_a и tenant_b.
  3. Напишите скрипт индексации: сгенерируйте синтетические документы (по 100 на tenant) с разными темами.
  4. Реализуйте API:
    • POST /index/{tenant_id} — добавление документа.
    • POST /search/{tenant_id} — поиск с заданным числом результатов (top-k).
  5. Добавьте rate limiter: 5 запросов в секунду на tenant (используйте token bucket из стандартной библиотеки или стороннюю).
  6. Протестируйте: отправьте 10 параллельных запросов от одного tenant — после пятого должен вернуться 429.
  7. Убедитесь, что поиск по tenant_a не возвращает документы tenant_b.

Ожидаемый результат Работающий сервис, где каждый tenant имеет изолированную коллекцию, rate limiting работает корректно, документация (README) описывает архитектуру.


Связь с другими вопросами

ВопросТема
1Проектирование RAG для 10 000 документов
7Уменьшение latency в RAG-системе
9Обновление документов в RAG
15Обеспечение безопасности и конфиденциальности в RAG
20Асинхронная обработка запросов
31Выбор векторной БД

Навигация