support-bot-saas/.opencode/plans/saas-rag-plan.md
2026-06-20 16:07:21 +03:00

374 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# План: 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
```python
# 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:**
```python
# 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` индексируется, поиск не замедляется
**Регистрация пользователя:**
1. Создать запись в PostgreSQL
2. Сгенерировать `api_key`
3. 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:
1. Парсинг в единую схему
2. apply `has_real_solution` filter
3. `build_chunk` → search_text + full_text
4. E5-large → вектор (через API embedding-воркера)
5. Upsert в Qdrant
6. Обновить `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": "Проверьте логин"}
]
}
]
}
```
Ответ:
```json
{
"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` и др. Выбор уточнить при старте — нужно тестировать качество на русском языке.
```python
# 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
}
```
Ответ:
```json
{
"answer": "Проверьте настройки камеры...",
"sources": [
{
"ticket_id": "ext-001",
"score": 3.51,
"category": "Сбой"
}
]
}
```
### Внутренняя логика search:
1. E5-large: query → вектор
2. Qdrant: search по tenant_id + collection_id → 20 кандидатов
3. Cross-encoder (CPU): rerank query vs full_text всех 20
4. Взять топ-5
5. Если `generate_answer=true` → LLM API (OpenAI/GigaChat) с системным промптом
6. Вернуть ответ + источники
### LLM провайдер — OpenRouter
На этапе разработки — бесплатные модели OpenRouter (deepseek, qwen, gemini-free). Потом платные.
Клиент — стандартный OpenAI SDK с `base_url="https://openrouter.ai/api/v1"`.
```python
# 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 (локальная разработка)
```yaml
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 — После запуска
1. **Мониторинг:** Prometheus + Grafana (latency поиска, ошибки LLM, кол-во тикетов)
2. **Биллинг:** Stripe / YooKassa — free (1 коллекция, 1000 тикетов), pro (10 коллекций, 100k тикетов)
3. **Документация:** OpenAPI (Swagger у FastAPI из коробки), README по интеграции
4. **Rate limiting:** slowapi — для free-тарифа 10 запросов/мин, для pro — без лимита
---
## Принятые решения
| Вопрос | Решение |
|--------|---------|
| LLM провайдер | OpenRouter (бесплатные → платные) |
| Embedding | OpenRouter API (если качество не устроит — отдельный E5-large микросервис на CPU) |
| Фронтенд | React SPA |
| Очередь задач | Celery + Redis |
| K8s GPU | Нет, всё через API |
| Тарифы | Free / Pro / Enterprise |
## Открытые вопросы
1. **Модель для embedding через OpenRouter** — нужно протестировать `text-embedding-3-small/large` на русском, сравнить с E5-large.
2. **LLM модель по умолчанию** — deepseek/qwen/gemini-free, решить после тестов.
3. **Регионы / 152-ФЗ** — если целевая аудитория в РФ, нужен GigaChat как опция и хранение в РФ.