Files
aiform_dev/docs/WIZARD_CACHING_STRATEGY.md
AI Assistant 4c8fda5f55 Добавлено логирование для отладки черновиков
- Добавлены логи в frontend (ClaimForm.tsx) для отслеживания unified_id и запросов к API
- Добавлены логи в backend (claims.py) для отладки SQL запросов
- Создан лог сессии с описанием проблемы и текущего состояния
- Проблема: API возвращает 0 черновиков, хотя в БД есть данные
2025-11-19 18:46:48 +03:00

449 lines
17 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.

# Стратегия кеширования визардов
**Дата:** 2025-01-XX
**Вопрос:** Как кешировать визарды, если они всегда индивидуальные?
---
## 🤔 Проблема
**Кажется, что визарды всегда индивидуальные:**
- Каждое описание проблемы уникально
- Разные детали, разные обстоятельства
- Как найти "похожий" визард?
**НО! На самом деле:**
- **Структура визарда** (вопросы, документы) часто **одинаковая** для похожих типов дел
- **Содержание** (ответы пользователя) - индивидуальное, но это не нужно кешировать
- **Типы дел** повторяются: "дефект товара", "некачественная услуга", "нарушение сроков"
---
## 💡 Решение: Многоуровневое кеширование
### Уровень 1: Кеш по типу дела (самый быстрый)
**Идея:** Визарды для одного типа дела имеют одинаковую структуру
**Как работает:**
```python
# После генерации визарда
case_type = classification["case_type"] # "product_defect", "service_issue", etc.
# Кешируем структуру визарда (без ответов!)
cache_key = f"wizard:template:{case_type}"
redis.set(cache_key, wizard_structure, ttl=86400) # 24 часа
# При следующем запросе
if cached := redis.get(cache_key):
# Используем кеш (0.001 сек)
return cached
```
**Плюсы:**
- ✅ Мгновенно (0.001 сек)
- ✅ Просто реализовать
- ✅ Работает для 80% случаев
**Минусы:**
-Не учитывает нюансы описания
- ❌ Может быть слишком общим
**Когда использовать:**
- Стандартные типы дел (дефект товара, некачественная услуга)
- После апрува визарда администратором
---
### Уровень 2: Кеш по похожести описания (семантический поиск)
**Идея:** Находим похожие описания через векторизацию
**Как работает:**
```python
# 1. Векторизуем описание проблемы
description = "Купил смартфон в DNS, через неделю сломался экран"
embedding = get_text_embedding(description) # [0.1, 0.2, ...]
# 2. Ищем похожие описания в Elasticsearch/векторной БД
similar_cases = vector_search(embedding, limit=5, min_similarity=0.85)
# 3. Если нашли похожий (similarity > 0.85)
if similar_cases:
similar_wizard = similar_cases[0]["wizard_plan"]
# Используем его структуру (можем адаптировать под текущий случай)
return adapt_wizard(similar_wizard, current_description)
```
**Структура в БД:**
```json
{
"description": "Купил смартфон в DNS, через неделю сломался экран",
"description_embedding": [0.1, 0.2, ...],
"wizard_plan": {
"questions": [...],
"documents": [...]
},
"case_type": "product_defect",
"approved": true,
"created_at": "2025-01-15T10:00:00Z"
}
```
**Плюсы:**
- ✅ Учитывает нюансы описания
- ✅ Находит действительно похожие случаи
- ✅ Можно использовать уже апрувленные визарды
**Минусы:**
- ❌ Требует векторную БД (Elasticsearch, Pinecone, Qdrant)
- ❌ Нужна векторизация каждого описания (0.5-1 сек)
- ❌ Поиск занимает время (0.1-0.5 сек)
**Когда использовать:**
- Сложные/уникальные случаи
- После апрува визарда администратором
- Для обучения системы на удачных примерах
---
### Уровень 3: Кеш по хешу описания (точное совпадение)
**Идея:** Если описание точно такое же (или очень похожее) - используем кеш
**Как работает:**
```python
# 1. Вычисляем хеш описания (первые 200-300 символов)
description_hash = hashlib.md5(description[:300].encode()).hexdigest()
# 2. Проверяем кеш
cache_key = f"wizard:hash:{description_hash}"
if cached := redis.get(cache_key):
return cached # Мгновенно!
# 3. Генерируем визард
wizard = generate_wizard(description)
# 4. Сохраняем в кеш
redis.set(cache_key, wizard, ttl=3600) # 1 час
```
**Плюсы:**
- ✅ Мгновенно (0.001 сек)
- ✅ Просто реализовать
- ✅ Работает для повторных запросов
**Минусы:**
- ❌ Только для точных совпадений
-Не учитывает синонимы/перефразировки
**Когда использовать:**
- Тестирование (повторные запросы)
- Защита от дубликатов
---
## 🎯 Комбинированная стратегия (рекомендуется)
### Алгоритм:
```python
async def get_wizard_cached(description: str) -> dict:
"""
Многоуровневое кеширование визардов
"""
# УРОВЕНЬ 1: Точное совпадение (хеш)
description_hash = hashlib.md5(description[:300].encode()).hexdigest()
cache_key_hash = f"wizard:hash:{description_hash}"
if cached := await redis.get(cache_key_hash):
logger.info("✅ Cache hit: hash")
return json.loads(cached)
# УРОВЕНЬ 2: Классификация + шаблон
classification = await classify_case(description) # ИИ: 5-10 сек
case_type = classification["case_type"]
cache_key_template = f"wizard:template:{case_type}"
if cached := await redis.get(cache_key_template):
logger.info("✅ Cache hit: template")
wizard = json.loads(cached)
# Адаптируем под текущий случай (автозаполнение)
wizard = adapt_wizard(wizard, classification["extracted_data"])
return wizard
# УРОВЕНЬ 3: Семантический поиск (похожие случаи)
embedding = await get_text_embedding(description) # 0.5-1 сек
similar_cases = await vector_search(embedding, limit=3, min_similarity=0.85)
if similar_cases and similar_cases[0]["similarity"] > 0.90:
logger.info("✅ Cache hit: similar case")
wizard = similar_cases[0]["wizard_plan"]
wizard = adapt_wizard(wizard, classification["extracted_data"])
return wizard
# УРОВЕНЬ 4: Генерация нового визарда
logger.info("🔄 Generating new wizard")
wizard = await generate_wizard(description) # 30-40 сек
# Сохраняем в кеши всех уровней
await save_to_cache(wizard, description, classification, embedding)
return wizard
async def save_to_cache(wizard, description, classification, embedding):
"""Сохраняем визард во все уровни кеша"""
# 1. Хеш (точное совпадение)
description_hash = hashlib.md5(description[:300].encode()).hexdigest()
await redis.set(
f"wizard:hash:{description_hash}",
json.dumps(wizard),
ttl=3600 # 1 час
)
# 2. Шаблон (по типу дела) - только если визард апрувлен
# (это делается вручную администратором)
# 3. Векторная БД (для семантического поиска)
await vector_db.insert({
"description": description,
"description_embedding": embedding,
"wizard_plan": wizard,
"case_type": classification["case_type"],
"approved": False, # Станет True после апрува
"created_at": datetime.now().isoformat()
})
```
---
## 📊 Когда что использовать
### Сценарий 1: Первый запрос (нет кеша)
```
Описание → Классификация (5-10 сек) → Генерация (30-40 сек) → Сохранение в кеш
```
**Время:** 35-50 секунд
### Сценарий 2: Повторный запрос (точное совпадение)
```
Описание → Хеш → Redis → Визард
```
**Время:** 0.001 секунды ⚡
### Сценарий 3: Похожий тип дела (шаблон)
```
Описание → Классификация (5-10 сек) → Redis (шаблон) → Адаптация → Визард
```
**Время:** 5-10 секунд ⚡⚡
### Сценарий 4: Похожее описание (семантический поиск)
```
Описание → Векторизация (0.5-1 сек) → Поиск (0.1-0.5 сек) → Адаптация → Визард
```
**Время:** 0.6-1.5 секунды ⚡⚡⚡
---
## ✅ Апрув визарда администратором
### Что происходит после апрува:
```python
async def approve_wizard(wizard_id: str):
"""
Администратор апрувит визард
"""
# 1. Получаем визард из БД
wizard = await db.get_wizard(wizard_id)
# 2. Сохраняем как шаблон для этого типа дела
case_type = wizard["case_type"]
await redis.set(
f"wizard:template:{case_type}",
json.dumps(wizard["wizard_plan"]),
ttl=None # Без срока (пока не обновим)
)
# 3. Помечаем в векторной БД как апрувленный
await vector_db.update(wizard_id, {"approved": True})
# 4. Теперь этот визард будет использоваться для всех похожих случаев
```
**Результат:**
-Все новые случаи этого типа будут использовать этот шаблон
- ✅ Время генерации: 5-10 сек (только классификация) вместо 30-40 сек
- ✅ Качество: гарантированно хороший визард (проверен администратором)
---
## 🗄️ Структура хранения
### Redis (быстрый кеш):
```
wizard:hash:{md5_hash} → Визард (TTL: 1 час)
wizard:template:{case_type} → Шаблон визарда (без TTL, обновляется вручную)
```
### Векторная БД (Elasticsearch/Pinecone/Qdrant):
```json
{
"id": "wizard_123",
"description": "Купил смартфон...",
"description_embedding": [0.1, 0.2, ...],
"wizard_plan": {
"questions": [...],
"documents": [...]
},
"case_type": "product_defect",
"approved": true,
"created_at": "2025-01-15T10:00:00Z",
"approved_at": "2025-01-15T11:00:00Z",
"approved_by": "admin@example.com"
}
```
### PostgreSQL (постоянное хранение):
```sql
CREATE TABLE wizard_cache (
id UUID PRIMARY KEY,
description TEXT,
description_hash VARCHAR(64),
case_type VARCHAR(50),
wizard_plan JSONB,
embedding VECTOR(1024), -- pgvector
approved BOOLEAN DEFAULT FALSE,
approved_at TIMESTAMP,
approved_by VARCHAR(255),
created_at TIMESTAMP DEFAULT NOW(),
usage_count INTEGER DEFAULT 0
);
CREATE INDEX idx_wizard_hash ON wizard_cache(description_hash);
CREATE INDEX idx_wizard_case_type ON wizard_cache(case_type);
CREATE INDEX idx_wizard_approved ON wizard_cache(approved) WHERE approved = TRUE;
CREATE INDEX idx_wizard_embedding ON wizard_cache USING ivfflat (embedding vector_cosine_ops);
```
---
## 🚀 Реализация
### Шаг 1: Добавить векторизацию описания
```python
# ticket_form/backend/app/services/embedding_service.py
from openai import OpenAI
class EmbeddingService:
async def get_embedding(self, text: str) -> list[float]:
"""Векторизация текста через OpenAI"""
client = OpenAI(api_key=settings.openai_api_key)
response = client.embeddings.create(
model="text-embedding-3-small", # Быстрая и дешёвая модель
input=text[:8000] # Ограничение длины
)
return response.data[0].embedding
```
### Шаг 2: Добавить векторный поиск
```python
# ticket_form/backend/app/services/wizard_cache_service.py
class WizardCacheService:
async def find_similar_wizards(
self,
embedding: list[float],
limit: int = 5,
min_similarity: float = 0.85
) -> list[dict]:
"""Поиск похожих визардов через векторный поиск"""
# Используем Elasticsearch (уже есть в проекте!)
query = {
"size": limit,
"query": {
"script_score": {
"query": {"match_all": {}},
"script": {
"source": "cosineSimilarity(params.query_vector, 'description_embedding') + 1.0",
"params": {"query_vector": embedding}
},
"min_score": min_similarity + 1.0
}
}
}
results = await elasticsearch.search(
index="wizard_cache",
body=query
)
return [
{
"wizard_plan": hit["_source"]["wizard_plan"],
"similarity": hit["_score"] - 1.0, # Нормализуем
"case_type": hit["_source"]["case_type"]
}
for hit in results["hits"]["hits"]
]
```
### Шаг 3: Интегрировать в генерацию визарда
```python
# ticket_form/backend/app/api/claims.py
@router.post("/wizard/generate")
async def generate_wizard(request: Request):
description = (await request.json())["description"]
# Многоуровневое кеширование
wizard = await wizard_cache_service.get_wizard_cached(description)
return {"wizard_plan": wizard}
```
---
## 📈 Ожидаемые результаты
### До кеширования:
- **Время:** 30-40 секунд для каждого запроса
- **Нагрузка:** Высокая (каждый раз обращение к ИИ)
### После кеширования:
- **Первый запрос:** 30-40 секунд (генерация)
- **Повторный запрос:** 0.001 секунды (хеш) ⚡
- **Похожий тип дела:** 5-10 секунд (шаблон) ⚡⚡
- **Похожее описание:** 0.6-1.5 секунды (семантический поиск) ⚡⚡⚡
### Экономия:
- **80% запросов** будут из кеша (0.001-10 сек вместо 30-40 сек)
- **Снижение нагрузки** на ИИ в 5-10 раз
- **Улучшение UX:** Пользователи получают визарды мгновенно
---
## ✅ Вывод
**Визарды не всегда индивидуальные!**
1. **Структура визарда** (вопросы, документы) повторяется для похожих типов дел
2. **Содержание** (ответы) - индивидуальное, но его не нужно кешировать
3. **Многоуровневое кеширование** позволяет использовать готовые визарды для похожих случаев
**Стратегия:**
- Кеш по хешу (точное совпадение) → 0.001 сек
- Кеш по типу дела (шаблон) → 5-10 сек
- Семантический поиск (похожие описания) → 0.6-1.5 сек
- Генерация нового → 30-40 сек (только если нет кеша)
**После апрува администратором:**
- Визард становится шаблоном для этого типа дела
- Все новые случаи используют этот шаблон (5-10 сек вместо 30-40 сек)