diff --git a/rag/functions/soma_evaluate.py b/rag/functions/soma_evaluate.py index a752ae6..4d296d4 100644 --- a/rag/functions/soma_evaluate.py +++ b/rag/functions/soma_evaluate.py @@ -1,11 +1,18 @@ +# rag/functions/soma_evaluate.py """ SOMA-анализ (LLM-as-judge) для оценки качества ответа. Оценивает ответ по критериям: релевантность, полнота, отсутствие галлюцинаций, стиль, полезность. Возвращает оценки и вердикт. + +Добавлено кэширование результатов (in-memory с TTL) для экономии токенов. +Ключ кэша — хеш от (query, context, answer). +TTL по умолчанию 300 секунд (5 минут), настраивается через config.soma_cache_ttl. """ import json import logging +import hashlib +import time from typing import Dict, Any from rag.services.giga_client import GigaClient @@ -13,6 +20,24 @@ from rag.config_models import AppConfig logger = logging.getLogger(__name__) +# In-memory кэш для SOMA-оценок +# Структура: {cache_key: (timestamp, result_dict)} +_soma_cache: Dict[str, tuple] = {} +# TTL по умолчанию (секунды) +_DEFAULT_TTL = 300 + + +def _get_cache_key(query: str, context: str, answer: str) -> str: + """ + Формирует ключ кэша на основе хеша от (query, context, answer). + """ + # Ограничиваем длину контекста для хеширования, чтобы не перегружать память + # Берём первые 2000 символов контекста, чтобы избежать слишком длинных строк + context_hash = hashlib.md5(context[:2000].encode('utf-8')).hexdigest() + answer_hash = hashlib.md5(answer[:2000].encode('utf-8')).hexdigest() + query_hash = hashlib.md5(query.encode('utf-8')).hexdigest() + return f"soma:{query_hash}:{context_hash}:{answer_hash}" + async def soma_evaluate( query: str, @@ -55,12 +80,28 @@ async def soma_evaluate( "error": None, } + # ---- Проверка кэша ---- + ttl = getattr(config, 'soma_cache_ttl', _DEFAULT_TTL) + cache_key = _get_cache_key(query, context, answer) + + if cache_key in _soma_cache: + timestamp, cached_result = _soma_cache[cache_key] + if time.time() - timestamp < ttl: + logger.debug(f"SOMA-оценка взята из кэша (TTL {ttl} сек)") + return cached_result + else: + # Истекло — удаляем + del _soma_cache[cache_key] + logger.debug("SOMA-кэш истёк") + + # ---- Проверка наличия промпта ---- prompt_template = prompts.get("soma_evaluate") if not prompt_template: logger.error("Промпт для SOMA-оценки не найден") default_result["error"] = "Отсутствует промпт soma_evaluate" return default_result + # ---- Формирование промпта ---- full_prompt = prompt_template.format( query=query, context=context[:3000] if context else "Нет контекста", @@ -93,7 +134,7 @@ async def soma_evaluate( threshold = getattr(config, 'soma_threshold', 3.5) verdict = "pass" if overall >= threshold else "fail" - return { + result = { "scores": scores, "overall_score": overall, "verdict": verdict, @@ -101,6 +142,20 @@ async def soma_evaluate( "error": None, } + # ---- Сохранение в кэш ---- + _soma_cache[cache_key] = (time.time(), result) + logger.debug(f"SOMA-оценка сохранена в кэш (всего {len(_soma_cache)} записей)") + + # Ограничим размер кэша, чтобы не переполнить память + if len(_soma_cache) > 1000: + # Удаляем самые старые записи (по времени) + sorted_keys = sorted(_soma_cache.keys(), key=lambda k: _soma_cache[k][0]) + for old_key in sorted_keys[:200]: # удаляем 200 самых старых + del _soma_cache[old_key] + logger.debug("Кэш SOMA очищен (старые записи удалены)") + + return result + except json.JSONDecodeError as e: logger.error(f"Ошибка парсинга JSON от GigaChat: {e}, ответ: {response[:200]}") default_result["error"] = f"Ошибка парсинга JSON: {e}"