Ночной дежурный: нейросеть следит за сервером, правила будят человека
Мы хотели, чтобы ночью за серверами дежурила нейросеть. Прежде чем доверить ей ночь, прогнали её на 20 своих старых авариях. Вывод: будить человека должны простые правила в коде, а нейросеть подключается после сигнала и объясняет, что случилось. Ниже схема по шагам. Отдайте гайд своему боту, он подберёт её под ваш стек.
Разбудила человека
- на упавшую базу ответила «подождём»
- 47 запросов к модели обошлись в $0,40
Назвала причину
- её действия: 10 помогли бы
- 8 ничего бы не изменили
- 2 навредили бы
Это повтор старых аварий: на каждом шаге модель видела только то, что уже произошло к этому моменту. Живой ночи наш дежурный ещё не дежурил, и действия ему мы пока не доверили. Выборка маленькая: на ваших авариях цифры будут другими.
Как отдать гайд боту
Скопируйте этот промпт и вставьте в чат с ботом. Бот сначала уточнит у вас, где работают серверы, какой мониторинг уже есть и кто отвечает ночью, сравнит варианты и предложит план. Делать что-то он начнёт только после вашего решения.
Помоги мне устроить ночное дежурство для серверов моего продукта по гайду «Ночной дежурный» (https://prtner.ai/guides/nochnoy-dezhurny/). Сделай так:1. Прочитай гайд целиком. Его схема: правила в коде будят человека, нейросеть только объясняет после сигнала, видит серверы лишь на чтение, действует через проверку в коде и в пределах дневного потолка. 2. Задай мне вопросы и ничего не делай: где работают мои серверы и база (облако, свой сервер, платформа), какой мониторинг и оповещения уже есть, кто отвечает ночью и как его будить, какие аварии были за последние месяцы и где лежат их записи, сколько я готов тратить на модель в день. 3. Сравни 2–3 варианта под мой случай, например: дописать правила в мой текущий мониторинг, поставить отдельного простого сторожа, взять готовый сервис дежурств. Для каждого назови плюсы, минусы и примерную цену. 4. Предложи план по шагам 1–6 гайда и остановись: дальше только после моего решения. Не запускай и не предлагай запускать команды, не проси доступов к серверам, пока я не решу. Никогда не проси прислать в чат пароли, ключи, токены или логи с личными данными.
Будят правила в коде
Решение «будить или нет» принимает код, а не модель. Правило простое: что проверяем, как часто, когда считаем отказом. Отказ подтверждается несколькими проверками подряд, иначе один сбой сети разбудит вас зря.
# rules.yaml: пример, подставьте своё
- id: api_down
check: GET https://api.example.com/health # ждём 200 за 5 секунд
every: 1m
fire: 3 неудачи подряд из двух точек
wake: yes
- id: db_down
check: SELECT 1 на основной базе
every: 1m
fire: 2 неудачи подряд
wake: yes
- id: disk_low
check: свободное место на диске базы
every: 5m
fire: меньше 10 % дольше 15 минут
wake: yes
- id: errors_5xx
check: доля ответов 5xx
every: 1m
fire: больше 5 % за 5 минут, если запросов 50 и больше
wake: yes
- id: backup_late
check: возраст последнего бэкапа
every: 1h
fire: старше 26 часов
wake: no # не будит, уходит в утреннюю сводкуСторож работает отдельно от того, что сторожит: на другом сервере или в облачной функции. Если упадёт ваш сервер, сторож должен остаться.
Сам сторож тоже может умереть. Поставьте «выключатель мертвеца»: после каждого прохода сторож пингует внешний сервис, и если пингов нет, этот сервис будит вас сам.
# в конце каждого прохода проверок
curl -fsS --max-time 10 https://hc.example.com/ping/<ваш-id>
# внешний сервис ждёт пинг каждые 5 минут;
# нет пинга 10 минут: звонок или пуш дежурномуНейросеть объясняет после сигнала
Модель подключается, когда правило уже сработало. Ей передают: какое правило, его показания за последние полчаса, недавние выкладки и список инструментов для чтения из шага 3. Её работа: коротко и простыми словами объяснить, что видно.
Оповещение уходит сразу, объяснение догоняет его вторым сообщением. Если модель не ответила или упала, человек всё равно разбужен.
Модель не может отменить сигнал. Даже если она пишет «всё в порядке», решение правила остаётся в силе.
Одно объяснение на одну аварию, а не на каждую проверку.
Что должно быть в сообщении:
Сработало: db_down. База не отвечает 4 минуты, 2 проверки из 2.
Что случилось: основная база не принимает соединения, API отвечает 503.
Вероятная причина: диск базы заполнен (свободно 0,3 %, за час +6 ГБ).
Уверенность: средняя. Логи базы прочитать не удалось: инструмент вернул ошибку.
Что проверить:
1. Что заняло диск базы.
2. Не копится ли журнал из-за слота репликации или сломанной архивации.
Чего не делать: не перезапускать базу, пока диск полон.
Предложение: нет. В этой ситуации действует только человек.В инструкции модели напишите прямо: «Ты не решаешь, будить ли человека. Пиши только то, что видел в данных. Скажи, что прочитать не удалось. Если данных мало, так и напиши».
Только чтение, через узкую дверь
У модели нет ключа SSH и нет пароля от базы. Она видит серверы только через небольшой набор инструментов, например через свой MCP-сервер. Каждый инструмент назван отдельно, только читает и ограничен по объёму.
service_status(service) работает ли, с какого времени, последний перезапуск
recent_errors(service, minutes) последние ошибки, не больше 60 минут и 50 строк
logs(service, minutes, contains) строки лога после маскировки, не больше 30 минут
metrics(name, minutes) ряд значений: CPU, память, диск, задержки
rule_history(rule_id) когда это правило срабатывало раньше
recent_deploys(hours) что и когда выкатывалиПароли и ключи живут у сервера инструментов, модель их не видит. Если инструменту нужна база, ему заводят отдельную роль только для чтения.
Модели разрешают инструменты по именам, а не весь сервер целиком. Новый инструмент появляется только после отдельного решения.
Логи маскируются до того, как попадут к модели: токены, пароли, почты, телефоны, номера карт.
-- роль для сервера инструментов: статистика без права писать
CREATE ROLE duty_reader LOGIN PASSWORD '...';
GRANT pg_monitor TO duty_reader;
ALTER ROLE duty_reader SET default_transaction_read_only = on;
ALTER ROLE duty_reader SET statement_timeout = '5s';# маскировка строки лога перед отдачей модели, порядок важен
import re
MASKS = [
(re.compile(r'(?i)\b(bearer|token|api[_-]?key|secret|password)\b\s*[=:]?\s*\S+'), r'\1 ***'),
(re.compile(r'\b\d{4}[ -]?\d{4}[ -]?\d{4}[ -]?\d{4}\b'), '<card>'),
(re.compile(r'[\w.+-]+@[\w-]+\.[\w.-]+'), '<email>'),
(re.compile(r'\+?\d[\d ()-]{8,}\d'), '<phone>'),
]
def mask(line):
for rx, repl in MASKS:
line = rx.sub(repl, line)
return lineДействия только через проверку в коде
Модель может предложить действие, но до сервера оно доходит только через проверку. Для каждой ситуации заранее записан список разрешённых действий и условия. Проверка берёт факты сама, словам модели о состоянии сервера она не верит.
# actions.yaml: что можно и когда
api_down:
restart_service:
target: api
only_if:
db_ok: true # база отвечает
deploy_running: false # не идёт выкладка
maintenance: false # нет плановых работ
restarts_last_30m: 0
enabled: false # включается только после учений
disk_low:
rotate_logs:
target: api
enabled: false
db_down: {} # только человекdef allow(situation, action, target):
rule = ACTIONS.get(situation, {}).get(action)
if not rule or not rule["enabled"] or rule["target"] != target:
return False
facts = collect_facts() # сторож проверяет сам
return all(facts.get(k) == v for k, v in rule.get("only_if", {}).items())Каждое действие включается только после учений. На копии сервиса вы ломаете его нарочно и смотрите две вещи: действие помогает, а в запрещённой ситуации проверка его не пропускает. До учений действие остаётся предложением, которое выполняет человек.
Жёсткий потолок на день
Ошибка в цикле может отправить к модели тысячи запросов. Потолок ставится в коде и отдельно у провайдера модели.
DAILY_CAP_USD = 10 # пример, подберите под себя
MAX_CALLS_PER_INCIDENT = 20
def explain(incident):
if spent_today() >= DAILY_CAP_USD or incident.calls >= MAX_CALLS_PER_INCIDENT:
notify(incident, "объяснения не будет: исчерпан лимит")
return
...Заведите дежурному отдельный ключ API и поставьте лимит расходов на него в кабинете провайдера.
Когда лимит кончился, оповещения идут дальше, просто без объяснений.
В нашем прогоне хватило уровня Claude Sonnet. Для ориентира: наш прогон 20 аварий, 47 запросов, стоил $0,40.
Прогоните свои прошлые аварии
Прежде чем доверить модели ночь, проверьте её на своих авариях. Соберите 15–30 случаев из разборов, чата и истории оповещений. Добавьте несколько спокойных, где будить никого не нужно было: так видно ложные тревоги.
{
"id": "inc-07",
"title": "Кончился диск на сервере базы",
"steps": [
{"t": 0, "readings": {"api_health": "ok", "disk_free_pct": 4.1}},
{"t": 10, "readings": {"api_health": "ok", "disk_free_pct": 0.9}},
{"t": 15, "readings": {"api_health": "503", "db_ping": "timeout", "disk_free_pct": 0.0}}
],
"expect": {
"wake_by_t": 15,
"cause": "диск базы заполнен архивом журналов",
"helpful": ["освободить место", "починить архивацию"],
"harmful": ["перезапуск базы при полном диске"]
}
}Прогон идёт по шагам. На каждом шаге модель видит только то, что уже случилось, без ответа из expect.
Прогоните дважды: модель одна и модель после сигнала правила. Так видно, кому что доверять.
Оценивает не та же модель в том же запросе: человек или отдельный вызов, которому дан ответ из expect.
Что считать:
Разбудил, когда был нужен: сколько из всех аварий, где нужен человек.
Ложные тревоги на спокойных случаях.
Причина: верно, частично, неверно.
Действия: помогли бы, ничего не изменили, навредили бы.
Цена прогона.
Что остаётся на вас
Каждую новую аварию добавляйте в набор для прогона.
Раз в неделю сверяйте объяснения модели с тем, что было на самом деле.
Раз в месяц проверяйте «выключатель мертвеца»: остановите сторожа и дождитесь звонка.
Держите правила простыми. Сложное правило, которое никто не понимает, ночью не поможет.
Меняйте пароли сервера инструментов так же, как остальные.
Гайд основан на нашем повторе 20 старых аварий в ноябре 2026. Это не живое дежурство: примеры правил и кода нужно подогнать под свой стек и проверить на копии.
Тимур Ахимов, @akhimovcom, prtner.ai. Шрифт Medeo, пиктограммы Lucide (ISC).