# Стратегия кеширования визардов **Дата:** 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 сек)