Ночной дежурный: нейросеть следит за сервером, правила будят человека

Мы хотели, чтобы ночью за серверами дежурила нейросеть. Прежде чем доверить ей ночь, прогнали её на 20 своих старых авариях. Вывод: будить человека должны простые правила в коде, а нейросеть подключается после сигнала и объясняет, что случилось. Ниже схема по шагам. Отдайте гайд своему боту, он подберёт её под ваш стек.

Нейросеть смотрела сама

Разбудила человека

2 из 20аварий, где он был нужен
  • на упавшую базу ответила «подождём»
  • 47 запросов к модели обошлись в $0,40
Сначала правило, потом нейросеть

Назвала причину

7 · 13 · 0верно · частично · неверно
  • её действия: 10 помогли бы
  • 8 ничего бы не изменили
  • 2 навредили бы

Это повтор старых аварий: на каждом шаге модель видела только то, что уже произошло к этому моменту. Живой ночи наш дежурный ещё не дежурил, и действия ему мы пока не доверили. Выборка маленькая: на ваших авариях цифры будут другими.

Как отдать гайд боту

Скопируйте этот промпт и вставьте в чат с ботом. Бот сначала уточнит у вас, где работают серверы, какой мониторинг уже есть и кто отвечает ночью, сравнит варианты и предложит план. Делать что-то он начнёт только после вашего решения.

Помоги мне устроить ночное дежурство для серверов моего продукта по гайду «Ночной дежурный» (https://prtner.ai/guides/nochnoy-dezhurny/). Сделай так:1. Прочитай гайд целиком. Его схема: правила в коде будят человека, нейросеть только объясняет после сигнала, видит серверы лишь на чтение, действует через проверку в коде и в пределах дневного потолка.
2. Задай мне вопросы и ничего не делай: где работают мои серверы и база (облако, свой сервер, платформа), какой мониторинг и оповещения уже есть, кто отвечает ночью и как его будить, какие аварии были за последние месяцы и где лежат их записи, сколько я готов тратить на модель в день.
3. Сравни 2–3 варианта под мой случай, например: дописать правила в мой текущий мониторинг, поставить отдельного простого сторожа, взять готовый сервис дежурств. Для каждого назови плюсы, минусы и примерную цену.
4. Предложи план по шагам 1–6 гайда и остановись: дальше только после моего решения.
Не запускай и не предлагай запускать команды, не проси доступов к серверам, пока я не решу. Никогда не проси прислать в чат пароли, ключи, токены или логи с личными данными.
Шаг 1

Будят правила в коде

Решение «будить или нет» принимает код, а не модель. Правило простое: что проверяем, как часто, когда считаем отказом. Отказ подтверждается несколькими проверками подряд, иначе один сбой сети разбудит вас зря.

# 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 минут: звонок или пуш дежурному

Готово, когда вы остановили тестовый сервис и вас разбудило правило, а потом остановили самого сторожа и вас разбудил внешний сервис.

Шаг 2

Нейросеть объясняет после сигнала

Модель подключается, когда правило уже сработало. Ей передают: какое правило, его показания за последние полчаса, недавние выкладки и список инструментов для чтения из шага 3. Её работа: коротко и простыми словами объяснить, что видно.

Оповещение уходит сразу, объяснение догоняет его вторым сообщением. Если модель не ответила или упала, человек всё равно разбужен.

Модель не может отменить сигнал. Даже если она пишет «всё в порядке», решение правила остаётся в силе.

Одно объяснение на одну аварию, а не на каждую проверку.

Что должно быть в сообщении:

Сработало: db_down. База не отвечает 4 минуты, 2 проверки из 2.

Что случилось: основная база не принимает соединения, API отвечает 503.
Вероятная причина: диск базы заполнен (свободно 0,3 %, за час +6 ГБ).
Уверенность: средняя. Логи базы прочитать не удалось: инструмент вернул ошибку.
Что проверить:
  1. Что заняло диск базы.
  2. Не копится ли журнал из-за слота репликации или сломанной архивации.
Чего не делать: не перезапускать базу, пока диск полон.
Предложение: нет. В этой ситуации действует только человек.

В инструкции модели напишите прямо: «Ты не решаешь, будить ли человека. Пиши только то, что видел в данных. Скажи, что прочитать не удалось. Если данных мало, так и напиши».

На этом мы споткнулись. Сначала мы дали модели только показания и попросили решить, будить ли человека. В 18 авариях из 20, где человек был нужен, она решила не будить. Когда правило уже подтвердило отказ, в нашем прогоне неверных причин не было, но верных только 7 из 20, ещё 13 частично.

Готово, когда на тестовом отказе вы получили сигнал сразу, а объяснение после него, и в объяснении есть все пять частей: что, причина, уверенность, что проверить, чего не делать.

Шаг 3

Только чтение, через узкую дверь

У модели нет ключа 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

Почему так строго. В логах бывает чужой текст: сообщения пользователей, заголовки запросов. Кто-то может написать туда команду для модели. Если у модели есть только чтение, обманутый вызов ничего не сломает.

Готово, когда там, где работает модель, нет ни ключей SSH, ни паролей базы, а вызов любого инструмента не из списка получает отказ.

Шаг 4

Действия только через проверку в коде

Модель может предложить действие, но до сервера оно доходит только через проверку. Для каждой ситуации заранее записан список разрешённых действий и условия. Проверка берёт факты сама, словам модели о состоянии сервера она не верит.

# 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())

Каждое действие включается только после учений. На копии сервиса вы ломаете его нарочно и смотрите две вещи: действие помогает, а в запрещённой ситуации проверка его не пропускает. До учений действие остаётся предложением, которое выполняет человек.

На этом мы споткнулись. Из 20 предложенных действий 10 помогли бы, 8 ничего бы не изменили, 2 навредили бы. Оба вредных были автоматическим перезапуском там, где наши правила его запрещают. Поэтому сейчас наш дежурный только читает и объясняет.

Готово, когда на учениях проверка пропускает действие в разрешённой ситуации и отказывает в каждой запрещённой.

Шаг 5

Жёсткий потолок на день

Ошибка в цикле может отправить к модели тысячи запросов. Потолок ставится в коде и отдельно у провайдера модели.

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.

Готово, когда вы поставили потолок ниже текущего расхода, вызвали тестовый отказ и получили сигнал с пометкой про лимит.

Шаг 6

Прогоните свои прошлые аварии

Прежде чем доверить модели ночь, проверьте её на своих авариях. Соберите 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).