Это руководство описывает полный процесс развёртывания системы Софтлайн Классификатор на целевом сервере.
Система запускается в Docker с помощью Docker Compose: все компоненты упакованы в контейнеры и управляются через единый файл docker-compose.yml. Для развёртывания достаточно Docker Engine и Docker Compose — никаких других зависимостей на хосте не требуется.
Документ предназначен для системных администраторов и инженеров, которые впервые запускают систему или восстанавливают её после переноса на новое окружение. Предполагается, что Docker-образы уже собраны и доступны локально или в реестре.
Здесь описано всё, что нужно для запуска: структура файлов, конфигурация, порядок запуска и первичная проверка работоспособности.
Установка состоит из четырёх шагов:
Подготовка файлов. Размещаются docker-compose.yml, service.yml (параметры системы: модели, БД, RAG-поиск) и categories.json (справочник категорий). Создаются директории для хранения данных PostgreSQL, Qdrant, базы знаний и логов.
Проверка доступности внешних сервисов. Убеждаемся, что серверы Ollama (LLM и эмбеддинги) и Whisper (распознавание речи) доступны изнутри Docker-контейнеров.
Запуск. Все четыре сервиса (frontend, backend, postgres, qdrant) поднимаются командой docker-compose up -d.
Проверка работоспособности. Убеждаемся, что веб-интерфейс открывается, контейнеры работают без ошибок, логи бэкенда не содержат критических сбоев.
Внешние зависимости: система не включает в себя 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
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". |
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. Оно выполняет две независимые функции.
При загрузке обучающего файла в систему каждая строка должна содержать текст и метку категории в колонке Категория. Значение в этой колонке должно точно совпадать с полем name одной из категорий из categories.json — именно по name происходит валидация при загрузке.
Это означает, что если в исходном датасете категории подписаны иначе (например, сокращённо или с опечатками), их нужно привести к каноническим name перед загрузкой. Поле alias здесь используется как справочник: оно документирует, какие альтернативные названия исторически применялись для данной категории, чтобы оператор мог правильно сопоставить данные перед загрузкой.
После того как LLM возвращает название категории, бэкенд проверяет его по следующей логике:
alias этой категории.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 можно добавлять любые произвольные поля, не ограничиваясь стандартным набором. Достаточно:
categories.json.fields нужного шаблона в service.yml.{имя_поля} в текст шаблона 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, затем по строковому сходству, затем по семантическому сходству эмбеддингов.
Разместите в корне проекта:
docker-compose.ymlservice.yml (заполненный по шаблону выше)categories.json (заполненный по шаблону выше)Создайте директории:
mkdir -p data/postgres data/qdrant data/kb logs
Убедитесь, что все хосты из service.yml доступны изнутри Docker-контейнеров:
Для хостов на той же машине используйте host.docker.internal вместо localhost.
docker-compose up -d Проверить статус контейнеров:
docker-compose ps Посмотреть логи бэкенда:
docker-compose logs -f backend
| Сервис | 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