- Исправлена ошибка ReferenceError при загрузке черновиков - Переименована локальная переменная claimId в finalClaimId для избежания конфликта с параметром функции - Обновлена логика извлечения claim_id из разных источников (claim.claim_id, payload.claim_id, body.claim_id, claim.id) - Добавлен fallback на параметр claimId функции для надёжности
17 KiB
17 KiB
Стратегия кеширования визардов
Дата: 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: Пользователи получают визарды мгновенно
✅ Вывод
Визарды не всегда индивидуальные!
- Структура визарда (вопросы, документы) повторяется для похожих типов дел
- Содержание (ответы) - индивидуальное, но его не нужно кешировать
- Многоуровневое кеширование позволяет использовать готовые визарды для похожих случаев
Стратегия:
- Кеш по хешу (точное совпадение) → 0.001 сек
- Кеш по типу дела (шаблон) → 5-10 сек
- Семантический поиск (похожие описания) → 0.6-1.5 сек
- Генерация нового → 30-40 сек (только если нет кеша)
После апрува администратором:
- Визард становится шаблоном для этого типа дела
- Все новые случаи используют этот шаблон (5-10 сек вместо 30-40 сек)