
У моего клиента есть пара железных серверов, которые используются для хранения и раздачи статических файлов. Все бы ничего, но любое оборудование требует внимания и регулярного мониторинга. Со временем диски, модули памяти и другие компоненты могут выходить из строя. Причем умирают они не сразу, сначала молча сыплются ошибки в логи, а потом уже поздно что-то предпринимать: даунтайм, kernel panic, fatal error, ретроспектива и панические атаки. Поэтому важно своевременно отслеживать состояние инфраструктуры и реагировать на предупреждения до того, как они перерастут в серьезные инциденты.
По-хорошему раз в месяц кто-то должен садиться и изучать логи на аномалии, если нужно — писать тикеты и ждать завершения технических работ с серверами. Вполне логичная и рабочая схема, в которой сама собой напрашивается автоматизация, комплексный сбор логов, выявление узких мест и уведомления в профильные каналы. Но, как ни крути, нужно оптимизировать процессы и резать косты, да и человек не всегда имеет желание следить за показателями.
Выражение «искусственный интеллект всех заменит» заиграло новыми красками. Нет, от естественного интеллекта я не отказываюсь, но конкретно анализом «здоровья» этих железных серверов теперь занимается локальная ИИшница. Сейчас покажу, что удалось запилить, как я прикрутил локальную модель и написал нишевого агента под нужды клиента, чисто для анализа логов с железных серверов.
Статью написал Роман Шубин, CTO и автор Telegram-канала Bash Days.
Делать будем как обычно «по-взрослому», Ollama и LM Studio сразу мимо: будем использовать llama.cpp, терминал, компилятор, хардкор и красные глаза. UI и мышка для слабаков.
Для начала подготовим облачный сервер с GPU и установим на него полезную нагрузку. Сервер я взял с запасом, потому что у клиента много других задач, связанных с ИИ. Почему облако? Потому что никуда не нужно идти, ничего не нужно собирать, обслуживать и т. п. Да и если вы купите (а не арендуете) подходящее железо, то что будете с ним делать, когда наиграетесь? Вот-вот, GPU в облаке — мастхэв.
В прошлом году я арендовал подобный сервер на несколько дней в целях эксперимента для научной статьи. Дополнительно приходилось вскрывать клиенту запароленный бэкап, а все вычисления с радужными таблицами и hashcat проводили именно на облачном GPU.
Ладно, это все лирика, продолжаем разговор.
Начинка у меня получилась такая:
8 vCPU,
32 ГБ RAM,
RTX 4090 24 ГБ,
SSD 100 ГБ.
Вполне сносно, чтобы развернуть локальную модель и натравить в нее агента. Но в целом можно выбрать и другую конфигурацию под любые задачи:

И это я еще не весь список вам показал. Как вариант, можно поставить галочку Прерываемый сервер, и он обойдется в разы дешевле.

Одна из причин развернуть собственную модель — сохранить полный контроль над данными. Все запросы, документы и результаты обработки остаются внутри вашей IT-инфраструктуры, а правила доступа и хранения информации определяете вы сами. Вопросы безопасности и работы с чувствительными данными в ИИ-системах — это тема для отдельной статьи. Поэтому пока просто фиксируем у себя в голове: собственная модель дает больше контроля над тем, как и где используются корпоративные данные. А пока двинемся дальше.
Арендуем сервер и накатываем математику:

Первым делом устанавливаем пакеты, драйверы и вспомогательные утилиты.
apt install -y build-essential git curl wget htop nvtop cmake python3 python3-pip ubuntu-drivers-common linux-headers-6.8.0-117-generic
После создания сервера проверяем, что видеокарта успешно выдана виртуальной машине. В панели управления видно, что используется конфигурация с 8 vCPU, 32 ГБ RAM и одной NVIDIA GeForce RTX 4090 с 24 ГБ видеопамяти. Подключаемся по SSH и убеждаемся, что система видит устройство NVIDIA через lspci

Этот вывод подтверждает, что гипервизор корректно пробросил видеокарту в виртуальную машину и можно переходить к установке драйверов CUDA и программного стека для запуска LLM.
Подготовка сервера:
apt update apt install -y \ build-essential \ git \ curl \ wget \ htop \ nvtop \ cmake \ python3 \ python3-pip \ ubuntu-drivers-common \ linux-headers-$(uname -r)
Устанавливаем рекомендуемый драйвер NVIDIA:
ubuntu-drivers devices apt install -y nvidia-driver-595-open reboot
Список доступных драйверов:

После перезагрузки воспользуемся утилитой, чтобы посмотреть, как себя чувствует GPU:
nvidia-smi
Ожидаемый результат - отображение RTX 4090, версии драйвера и объема видеопамяти. Если nvidia-smi отрабатывает без ошибок, сервер полностью готов для установки CUDA, Ollama, vLLM, SGLang, LM Studio Server или других инструментов для запуска и обучения моделей ИИ.

Новые GPU в облаке Selectel от 131,77 ₽/час
Видеокарты для ресурсоемких задач — NVIDIA® H200, RTX™ 6000 Pro.

Отлично, GPU подключилась, CUDA работает. Теперь можно ставить мозги.
fallocate -l 16G /swapfile chmod 600 /swapfile mkswap /swapfile swapon /swapfile echo '/swapfile none swap sw 0 0' >> /etc/fstab apt install -y git cmake build-essential cd /opt git clone https://github.com/ggerganov/llama.cpp.git cd llama.cpp apt install -y nvidia-cuda-toolkit cmake -B build -DGGML_CUDA=ON cmake --build build -j2
На AI серверах swap нужен даже с большой RAM. Тем более при сборке бинарника, когда память начнет стремительно заканчиваться. Поэтому используем ключ -j2 и страхуемся с помощью swap. Если совсем вилы, то можно с j1 поэкспериментировать. Этот ключ показывает, сколько потоков/процессов компиляции запускать одновременно. Но при ограничении потоков сборки компиляция станет в разы медленнее, учитывайте этот момент.

Проверяем:
./build/bin/llama-cli --list-devices

Отлично, бинарник запускается и выводит ожидаемый результат. Скачиваем и устанавливаем модели:
mkdir -p /opt/models/qwen3 cd /opt/models/qwen3 pip install -U huggingface_hub --break-system-packages hf download Aldaris/Qwen3-14B-Q4_K_M-GGUF --include "*.gguf" --local-dir .
Если смущает --break-system-packages, то заведите все это дело в venv, будет более правильно и чисто.
После того как скачали модели, можно делать пробный запуск:
cd /opt/llama.cpp ./build/bin/llama-server \ -m /opt/models/qwen3/qwen3-14b-q4_k_m.gguf \ --host 0.0.0.0 \ --port 8080 \ -ngl 999 \ -t 8 \ --ctx-size 16384 \ --batch-size 1024 \ --ubatch-size 512 \ --flash-attn on\ --parallel 1 \ --mlock \ --no-webui \ --api-key SUPER_SECRET_TOKEN
Что означают параметры:
-m— какую модель будем использовать;--host— открываем 8080 порт и делаем его доступным в интернете;--api-key— ключ для подключения, в запросе нужно его передавать;-ngl 999— Number GPU Layers, выгрузите все слои модели в GPU;--flash-attn on— ускоряет attention, уменьшает memory bandwidth usage;-t 8— Количество CPU threads (больше не всегда быстрее);--ctx-size 16384— размер контекстного окна, память диалога;--batch-size 1024— скорость обработки входного промпта;--parallel 1— количество одновременных inference slots;--mlock— помогает уменьшить swap;--no-webui— нет графического веб-интерфейса.

Проверяем, что все запустилось:
watch -n1 nvidia-smi

Видим, что моделька полностью поместилась в VRAM и это идеально. Проверяем API:
curl http://127.0.0.1:8080/v1/models -H "Authorization: Bearer SUPER_SECRET_TOKEN"

Красота. Все запустилось, API отвечает корректно. Порт 8080 торчит наружу, но пусть вас это не смущает, потому что используется авторизация по токену.
А теперь давайте развлекаться, делаем запрос на API с конкретной задачей:
curl http://127.0.0.1:8080/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer SUPER_SECRET_TOKEN" \ -d '{ "messages":[ { "role":"user", "content":"Привет! Напиши короткий стих." } ] }'
Супер. Получаем ответ:

Видим: predicted_per_second":87.46 — очень достойный результат ~87 токенов/сек

Еще в ответе видим reasoning_content.
То есть модель очень долго думала, но при этом дала короткий ответ. Давайте отключим размышления, так будет быстрее, а на нашу задачу это никак не повлияет.
В запуск llama-server добавляем ключ:
--jinja
И модифицируем запрос:
curl http://127.0.0.1:8080/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer SUPER_SECRET_TOKEN" \ -d '{ "messages":[ { "role":"user", "content":"/no_think Привет! Напиши короткий стих." } ] }'
Получаем моментальный ответ:

Если нужно «подумать», убираем из запроса /no_think. Все гибко и прозрачно. Но под анализ логов «думать» нам особо не нужно. Тут вы сами выбираете баланс — либо шашечки, либо ехать.
Окей, математику и мозги настроили. Идем на сервер, на котором будем анализировать логи, и пишем агента:
/usr/local/sbin/log-agent/agent.py import subprocess import requests import time import hashlib import json import socket from datetime import datetime import psutil from colorama import Fore, Style, init # ========================= # CONFIG # ========================= LLAMA_CPP_URL = "http://195.225.108.127:8080/v1/chat/completions" API_KEY = "SUPER_SECRET_TOKEN" MODEL = "qwen3-14b-q4_k_m.gguf" MATTERMOST_WEBHOOK = "https://chat.bashdays.com/hooks/gfohoxxg" DEDUP_TTL = 1800 # ========================= # INIT # ========================= init(autoreset=True) print("AI LOG AGENT STARTED") HOSTNAME = socket.gethostname() seen = {} KEYWORDS = [ "oom", "out of memory", "segfault", "panic", "fatal", "critical", "corrupt", "i/o error", "disk full", "cannot allocate memory", "too many open files", "database is locked", "connection refused", "killed process", "edac", "ecc", "hardware error", "machine check", "mce", "memory corruption", "bad page state", "page allocation failure", ] PROMPT = """ Ты AI-анализатор Linux серверов и инфраструктуры. Тебе дают потенциально опасные логи. Определи: - ignore - warning - critical Отвечай ТОЛЬКО JSON. Ответы пиши на Русском языке. Формат: { "severity": "critical", "reason": "краткое описание проблемы", "action": "что делать админу" } Правила: critical: - out of memory - kernel panic - filesystem corruption - disk full - repeated crashes - database crash - segfault важных процессов - memory corruption - raid degradation warning: - ECC correctable errors - EDAC errors - machine check events - repeated connection failures ignore: - обычные 404 - reconnect - info/debug - единичные timeout - healthcheck ошибки ОТВЕЧАЙ ТОЛЬКО JSON. БЕЗ markdown. БЕЗ объяснений. БЕЗ текста вне JSON. """ # ========================= # HELPERS # ========================= def log_info(msg): print( Fore.CYAN + f"[{datetime.now().strftime('%H:%M:%S')}] [INFO] " + Style.RESET_ALL + msg ) def log_match(msg): print( Fore.YELLOW + f"[{datetime.now().strftime('%H:%M:%S')}] [MATCH] " + Style.RESET_ALL + msg ) def log_critical(msg): print( Fore.RED + f"[{datetime.now().strftime('%H:%M:%S')}] [CRITICAL] " + Style.RESET_ALL + msg ) def log_warning(msg): print( Fore.MAGENTA + f"[{datetime.now().strftime('%H:%M:%S')}] [WARNING] " + Style.RESET_ALL + msg ) def log_ignore(msg): print( Fore.GREEN + f"[{datetime.now().strftime('%H:%M:%S')}] [IGNORE] " + Style.RESET_ALL + msg ) def should_check(line): l = line.lower() for k in KEYWORDS: if k in l: log_info(f"Matched keyword: {k}") return True return False def dedup(line): h = hashlib.md5(line.encode()).hexdigest() now = time.time() if h in seen: if now - seen[h] < DEDUP_TTL: return False seen[h] = now return True def extract_json(content): content = content.replace("```json", "") content = content.replace("```", "") content = content.strip() if "<think>" in content: parts = content.split("</think>") content = parts[-1].strip() print("\n===== AI RAW RESPONSE =====") print(content) print("===========================\n") start = content.find("{") end = content.rfind("}") if start == -1 or end == -1: raise Exception(f"No JSON found in AI response:\n{content}") content = content[start:end + 1] return json.loads(content) def ask_ai(logline): payload = { "model": MODEL, "messages": [ { "role": "system", "content": PROMPT }, { "role": "user", "content": f"/no_think\n{logline}" } ], "temperature": 0, "max_tokens": 300 } r = requests.post( LLAMA_CPP_URL, headers={ "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json" }, json=payload, timeout=120 ) r.raise_for_status() data = r.json() print("\n===== FULL API RESPONSE =====") print(json.dumps(data, indent=2, ensure_ascii=False)) print("=============================\n") content = data["choices"][0]["message"]["content"] return extract_json(content) def get_system_info(): ram = psutil.virtual_memory() disk = psutil.disk_usage("/") cpu = psutil.cpu_percent(interval=1) return { "cpu": cpu, "ram": ram.percent, "disk": disk.percent } def notify(severity, reason, action, logline): sysinfo = get_system_info() emoji = "🚨" if severity == "critical" else "⚠️" text = ( f"{emoji} **AI Infrastructure Alert**\n\n" f"**Server:** `{HOSTNAME}`\n" f"**Severity:** `{severity.upper()}`\n" f"**Time:** `{datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC`\n\n" f"**Problem:**\n" f"{reason}\n\n" f"**Recommended action:**\n" f"{action}\n\n" f"**System status:**\n" f"• CPU: {sysinfo['cpu']}%\n" f"• RAM: {sysinfo['ram']}%\n" f"• Disk: {sysinfo['disk']}%\n\n" f"**Log:**\n" f"```text\n{logline.strip()}\n```" ) requests.post( MATTERMOST_WEBHOOK, json={"text": text}, timeout=30 ) # ========================= # MAIN # ========================= log_info(f"Starting AI log agent on {HOSTNAME}") proc = subprocess.Popen( ["journalctl", "-f", "-n", "0"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1 ) for line in proc.stdout: line = line.strip() print("RAW:", line) if not line: continue if not should_check(line): continue log_match(line) if not dedup(line): log_info("Duplicate suppressed") continue try: log_info("Sending to llama.cpp") result = ask_ai(line) severity = result.get("severity", "ignore") reason = result.get("reason", "") action = result.get("action", "") log_info(f"AI RESULT: {severity}") if severity == "critical": log_critical(reason) notify(severity, reason, action, line) elif severity == "warning": log_warning(reason) notify(severity, reason, action, line) else: log_ignore(reason) except Exception as e: print( Fore.RED + f"[ERROR] {e}" + Style.RESET_ALL )
В скрипте заполняем:
LLAMA_CPP_URL = "" API_KEY = "" MATTERMOST_WEBHOOK = ""
Устанавливаем зависимости и запускаем:
mkdir -p /usr/local/sbin/log-agent cd /usr/local/sbin/ apt install python3.10-venv python3 -m venv venv source venv/bin/activate pip install requests pip install psutil colorama requests python /usr/local/sbin/log-agent/agent.py

Анализ логов запущен и функционирует. Попробуем симулировать ситуацию с битой плашкой памяти:
logger "kernel: Memory corruption detected in slab allocator"
В канал с алертами получаем ожидаемый результат:

Отлично! Наш локальный ИИ отлично справился с задачей. Теперь ждем появление новых записей в логах.
Вот, как раз с продашена прилетело, делюсь инсайтом:


Ну шикарно же! Теперь все это можно упаковать в единый systemd-юнит /etc/systemd/system/ai-log-agent.service и внедрять в свои процессы.
[Unit] Description=AI Log Agent After=network.target [Service] Type=simple WorkingDirectory=/usr/local/sbin/log-agent/ ExecStart=/usr/local/sbin/log-agent/venv/bin/python /usr/local/sbin/log-agent/agent.py Restart=always RestartSec=5 [Install] WantedBy=multi-user.target systemctl daemon-reload systemctl enable --now ai-log-agent systemctl status ai-log-agent
В массиве KEYWORDS вы можете добавлять свои паттерны, на что скрипт будет триггериться и отправлять проблемные логи в ИИшницу. Ну и если включить thinking, будет поинтереснее, но чуть медленнее. Зато ответы будут более емкие.
Напоследок еще несколько картинок того, как оно работает:


Сейчас у нас все работает на http-протоколе, рекомендую перед llama.cpp воткнуть условный nginx и настроить работу с SSL-сертификатами.
Это действующий проект моего клиента, но на нем происходит не только обработка логов — ребята из смежных отделов активно пользуют hermes agent для генерации отчетных документов, получения erid-токенов для маркировки рекламы и т. п. В общем, используют активно. Так что, если раньше вы скептически относились к подобным решениям, рекомендую пересмотреть свои взгляды.
Что тут еще можно сказать. Облачные сервера с GPU — это прямо прорыв, а для компаний — отличный способ держать корпоративные данные под контролем без лишних затрат на локальную инфраструктуру и обслуживающий персонал.
