From f3e31e4310eec2e60373ff23212a4a8db6607231 Mon Sep 17 00:00:00 2001 From: Andrey Date: Wed, 28 Jan 2026 00:55:54 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20endpoints=20=D0=B4=D0=BB=D1=8F=20=D1=83=D0=BF?= =?UTF-8?q?=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BF=D0=B0?= =?UTF-8?q?=D1=80=D0=B0=D0=BC=D0=B5=D1=82=D1=80=D0=B0=D0=BC=D0=B8=20=D1=84?= =?UTF-8?q?=D0=BE=D1=80=D0=BC=D1=83=D0=BB=D1=8B,=20=D1=83=D0=B4=D0=B0?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BD=D0=B5=D0=BD=D1=83=D0=B6=D0=BD?= =?UTF-8?q?=D1=8B=D0=B5=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4=D1=8B,=20=D0=B8?= =?UTF-8?q?=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=B4?= =?UTF-8?q?=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/routes.py | 88 ++++++++++++++++++++------- app/main.py | 19 ++---- app/schemas.py | 116 ++++++++++++++++++++++++++++++++++-- app/services/rag_service.py | 1 - app/storage/vector_store.py | 95 ++++++++++++++++++++++++----- 5 files changed, 264 insertions(+), 55 deletions(-) diff --git a/app/api/routes.py b/app/api/routes.py index ec35408..7d2c714 100644 --- a/app/api/routes.py +++ b/app/api/routes.py @@ -23,7 +23,9 @@ from app.schemas import ( ScoreMetadata, ScoreRequest, ScoreResponse, + ScoringParamsResponse, StatsResponse, + UpdateScoringParamsRequest, VectorStoreStats, WarmupResponse, ) @@ -53,7 +55,7 @@ RAGServiceDep = Annotated[RAGService, Depends(get_service)] summary="Проверка здоровья сервиса", tags=["health"], ) -async def health_check(service: RAGServiceDep) -> HealthResponse: +async def health_check(service: RAGServiceDep, _auth: AuthDep) -> HealthResponse: """ Проверяет состояние сервиса. @@ -305,7 +307,6 @@ async def get_stats(service: RAGServiceDep, _auth: AuthDep) -> StatsResponse: model_name=stats["model_name"], model_loaded=stats["model_loaded"], device=stats["device"], - cache_dir=stats["cache_dir"], vector_store=VectorStoreStats(**stats["vector_store"]), ) @@ -349,37 +350,80 @@ async def warmup(service: RAGServiceDep, _auth: AuthDep) -> WarmupResponse: ) -@router.post( - "/save", - response_model=dict, +# ============================================================================= +# Scoring Parameters +# ============================================================================= + +@router.get( + "/scoring/params", + response_model=ScoringParamsResponse, responses={ 401: {"model": ErrorResponse, "description": "Не авторизован"}, 403: {"model": ErrorResponse, "description": "Доступ запрещён"}, }, - summary="Сохранить векторы на диск", - tags=["management"], + summary="Получить параметры формулы расчета score", + tags=["scoring"], ) -async def save_vectors(service: RAGServiceDep, _auth: AuthDep) -> dict: +async def get_scoring_params( + service: RAGServiceDep, + _auth: AuthDep, +) -> ScoringParamsResponse: """ - Сохраняет векторы на диск. + Возвращает текущие параметры формулы расчета score. Args: service: RAG сервис Returns: - dict: Результат сохранения + ScoringParamsResponse: Текущие параметры формулы + """ + params = service.vector_store.get_scoring_params() + return ScoringParamsResponse(**params) + + +@router.put( + "/scoring/params", + response_model=ScoringParamsResponse, + responses={ + 400: {"model": ErrorResponse, "description": "Ошибка в запросе"}, + 401: {"model": ErrorResponse, "description": "Не авторизован"}, + 403: {"model": ErrorResponse, "description": "Доступ запрещён"}, + }, + summary="Обновить параметры формулы расчета score", + tags=["scoring"], +) +async def update_scoring_params( + request: UpdateScoringParamsRequest, + service: RAGServiceDep, + _auth: AuthDep, +) -> ScoringParamsResponse: + """ + Обновляет параметры формулы расчета score. + + Можно обновить один или несколько параметров одновременно. + Параметры, которые не указаны, остаются без изменений. + + Args: + request: Запрос с новыми параметрами + service: RAG сервис + + Returns: + ScoringParamsResponse: Обновленные параметры формулы + + Raises: + HTTPException: При невалидных значениях параметров """ try: - service.save_vectors() - return { - "success": True, - "message": "Векторы сохранены на диск", - "positive_count": service.vector_store.positive_count, - "negative_count": service.vector_store.negative_count, - } - except Exception as e: - logger.error(f"Ошибка сохранения векторов: {e}") - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail={"detail": str(e), "error_type": "VectorStoreError"}, + params = service.vector_store.update_scoring_params( + score_multiplier=request.score_multiplier, + k_min=request.k_min, + k_max=request.k_max, + base_multiplier_factor=request.base_multiplier_factor, + ) + return ScoringParamsResponse(**params) + except ValueError as e: + logger.warning(f"Невалидные параметры формулы: {e}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail={"detail": str(e), "error_type": "ValueError"}, ) diff --git a/app/main.py b/app/main.py index 1250357..45a08e3 100644 --- a/app/main.py +++ b/app/main.py @@ -146,7 +146,7 @@ app = FastAPI( * **Скоринг** - оценка текстов на основе векторного сходства с примерами * **Примеры** - добавление положительных и отрицательных примеров * **Статистика** - мониторинг состояния сервиса - * **Управление** - прогрев модели, сохранение векторов + * **Управление** - прогрев модели, настройка параметров формулы ## Алгоритм скоринга @@ -160,6 +160,11 @@ app = FastAPI( docs_url="/docs", redoc_url="/redoc", openapi_url="/openapi.json", + swagger_ui_parameters={ + "syntaxHighlight.theme": "agate", + "defaultModelsExpandDepth": 1, + "defaultModelExpandDepth": 1, + }, ) # CORS middleware (для возможных веб-клиентов) @@ -175,18 +180,6 @@ app.add_middleware( app.include_router(router, prefix="/api/v1") -# Корневой endpoint -@app.get("/", tags=["root"]) -async def root() -> dict: - """Корневой endpoint с информацией о сервисе.""" - return { - "service": "RAG Service", - "version": __version__, - "docs": "/docs", - "health": "/api/v1/health", - } - - if __name__ == "__main__": import uvicorn diff --git a/app/schemas.py b/app/schemas.py index 86fa688..69c86d4 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -99,7 +99,6 @@ class VectorStoreStats(BaseModel): total_count: int = Field(..., description="Общее количество примеров") vector_dim: int = Field(..., description="Размерность векторов") max_examples: int = Field(..., description="Максимальное количество примеров") - storage_path: Optional[str] = Field(None, description="Путь к файлу хранилища") class StatsResponse(BaseModel): @@ -107,7 +106,6 @@ class StatsResponse(BaseModel): model_name: str = Field(..., description="Название модели") model_loaded: bool = Field(..., description="Загружена ли модель") device: Optional[str] = Field(None, description="Устройство (cpu/cuda)") - cache_dir: str = Field(..., description="Директория кеша модели") vector_store: VectorStoreStats = Field(..., description="Статистика хранилища векторов") model_config = { @@ -116,14 +114,12 @@ class StatsResponse(BaseModel): "model_name": "DeepPavlov/rubert-base-cased", "model_loaded": True, "device": "cpu", - "cache_dir": "data/models", "vector_store": { "positive_count": 500, "negative_count": 350, "total_count": 850, "vector_dim": 768, - "max_examples": 10000, - "storage_path": "data/vectors/vectors.npz" + "max_examples": 10000 } } } @@ -177,3 +173,113 @@ class HealthResponse(BaseModel): } } } + + +class ScoringParamsResponse(BaseModel): + """Ответ с текущими параметрами формулы расчета score.""" + score_multiplier: float = Field( + ..., + description=( + "Базовый множитель для усиления разницы в скорах. " + "Используется как основа для расчета финального множителя. " + "Чем больше значение, тем сильнее влияние разницы между положительными и отрицательными примерами на итоговый score. " + "Рекомендуемое значение: 5.0" + ) + ) + k_min: int = Field( + ..., + description=( + "Минимальное количество ближайших примеров для расчета среднего сходства. " + "Алгоритм берет топ-k самых похожих примеров из каждого типа (положительные/отрицательные) " + "и вычисляет среднее косинусное сходство. " + "Меньшее значение k делает алгоритм более чувствительным к различиям, но может быть менее стабильным. " + "Рекомендуемое значение: 5" + ) + ) + k_max: int = Field( + ..., + description=( + "Максимальное количество ближайших примеров для расчета среднего сходства. " + "Алгоритм выбирает k в диапазоне [k_min, k_max] в зависимости от количества доступных примеров. " + "Большее значение k делает алгоритм более стабильным, но менее чувствительным к различиям. " + "Должно быть >= k_min. Рекомендуемое значение: 10" + ) + ) + base_multiplier_factor: float = Field( + ..., + description=( + "Множитель для базового score_multiplier. " + "Финальный множитель рассчитывается как: score_multiplier * base_multiplier_factor * адаптивный_коэффициент. " + "Этот параметр усиливает влияние разницы между положительными и отрицательными примерами. " + "Чем больше значение, тем больше диапазон итогового score (от 0 до 1). " + "Рекомендуемое значение: 15.0" + ) + ) + + model_config = { + "json_schema_extra": { + "example": { + "score_multiplier": 5.0, + "k_min": 5, + "k_max": 10, + "base_multiplier_factor": 15.0 + } + } + } + + +class UpdateScoringParamsRequest(BaseModel): + """Запрос на обновление параметров формулы расчета score.""" + score_multiplier: Optional[float] = Field( + None, + gt=0, + description=( + "Базовый множитель для усиления разницы в скорах. " + "Используется как основа для расчета финального множителя. " + "Чем больше значение, тем сильнее влияние разницы между положительными и отрицательными примерами на итоговый score. " + "Должен быть > 0. Рекомендуемое значение: 5.0" + ) + ) + k_min: Optional[int] = Field( + None, + ge=1, + description=( + "Минимальное количество ближайших примеров для расчета среднего сходства. " + "Алгоритм берет топ-k самых похожих примеров из каждого типа (положительные/отрицательные) " + "и вычисляет среднее косинусное сходство. " + "Меньшее значение k делает алгоритм более чувствительным к различиям, но может быть менее стабильным. " + "Должно быть >= 1. Рекомендуемое значение: 5" + ) + ) + k_max: Optional[int] = Field( + None, + ge=1, + description=( + "Максимальное количество ближайших примеров для расчета среднего сходства. " + "Алгоритм выбирает k в диапазоне [k_min, k_max] в зависимости от количества доступных примеров. " + "Большее значение k делает алгоритм более стабильным, но менее чувствительным к различиям. " + "Должно быть >= 1 и >= k_min. Рекомендуемое значение: 10" + ) + ) + base_multiplier_factor: Optional[float] = Field( + None, + gt=0, + description=( + "Множитель для базового score_multiplier. " + "Финальный множитель рассчитывается как: score_multiplier * base_multiplier_factor * адаптивный_коэффициент. " + "Этот параметр усиливает влияние разницы между положительными и отрицательными примерами. " + "Чем больше значение, тем больше диапазон итогового score (от 0 до 1). " + "Должен быть > 0. Рекомендуемое значение: 15.0" + ) + ) + + model_config = { + "json_schema_extra": { + "example": { + "score_multiplier": 5.0, + "k_min": 5, + "k_max": 10, + "base_multiplier_factor": 15.0 + } + } + } diff --git a/app/services/rag_service.py b/app/services/rag_service.py index c94ef9f..990b95b 100644 --- a/app/services/rag_service.py +++ b/app/services/rag_service.py @@ -466,7 +466,6 @@ class RAGService: "model_name": self.model_name, "model_loaded": self._model_loaded, "device": self._device, - "cache_dir": self.cache_dir, "vector_store": self.vector_store.get_stats(), } diff --git a/app/storage/vector_store.py b/app/storage/vector_store.py index c99dfbb..4eaec69 100644 --- a/app/storage/vector_store.py +++ b/app/storage/vector_store.py @@ -37,6 +37,9 @@ class VectorStore: max_examples: int = 10000, storage_path: Optional[str] = None, score_multiplier: float = 5.0, + k_min: int = 5, + k_max: int = 10, + base_multiplier_factor: float = 15.0, ): """ Инициализация хранилища. @@ -45,12 +48,18 @@ class VectorStore: vector_dim: Размерность векторов max_examples: Максимальное количество примеров каждого типа storage_path: Путь для сохранения/загрузки векторов (опционально) - score_multiplier: Множитель для усиления разницы в скорах + score_multiplier: Базовый множитель для усиления разницы в скорах + k_min: Минимальное значение k для топ-k ближайших примеров + k_max: Максимальное значение k для топ-k ближайших примеров + base_multiplier_factor: Множитель для базового score_multiplier """ self.vector_dim = vector_dim self.max_examples = max_examples self.storage_path = storage_path self.score_multiplier = score_multiplier + self.k_min = k_min + self.k_max = k_max + self.base_multiplier_factor = base_multiplier_factor # Инициализируем пустые массивы # Используем список для динамического добавления, потом конвертируем в numpy @@ -284,15 +293,15 @@ class VectorStore: neg_similarities = np.array([]) # Используем топ-k ближайших примеров для более чувствительной оценки - # Берем очень небольшое k (3-5) для максимальной чувствительности к различиям - k_pos = min(5, max(3, len(pos_similarities))) + # Берем k в диапазоне [k_min, k_max] для большей чувствительности к различиям + k_pos = min(self.k_max, max(self.k_min, len(pos_similarities))) # Топ-k положительных примеров (самые близкие) top_k_pos_sim = float(np.mean(np.sort(pos_similarities)[-k_pos:])) # Для отрицательных: если их меньше k, берем все, иначе топ-k if len(neg_similarities) > 0: - k_neg = min(5, max(3, len(neg_similarities))) + k_neg = min(self.k_max, max(self.k_min, len(neg_similarities))) top_k_neg_sim = float(np.mean(np.sort(neg_similarities)[-k_neg:])) else: # Если нет отрицательных примеров, используем нейтральное значение @@ -303,20 +312,14 @@ class VectorStore: diff = top_k_pos_sim - top_k_neg_sim # Увеличиваем множитель для большей чувствительности к малым различиям - # Базовый множитель умножаем на 25-30 для работы с топ-k (которые дают значения 0.95-0.99) - base_multiplier = self.score_multiplier * 25.0 + # Базовый множитель умножаем на base_multiplier_factor для работы с топ-k + base_multiplier = self.score_multiplier * self.base_multiplier_factor # Адаптивный множитель: чем больше примеров, тем выше чувствительность # При 500 примерах: 1.25, при 1000+: 1.5 adaptive_multiplier = base_multiplier * (1.0 + min(0.5, (self.positive_count + self.negative_count) / 2000)) - # Используем нелинейное преобразование для усиления различий - # Применяем квадратичную функцию к разнице для большей чувствительности - # Если diff положительный - усиливаем, если отрицательный - тоже усиливаем - sign = 1.0 if diff >= 0 else -1.0 - amplified_diff = sign * (abs(diff) ** 0.8) * 1.2 # Слегка нелинейное усиление - - score_neg_pos = 0.5 + (amplified_diff * adaptive_multiplier) + score_neg_pos = 0.5 + (diff * adaptive_multiplier) score_neg_pos = max(0.0, min(1.0, score_neg_pos)) # === Вариант 2: pos only (только положительные, топ-k ближайших) === @@ -443,5 +446,69 @@ class VectorStore: "total_count": self.total_count, "vector_dim": self.vector_dim, "max_examples": self.max_examples, - "storage_path": self.storage_path, } + + def get_scoring_params(self) -> dict: + """Возвращает текущие параметры формулы расчета score.""" + return { + "score_multiplier": self.score_multiplier, + "k_min": self.k_min, + "k_max": self.k_max, + "base_multiplier_factor": self.base_multiplier_factor, + } + + def update_scoring_params( + self, + score_multiplier: Optional[float] = None, + k_min: Optional[int] = None, + k_max: Optional[int] = None, + base_multiplier_factor: Optional[float] = None, + ) -> dict: + """ + Обновляет параметры формулы расчета score. + + Args: + score_multiplier: Базовый множитель (должен быть > 0) + k_min: Минимальное значение k (должно быть >= 1) + k_max: Максимальное значение k (должно быть >= k_min) + base_multiplier_factor: Множитель для базового score_multiplier (должен быть > 0) + + Returns: + dict: Обновленные параметры + + Raises: + ValueError: При невалидных значениях + """ + with self._lock: + if score_multiplier is not None: + if score_multiplier <= 0: + raise ValueError("score_multiplier должен быть > 0") + self.score_multiplier = score_multiplier + + if k_min is not None: + if k_min < 1: + raise ValueError("k_min должен быть >= 1") + if self.k_max < k_min: + raise ValueError("k_min не может быть больше k_max") + self.k_min = k_min + + if k_max is not None: + if k_max < 1: + raise ValueError("k_max должен быть >= 1") + if k_max < self.k_min: + raise ValueError("k_max не может быть меньше k_min") + self.k_max = k_max + + if base_multiplier_factor is not None: + if base_multiplier_factor <= 0: + raise ValueError("base_multiplier_factor должен быть > 0") + self.base_multiplier_factor = base_multiplier_factor + + logger.info( + f"VectorStore: Параметры формулы обновлены: " + f"score_multiplier={self.score_multiplier}, " + f"k_min={self.k_min}, k_max={self.k_max}, " + f"base_multiplier_factor={self.base_multiplier_factor}" + ) + + return self.get_scoring_params()