13 KiB
План: RAG SaaS для техподдержки
Статус
- Этап 0 — Форк и структура проекта
- Этап 1 — Бэкенд: регистрация, авторизация, коллекции
- Этап 2 — Бэкенд: приём тикетов (UI + API)
- Этап 3 — Бэкенд: поиск и генерация ответа
- Этап 4 — Фронтенд: SPA
- Этап 5 — Инфраструктура: K8s + CI/CD
- Этап 6 — Мониторинг, биллинг, доки
Этап 0 — Форк и структура
Сделать форк текущего коммита 41b84e6. Новая структура:
support-bot-saas/
├── backend/
│ ├── app/
│ │ ├── main.py # FastAPI приложение
│ │ ├── config.py # настройки (env)
│ │ ├── auth/ # JWT, регистрация
│ │ ├── models/ # Pydantic схемы
│ │ ├── routers/
│ │ │ ├── auth.py # /api/register, /api/login
│ │ │ ├── collections.py # /api/collections
│ │ │ ├── tickets.py # /api/collections/:id/tickets
│ │ │ └── search.py # /api/search
│ │ ├── services/
│ │ │ ├── embedding.py # E5-large (общий воркер)
│ │ │ ├── vector_store.py # Qdrant client
│ │ │ ├── chunking.py # подготовка текста
│ │ │ └── llm.py # внешний LLM
│ │ └── workers/
│ │ └── process_file.py # Celery — обработка файлов
│ ├── Dockerfile
│ └── requirements.txt
├── frontend/
│ ├── src/
│ └── Dockerfile
├── helm/ # K8s чарты
├── docker-compose.yml # для локальной разработки
└── .github/workflows/ # CI/CD
Что берём из текущего кода:
- chunking.py —
build_chunk,has_real_solution,clean_text - cross-encoder реранжировщик (на Django-side или отдельный микросервис)
Этап 1 — Бэкенд: регистрация, коллекции, Qdrant
1.1 FastAPI + PostgreSQL
# models/user.py
class User(Base):
id: uuid
email: str
password_hash: str
api_key: str # для API-интеграции
created_at: datetime
plan: str # free, pro, enterprise
# models/collection.py
class Collection(Base):
id: uuid
user_id: uuid # FK → user
name: str
ticket_count: int
created_at: datetime
1.2 Qdrant multi-tenancy
Одна коллекция Qdrant tickets, tenant_id в payload:
# services/vector_store.py
async def create_tenant_index(user_id: str):
# Qdrant: одна общая коллекция + фильтр по tenant_id
# Индекс создаётся один раз глобально
pass
async def upsert_chunks(user_id: str, chunks: list[dict]):
client.upsert(
collection_name="tickets",
points=[
PointStruct(
id=chunk["id"],
vector=chunk["vector"],
payload={
"tenant_id": user_id,
"ticket_id": chunk["ticket_id"],
"category": chunk["category"],
"search_text": chunk["search_text"],
"full_text": chunk["full_text"],
}
)
]
)
async def search(user_id: str, query_vector: list, k: int = 20):
return client.query_points(
collection_name="tickets",
query=query_vector,
query_filter=Filter(
must=[FieldCondition(key="tenant_id", match={"value": user_id})]
),
limit=k,
)
Почему одна коллекция, а не на пользователя:
- Qdrant сам рекомендует такую схему для SaaS
- Тысячи коллекций жрут память (каждая коллекция = свой HNSW-граф в RAM)
- Фильтр по
tenant_idиндексируется, поиск не замедляется
Регистрация пользователя:
- Создать запись в PostgreSQL
- Сгенерировать
api_key - Qdrant ничего не делать — фильтрация по
tenant_idбудет работать автоматически
API endpoints этапа 1
POST /api/register # email + пароль → JWT + api_key
POST /api/login # email + пароль → JWT
GET /api/me # профиль
POST /api/collections # создать базу знаний
GET /api/collections # список баз
DELETE /api/collections/:id # удалить базу (не рвать Qdrant)
Этап 2 — Приём тикетов
2.1 Через UI (загрузка файла)
- Поддерживаемые форматы: JSON, CSV, XLSX
- После загрузки → Celery task:
- Парсинг в единую схему
- apply
has_real_solutionfilter build_chunk→ search_text + full_text- E5-large → вектор (через API embedding-воркера)
- Upsert в Qdrant
- Обновить
ticket_countв Collection
2.2 Через API
POST /api/collections/:id/tickets
Authorization: Bearer <api_key>
Content-Type: application/json
{
"tickets": [
{
"ticket_id": "ext-001",
"category": "Сбой",
"description": "Не могу войти",
"messages": [
{"role": "client", "text": "Пишет ошибку"},
{"role": "support", "text": "Проверьте логин"}
]
}
]
}
Ответ:
{
"processed": 1,
"skipped": 0,
"collection_ticket_count": 142
}
2.3 Embedding — через OpenRouter API
GPU в K8s не будет, embedding тоже через OpenRouter. OpenRouter поддерживает несколько embedding-моделей: text-embedding-3-small, text-embedding-3-large и др. Выбор уточнить при старте — нужно тестировать качество на русском языке.
# services/embedding.py
class OpenRouterEmbeddings:
def __init__(self, api_key: str, model: str = "text-embedding-3-small"):
self.client = OpenAI(base_url="https://openrouter.ai/api/v1", api_key=api_key)
self.model = model
async def embed(self, texts: list[str]) -> list[list[float]]:
response = await self.client.embeddings.create(
model=self.model,
input=texts
)
return [item.embedding for item in response.data]
Альтернатива: если качество OpenRouter embedding не устроит — вынести E5-large в отдельный микросервис с CPU (batch inference, ~5-10 текстов/сек) или использовать другой API (Jina, Voyage, Cohere).
Этап 3 — Поиск и генерация
POST /api/search
Authorization: Bearer <api_key>
{
"collection_id": "uuid",
"query": "не могу загрузить фото в приложение",
"generate_answer": true
}
Ответ:
{
"answer": "Проверьте настройки камеры...",
"sources": [
{
"ticket_id": "ext-001",
"score": 3.51,
"category": "Сбой"
}
]
}
Внутренняя логика search:
- E5-large: query → вектор
- Qdrant: search по tenant_id + collection_id → 20 кандидатов
- Cross-encoder (CPU): rerank query vs full_text всех 20
- Взять топ-5
- Если
generate_answer=true→ LLM API (OpenAI/GigaChat) с системным промптом - Вернуть ответ + источники
LLM провайдер — OpenRouter
На этапе разработки — бесплатные модели OpenRouter (deepseek, qwen, gemini-free). Потом платные.
Клиент — стандартный OpenAI SDK с base_url="https://openrouter.ai/api/v1".
# services/llm.py
class OpenRouterLLM:
def __init__(self, api_key: str, model: str = "deepseek/deepseek-chat"):
self.client = OpenAI(base_url="https://openrouter.ai/api/v1", api_key=api_key)
self.model = model
async def generate(self, system_prompt: str, context: str, query: str) -> str:
response = await self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"Контекст:\n{context}\n\nВопрос:\n{query}"}
]
)
return response.choices[0].message.content
Модель по умолчанию: уточнить после тестов. Варианты: deepseek/deepseek-chat (дёшево, хорошо для RAG), qwen/qwen-2.5-7b-instruct (бесплатно), google/gemini-2.0-flash-free (бесплатно).
Системный промпт — тот же, что сейчас в app.py.
Этап 4 — Фронтенд
SPA на React (или Vue, решать тебе):
Страницы:
/login,/register/dashboard— список коллекций, статистика/collections/:id— просмотр тикетов, загрузка файла, поиск/settings— API ключ, профиль
Чат-интерфейс поиска:
┌─────────────────────┐
│ Вопрос: │
│ [________________] │
│ [Найти] │
├─────────────────────┤
│ Ответ ассистента │
│ ─────────────────── │
│ Текст... │
│ │
│ 📄 Найденные │
│ Тикет №123 │
│ Тикет №456 │
└─────────────────────┘
Технологии: React + Vite + Tailwind или просто Jinja2 + HTMX для MVP.
Этап 5 — Инфраструктура
docker-compose (локальная разработка)
services:
postgres:
redis:
qdrant:
backend:
build: ./backend
env:
- OPENROUTER_API_KEY=${OPENROUTER_API_KEY}
- DATABASE_URL=postgresql://...
frontend:
build: ./frontend
env: ...
worker: # Celery — парсинг, векторизация, upsert
build: ./backend
command: celery -A app.workers worker
K8s (Helm-чарт helm/support-bot-saas/)
Deployments:
├── backend — FastAPI (2-3 реплики)
├── worker — Celery (1-2 реплики)
├── frontend — Nginx + SPA
└── cronjob — очистка старых данных
StatefulSets:
├── postgres
├── redis
└── qdrant
Ingress: api.domain.com, app.domain.com
CI/CD (GitHub Actions)
push → lint + test → build Docker → push to registry → deploy to k8s
Этап 6 — После запуска
- Мониторинг: Prometheus + Grafana (latency поиска, ошибки LLM, кол-во тикетов)
- Биллинг: Stripe / YooKassa — free (1 коллекция, 1000 тикетов), pro (10 коллекций, 100k тикетов)
- Документация: OpenAPI (Swagger у FastAPI из коробки), README по интеграции
- Rate limiting: slowapi — для free-тарифа 10 запросов/мин, для pro — без лимита
Принятые решения
| Вопрос | Решение |
|---|---|
| LLM провайдер | OpenRouter (бесплатные → платные) |
| Embedding | OpenRouter API (если качество не устроит — отдельный E5-large микросервис на CPU) |
| Фронтенд | React SPA |
| Очередь задач | Celery + Redis |
| K8s GPU | Нет, всё через API |
| Тарифы | Free / Pro / Enterprise |
Открытые вопросы
- Модель для embedding через OpenRouter — нужно протестировать
text-embedding-3-small/largeна русском, сравнить с E5-large. - LLM модель по умолчанию — deepseek/qwen/gemini-free, решить после тестов.
- Регионы / 152-ФЗ — если целевая аудитория в РФ, нужен GigaChat как опция и хранение в РФ.