Files
crm.clientright.ru/ticket_form/docs/WIZARD_CACHING_STRATEGY.md
Fedor de011efba9 fix: исправлен конфликт имён переменных в loadDraft (claimId -> finalClaimId)
- Исправлена ошибка ReferenceError при загрузке черновиков
- Переименована локальная переменная claimId в finalClaimId для избежания конфликта с параметром функции
- Обновлена логика извлечения claim_id из разных источников (claim.claim_id, payload.claim_id, body.claim_id, claim.id)
- Добавлен fallback на параметр claimId функции для надёжности
2025-11-19 23:33:52 +03:00

17 KiB
Raw Permalink Blame History

Стратегия кеширования визардов

Дата: 2025-01-XX
Вопрос: Как кешировать визарды, если они всегда индивидуальные?


🤔 Проблема

Кажется, что визарды всегда индивидуальные:

  • Каждое описание проблемы уникально
  • Разные детали, разные обстоятельства
  • Как найти "похожий" визард?

НО! На самом деле:

  • Структура визарда (вопросы, документы) часто одинаковая для похожих типов дел
  • Содержание (ответы пользователя) - индивидуальное, но это не нужно кешировать
  • Типы дел повторяются: "дефект товара", "некачественная услуга", "нарушение сроков"

💡 Решение: Многоуровневое кеширование

Уровень 1: Кеш по типу дела (самый быстрый)

Идея: Визарды для одного типа дела имеют одинаковую структуру

Как работает:

# После генерации визарда
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: Кеш по похожести описания (семантический поиск)

Идея: Находим похожие описания через векторизацию

Как работает:

# 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)

Структура в БД:

{
  "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: Кеш по хешу описания (точное совпадение)

Идея: Если описание точно такое же (или очень похожее) - используем кеш

Как работает:

# 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 сек)
  • Просто реализовать
  • Работает для повторных запросов

Минусы:

  • Только для точных совпадений
  • Не учитывает синонимы/перефразировки

Когда использовать:

  • Тестирование (повторные запросы)
  • Защита от дубликатов

🎯 Комбинированная стратегия (рекомендуется)

Алгоритм:

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 секунды


Апрув визарда администратором

Что происходит после апрува:

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):

{
  "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 (постоянное хранение):

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: Добавить векторизацию описания

# 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: Добавить векторный поиск

# 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: Интегрировать в генерацию визарда

# 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 сек)