no title

Софтлайн Классификатор. Руководство по установке

О документе

Это руководство описывает полный процесс развёртывания системы Софтлайн Классификатор на целевом сервере.

Система запускается в Docker с помощью Docker Compose: все компоненты упакованы в контейнеры и управляются через единый файл docker-compose.yml. Для развёртывания достаточно Docker Engine и Docker Compose — никаких других зависимостей на хосте не требуется.

Документ предназначен для системных администраторов и инженеров, которые впервые запускают систему или восстанавливают её после переноса на новое окружение. Предполагается, что Docker-образы уже собраны и доступны локально или в реестре.

Здесь описано всё, что нужно для запуска: структура файлов, конфигурация, порядок запуска и первичная проверка работоспособности.


Процесс установки (высокоуровнево)

Установка состоит из четырёх шагов:

  1. Подготовка файлов. Размещаются docker-compose.yml, service.yml (параметры системы: модели, БД, RAG-поиск) и categories.json (справочник категорий). Создаются директории для хранения данных PostgreSQL, Qdrant, базы знаний и логов.

  2. Проверка доступности внешних сервисов. Убеждаемся, что серверы Ollama (LLM и эмбеддинги) и Whisper (распознавание речи) доступны изнутри Docker-контейнеров.

  3. Запуск. Все четыре сервиса (frontend, backend, postgres, qdrant) поднимаются командой docker-compose up -d.

  4. Проверка работоспособности. Убеждаемся, что веб-интерфейс открывается, контейнеры работают без ошибок, логи бэкенда не содержат критических сбоев.

Внешние зависимости: система не включает в себя LLM-сервер и сервер распознавания речи — они должны быть развёрнуты заранее и быть доступны из Docker-контейнеров.


Архитектура системы

Система состоит из четырёх сервисов, описанных в docker-compose.yml:

Сервис Образ Назначение
frontend slclass-front Веб-интерфейс
backend slclass-back Логика классификации, API
postgres pgvector/pgvector:pg17 Реляционная БД с поддержкой векторов
qdrant qdrant/qdrant Векторная БД для семантического поиска

Структура директорий

Перед запуском создайте следующую структуру в корне проекта:

.
├── docker-compose.yml
├── service.yml          # Конфигурация системы
├── categories.json      # Справочник категорий
├── logs/                # Логи приложения
└── data/
    ├── kb/              # База знаний (документы для RAG)
    ├── postgres/        # Данные PostgreSQL
    └── qdrant/          # Данные Qdrant

Создать директории одной командой:

mkdir -p data/postgres data/qdrant data/kb logs

Файл 1: service.yml

Основной конфигурационный файл системы. Монтируется в оба контейнера — frontend и backend.

Шаблон файла

app: frontend: category_timeout: 600000 backend: host: "0.0.0.0" port: 7700 service_name: "backend" database: postgres: host: "postgres" port: 5432 user: "postgres" password: "" database: "classification_db" embedding_service: vector_storage: "qdrant" ollama_host: "" # Укажите URL Ollama ollama_model: "" # Укажите модель для embedding в Ollama ollama_context_length: 40000 qdrant: host: "qdrant" port: 6333 api_key: "" collection_prefix: "class" collection_name: "class" rag: hybrid_search_alpha: 0.2 top_k: 50 min_text_score: 0.001 min_semantic_score: 0.0 min_total_score: 0.0 categories_top_k: 10 prompts: rag_query: - text: |
            {text_query} - fields: - "text_query" rag_query_image: - text: |
            {text_query}
            {images} - fields: - "text_query" - "images" rag_image_template: - text: |
            Текст изображения: {image_text}
            Объекты изображения: {image_objects} - fields: - "image_text" - "image_objects" rag_audio_template: - text: |
            Аудио: {audio} - fields: - "audio" rag_query_audio: - text: |
            {text_query}
            {audio} - fields: - "text_query" - "audio" rag_query_audio_image: - text: |
            {text_query}
            {audio_image} - fields: - "text_query" - "audio_image" rag_audio_image: - text: |
            {audio}
            {images} - fields: - "audio" - "images" system: - text: |
            Ты - эксперт по категоризации заявок или различных текстов. Ты отлично справляешься с этой задачей, помогая пользователям определить категорию их сообщения. ЗАДАЧА ПО ШАГАМ:
              1. От пользователя ты получаешь заявку <INPUT> и возможные категории для данной заявки <CATEGORIES>.
              2. Пользователь хочет узнать к какой категории относится заявка <INPUT>.
              3. Твоя задача определить какая из категорий наиболее подходит для заявки <INPUT>.
              4. В результате ты должен написать одну категорию, которая относится к заявке <INPUT>.
              5. Ответ нужно вернуть в XML формате. "<КАТЕГОРИЯ>тут указана выбранная категория</КАТЕГОРИЯ>"

            ВАЖНО УЧЕСТЬ: - Пользователь хочет знать только к какой категории относится заявка <INPUT>.
              - Входная заявка <INPUT> может содержать текст <TEXT>, изображения <IMAGES>, аудио <AUDIO>.
              - Используй ТОЛЬКО категории, которые есть в <CATEGORIES>.
              - Перед окончательным ответом убедись, что выбрал одну категорию.
              - Все размышления должны быть ТОЛЬКО на русском языке.

            ЗАПРЕЩЕНО: - Не давать никаких рекомендаций по улучшению.
              - Не давать никаких пояснений в финальном ответе, только категорию.
              - Использовать вымышленные категории, которых нет в <CATEGORIES>.

      system_with_rag: - text: |
            Ты - эксперт по категоризации заявок или различных текстов. Ты отлично справляешься с этой задачей, помогая пользователям определить категорию их текста. ЗАДАЧА ПО ШАГАМ:
              1. От пользователя ты получаешь заявку <INPUT> и возможные категории для данной заявки <CATEGORIES>.
              2. Пользователь хочет узнать к какой категории относится заявка <INPUT>.
              3. Твоя задача определить какая из категорий наиболее подходит для заявки <INPUT>.
              4. В результате ты должен написать одну категорию, которая относится к заявке <INPUT>.
              5. Ответ нужно вернуть в XML формате. "<КАТЕГОРИЯ>тут указана выбранная категория</КАТЕГОРИЯ>"

            ВАЖНО УЧЕСТЬ: - Используй ТОЛЬКО категории, которые есть в <CATEGORIES>.
              - Процент похожих заявок при принятии решения. Если процент высокий и смысл заявки совпадает с категорией, то выбери именно эту категорию.
              - Перед окончательным ответом убедись, что выбрал одну категорию.
              - Все размышления должны быть ТОЛЬКО на русском языке.

            ЗАПРЕЩЕНО: - Не давать никаких рекомендаций по улучшению.
              - Не давать никаких пояснений в финальном ответе, только категорию.
              - Использовать вымышленные категории, которых нет в <CATEGORIES>.

      system_prompt_extraction: - text: |
            Ты являешься помощником для извлечения категории из текста. Правила: - В тексте может содержаться категория, чаще всего она встречается внутри XML-тега <КАТЕГОРИЯ> ... </КАТЕГОРИЯ> или рядом с подстрокой "Категория:".
            - Если категория найдена — верни её точное значение (без изменений). Установи status = true.
            - Если категория не найдена — верни пустую строку и status = false.
            - Ничего не додумывай.

      category_template: - text: |
            <CATEGORY>
              <ИМЯ_КАТЕГОРИИ>{name}</ИМЯ_КАТЕГОРИИ>
              <ОПИСАНИЕ>{description}</ОПИСАНИЕ>
              <НЕ_ПОПАДАЮЩИЕ_В_КАТЕГОРИЮ>{not_include}</НЕ_ПОПАДАЮЩИЕ_В_КАТЕГОРИЮ>
              <ПРИМЕРЫ_ЗАПРОСОВ>{examples}</ПРИМЕРЫ_ЗАПРОСОВ>
            </CATEGORY> - fields: - "name" - "description" - "not_include" - "examples" category_template_with_rag: - text: |
            <CATEGORY>
            <ИМЯ_КАТЕГОРИИ>{name}</ИМЯ_КАТЕГОРИИ>
            Описание: {description}
            <НЕ_ПОПАДАЮЩИЕ_В_КАТЕГОРИЮ>{not_include}</НЕ_ПОПАДАЮЩИЕ_В_КАТЕГОРИЮ>
            <ПРИМЕРЫ_ЗАПРОСОВ>{examples}</ПРИМЕРЫ_ЗАПРОСОВ>
            <КОЛИЧЕСТВО_ПОХОЖИХ_ЗАПРОСОВ>{category_count}%</КОЛИЧЕСТВО_ПОХОЖИХ_ЗАПРОСОВ>
            </CATEGORY> - fields: - "name" - "description" - "not_include" - "examples" - "category_count" user_prompt: - text: |
            Помоги определить категорию заявки:
            <INPUT>
            <TEXT>
            {text}
            </TEXT>
            <IMAGES>
            {images}
            </IMAGES>
            <AUDIO>
            {audio}
            </AUDIO>
            </INPUT>

            <CATEGORIES> {categories}
            </CATEGORIES> - fields: - "text" - "categories" - "images" - "audio" llms: models: - |
        {
          "id": "MODEL_ID",
          "name": "Отображаемое имя",
          "provider": "ollama",
          "type": "casual",
          "host": "http://HOST:PORT"
        } vision_models: - |
        {
          "id": "MODEL_ID",
          "name": "Отображаемое имя",
          "provider": "ollama",
          "type": "vision",
          "host": "http://HOST:PORT"
        } extraction_model: provider: "Ollama" config: model: "" base_url: "" asr_model: provider: "openai" config: model: "whisper" base_url: "" api_key: "" language: "ru" 

Описание всех переменных

app.frontend

Переменная Тип Описание
category_timeout int (мс) Таймаут ожидания ответа от бэкенда на фронтенде. По умолчанию 600000 (10 минут).

app.backend

Переменная Тип Описание
host string Адрес, на котором слушает бэкенд. Обычно "0.0.0.0".
port int Порт бэкенда. По умолчанию 7700.
service_name string Внутреннее имя сервиса.

app.backend.database.postgres

Переменная Тип Описание
host string Хост PostgreSQL. Внутри Docker — имя сервиса "postgres".
port int Порт PostgreSQL. По умолчанию 5432.
user string Имя пользователя БД. Должно совпадать с POSTGRES_USER в docker-compose.yml.
password string Пароль пользователя БД. Должно совпадать с POSTGRES_PASSWORD в docker-compose.yml.
database string Имя базы данных. Должно совпадать с POSTGRES_DB в docker-compose.yml.

app.backend.embedding_service

Переменная Тип Описание
vector_storage string Бэкенд для хранения векторов. Единственный поддерживаемый вариант: "qdrant".
ollama_host string URL сервера Ollama для генерации эмбеддингов. Пример: "http://host.docker.internal:11434".
ollama_model string Название модели эмбеддингов в Ollama. Пример: "qwen3-embedding:8b-q4_K_M".
ollama_context_length int Максимальная длина контекста для модели эмбеддингов в токенах.

app.backend.embedding_service.qdrant

Переменная Тип Описание
host string Хост Qdrant. Внутри Docker — имя сервиса "qdrant".
port int Порт REST API Qdrant. По умолчанию 6333.
api_key string API-ключ для Qdrant (если настроена аутентификация). Оставить пустым, если не используется.
collection_prefix string Префикс для имён коллекций. Оставить пустым, если не используется.
collection_name string Имя коллекции в Qdrant для хранения векторов документов базы знаний.

app.backend.embedding_service.rag

Переменная Тип Диапазон Описание
hybrid_search_alpha float 0.0–1.0 Баланс между полнотекстовым (0.0) и семантическим (1.0) поиском. Рекомендуется 0.2.
top_k int 1–∞ Количество документов, извлекаемых из базы знаний на этапе поиска.
min_text_score float 0.0–1.0 Минимальный порог релевантности для полнотекстового поиска. Документы ниже порога отбрасываются.
min_semantic_score float 0.0–1.0 Минимальный порог релевантности для семантического поиска.
min_total_score float 0.0–1.0 Минимальный порог итогового гибридного скора.
categories_top_k int 1–∞ Количество категорий-кандидатов, передаваемых в LLM для финального выбора.

app.llms.models — список LLM-моделей

Каждая модель описывается JSON-строкой со следующими полями:

Поле Тип Описание
id string Идентификатор модели в провайдере (например, "llama3.1:8b-instruct-q8_0").
name string Отображаемое имя модели в интерфейсе.
provider string Провайдер модели: "ollama" или "openai".
type string Тип модели: "casual" (обычная) или "reasoning" (с цепочкой рассуждений).
host string URL API-сервера модели. Пример: "http://host.docker.internal:11434".
api_key string API-ключ (только для провайдера "openai").

app.llms.vision_models — список мультимодальных моделей

Те же поля, что и у models. Используются для обработки изображений. Тип должен быть "vision".

app.extraction_model

Модель для извлечения названия категории из ответа основной LLM.

Переменная Тип Описание
provider string Провайдер: "Ollama" или "openai".
config.model string Идентификатор модели. Пример: "qwen3:4b".
config.base_url string URL API-сервера.

app.asr_model

Модель для распознавания речи (Automatic Speech Recognition).

Переменная Тип Описание
provider string Провайдер: "openai" (совместимый с Whisper API).
config.model string Идентификатор модели. Пример: "whisper".
config.base_url string URL API-сервера Whisper.
config.api_key string API-ключ. Если аутентификация не нужна, можно указать любую строку, например "x".
config.language string Язык распознавания (ISO 639-1). Пример: "ru".

Файл 2: categories.json

Справочник категорий для классификации. Бэкенд читает этот файл при старте и загружает категории в систему.

Шаблон файла

{ "categories": [ { "name": "Название категории", "description": "Подробное описание того, что входит в эту категорию.", "not_include": "Описание того, что НЕ должно попадать в эту категорию.", "examples": "Примеры типичных запросов для этой категории.", "alias": ["Синоним 1", "Синоним 2"] } ] } 

Описание полей

Поле Тип Обязательное Описание
name string Да Уникальное каноническое название категории. Именно это значение возвращается системой как итоговый результат классификации.
description string Да Подробное описание категории. Передаётся в LLM как контекст для принятия решения. Чем точнее описание — тем лучше качество классификации.
not_include string Рекомендуется Явные исключения: что не должно попадать в эту категорию. Помогает LLM разграничивать похожие категории.
examples string Рекомендуется Примеры реальных запросов, которые относятся к этой категории. Используются как few-shot примеры для LLM.
alias array of strings Важно Альтернативные названия категории. Подробнее — в разделе ниже.
code string Нет Альтернативный идентификатор категории. Используется бэкендом для сопоставления ответа LLM с эталонным названием через строковое и семантическое сходство.
любое поле string Нет Произвольные пользовательские поля. Могут быть подставлены в промпт через шаблон category_template в service.yml. Подробнее — в разделе «Как работает система шаблонов» ниже.

Важно: После изменения categories.json необходимо перезапустить сервис backend:

docker-compose restart backend

Поле alias и его роль в системе

Поле alias — одно из ключевых в categories.json. Оно выполняет две независимые функции.

1. Разметка базы знаний (при загрузке документов)

При загрузке обучающего файла в систему каждая строка должна содержать текст и метку категории в колонке Категория. Значение в этой колонке должно точно совпадать с полем name одной из категорий из categories.json — именно по name происходит валидация при загрузке.

Это означает, что если в исходном датасете категории подписаны иначе (например, сокращённо или с опечатками), их нужно привести к каноническим name перед загрузкой. Поле alias здесь используется как справочник: оно документирует, какие альтернативные названия исторически применялись для данной категории, чтобы оператор мог правильно сопоставить данные перед загрузкой.

2. Резервное сопоставление ответа LLM

После того как LLM возвращает название категории, бэкенд проверяет его по следующей логике:

  1. Если ответ LLM точно совпадает с одной из категорий-кандидатов — используется как есть.
  2. Если точного совпадения нет — бэкенд перебирает все категории и проверяет, входит ли ответ LLM в массив alias этой категории.
  3. Если совпадение найдено — возвращается каноническое name этой категории.

Это позволяет системе корректно обрабатывать случаи, когда LLM возвращает синоним или альтернативное написание вместо точного названия.

Пример:

{ "name": "Здоровье, медицина", "alias": ["Здоровье", "Медицина", "Здравоохранение", "здоровье", "медицина"] } 

Если LLM ответит "Медицина" — система вернёт "Здоровье, медицина".

Рекомендация: Заполняйте alias максимально полно — включайте все варианты написания (с заглавной и строчной буквы, сокращения, синонимы). Это напрямую влияет на надёжность финального результата.

Как работает система шаблонов

Связь между categories.json и service.yml строится через механизм шаблонов. Это позволяет гибко управлять тем, какие данные о категории передаются в LLM и в каком формате.

Принцип работы

При формировании промпта бэкенд берёт каждую категорию-кандидат и рендерит для неё блок текста по шаблону category_template (или category_template_with_rag при использовании RAG). Шаблон описан в service.yml и состоит из двух частей:

  • text — строка с плейсхолдерами вида {имя_поля}
  • fields — список полей, которые будут подставлены в плейсхолдеры

Бэкенд берёт объект категории из categories.json, для каждого поля из списка fields находит значение в объекте категории и подставляет его в text. Если поле в категории отсутствует — подставляется пустая строка.

Пользовательские поля

Ключевая возможность: в categories.json можно добавлять любые произвольные поля, не ограничиваясь стандартным набором. Достаточно:

  1. Добавить поле в объект категории в categories.json.
  2. Добавить имя этого поля в список fields нужного шаблона в service.yml.
  3. Добавить плейсхолдер {имя_поля} в текст шаблона text.

Пример: добавление поля tone

Допустим, нужно передавать в LLM информацию о тональности категории.

categories.json:

{ "categories": [ { "name": "Жалобы", "description": "Сообщения с выражением недовольства...", "not_include": "Жалобы на ЖКХ...", "examples": "«Нарушены трудовые права»", "alias": ["Жалобы"], "tone": "негативная, эмоциональная" } ] } 

service.yml — добавляем поле и плейсхолдер в шаблон:

category_template: - text: |
      <CATEGORY>
        <ИМЯ_КАТЕГОРИИ>{name}</ИМЯ_КАТЕГОРИИ>
        <ОПИСАНИЕ>{description}</ОПИСАНИЕ>
        <НЕ_ПОПАДАЮЩИЕ_В_КАТЕГОРИЮ>{not_include}</НЕ_ПОПАДАЮЩИЕ_В_КАТЕГОРИЮ>
        <ПРИМЕРЫ_ЗАПРОСОВ>{examples}</ПРИМЕРЫ_ЗАПРОСОВ>
        <ТОНАЛЬНОСТЬ>{tone}</ТОНАЛЬНОСТЬ>
      </CATEGORY> - fields: - "name" - "description" - "not_include" - "examples" - "tone" 

В результате LLM получит блок с тегом <ТОНАЛЬНОСТЬ>негативная, эмоциональная</ТОНАЛЬНОСТЬ> для этой категории.

Зарезервированное поле category_count

В шаблоне category_template_with_rag есть специальное поле category_count — его не нужно добавлять в categories.json. Бэкенд вычисляет его автоматически: это процент документов из базы знаний, которые попали в данную категорию при RAG-поиске. Значение передаётся в LLM как подсказка о статистической близости запроса к категории.

Зарезервированное поле code

Поле code в объекте категории используется бэкендом для сопоставления результата LLM с эталонным названием категории. Если LLM вернула код вместо имени, система попытается найти категорию сначала по точному совпадению name или code, затем по строковому сходству, затем по семантическому сходству эмбеддингов.


Порядок запуска с нуля

Шаг 1. Подготовка файлов

Разместите в корне проекта:

  • docker-compose.yml
  • service.yml (заполненный по шаблону выше)
  • categories.json (заполненный по шаблону выше)

Создайте директории:

mkdir -p data/postgres data/qdrant data/kb logs

Шаг 2. Проверка доступности внешних сервисов

Убедитесь, что все хосты из service.yml доступны изнутри Docker-контейнеров:

  • Серверы Ollama (для эмбеддингов, LLM, extraction_model)
  • Сервер Whisper (для ASR)

Для хостов на той же машине используйте host.docker.internal вместо localhost.

Шаг 3. Запуск

docker-compose up -d 

Проверить статус контейнеров:

docker-compose ps 

Посмотреть логи бэкенда:

docker-compose logs -f backend

Шаг 4. Проверка работоспособности

Сервис URL
Веб-интерфейс http://localhost:3000 (или значение FRONTEND_PORT)

Порты PostgreSQL и Qdrant по умолчанию закомментированы в docker-compose.yml и наружу не пробрасываются. Чтобы получить прямой доступ, раскомментируйте нужные строки ports: в docker-compose.yml.


Переменные окружения docker-compose.yml

Можно переопределить через файл .env в корне проекта:

Переменная Значение по умолчанию Описание
FRONTEND_PORT 3000 Внешний порт веб-интерфейса.
ENABLE_CONSOLE_LOGGING false Включить вывод логов бэкенда в консоль Docker.
POSTGRES_PORT 5432 Внешний порт PostgreSQL. По умолчанию закомментирован в docker-compose.yml — наружу не пробрасывается.
QDRANT_PORT 6333 Внешний порт Qdrant (REST API). По умолчанию закомментирован в docker-compose.yml — наружу не пробрасывается.
QDRANT_GRPC_PORT 6334 Внешний порт Qdrant (gRPC). По умолчанию закомментирован в docker-compose.yml — наружу не пробрасывается.

Приложение: docker-compose.yml

services: frontend: image: docker.slutech.ru/slclass-front:2.1.3
    volumes: - ./service.yml:/app/config/service.yml:ro
    ports: - "${FRONTEND_PORT:-3000}:3000" depends_on: - backend
    restart: "unless-stopped" backend: image: docker.slutech.ru/slclass-back:2.0.1
    environment: - ENABLE_CONSOLE_LOGGING=${ENABLE_CONSOLE_LOGGING:-false} volumes: - ./service.yml:/root/app/service.yml
      - ./categories.json:/root/app/categories.json
      - ./data/kb:/root/app/kb_documents
      - ./logs:/root/app/logs
    depends_on: - postgres
      - qdrant
    restart: "unless-stopped" postgres: image: pgvector/pgvector:pg17
    environment: POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres_password_test
      POSTGRES_DB: classification_db
      POSTGRES_HOST_AUTH_METHOD: md5
      POSTGRES_INITDB_ARGS: --auth-host=scram-sha-256 # ports: #   - "${POSTGRES_PORT:-5432}:5432" volumes: - ./data/postgres:/var/lib/postgresql/data
    command: ["postgres", "-c", "listen_addresses=*", "-c", "max_connections=100"] restart: "unless-stopped" qdrant: image: qdrant/qdrant:latest
    ports: #  - "${QDRANT_PORT:-6333}:6333"  # REST API #  - "${QDRANT_GRPC_PORT:-6334}:6334"  # gRPC API volumes: - ./data/qdrant:/qdrant/storage
    environment: - QDRANT__TELEMETRY_DISABLED=true
    restart: unless-stopped
    healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8333/"] interval: 10s
      timeout: 5s
      retries: 5