Продвинутый

Агент-управляемый экспонат музея на Rust

Алексей Кузнецов
Алексей Кузнецов
Системный администратор21 октября 2026 г.13 мин чтения

Экспонат музея, разработанный и поддерживаемый ИИ-агентом с использованием Rust и Gajae-Code / LazyCodex без участия человека.

Агент-управляемый экспонат музея на Rust

Экспонат музея, разработанный и поддерживаемый ИИ-агентом с использованием Rust и Gajae-Code / LazyCodex без участия человека.

Архитектура экспоната без человеческого вмешательства

Роль ИИ-агента в разработке и поддержке

Самое утомительное в поддержке экспонатов — это когда каждая правка требует «сесть за клавиатуру, вспомнить, как это собрано, поправить, протестировать». ИИ-агент решает это иначе: он не просто пишет код, а становится постоянным хранителем знаний о системе.

Мне довелось запустить подобный эксперимент на выставке «Цифровая эволюция» в музее истории IT. Экспонат — интерактивный дисплей с датчиками движения, который реагирует на приближение посетителя. Раньше за ним присматривал один человек. Теперь агент:

  • Анализирует логи в реальном времени — если датчики перестают отвечать, агент сам диагностирует проблему.
  • Вносит правки в конфигурацию — без перезагрузки.
  • Обновляет прошивку контроллеров — по расписанию или по тревоге.

Пример: однажды в 3 часа ночи датчики на экспонате «Пятачок» (да-да, я так именую свои устройства) перестали передавать данные. Агент не стал ждать утреннего звонка. Он:

  1. Проверил состояние systemd-юнита:
bash
systemctl status pygmy-sensor@piglet.service
  1. Увидел, что процесс падает с сегфолтом.
  2. Автоматически откатился на предыдущую версию прошивки, используя резервную копию в /opt/firmware/backup/.
  3. Запустил повторную проверку через 10 минут — всё заработало.

Человек узнал об этом только из отчёта агента на следующее утро.

Агент работает на базе Gajae-Code — фреймворке для ИИ-агентов в инфраструктуре. Он не «генерирует» код случайно: каждое действие логируется, верифицируется и подтверждается тестами. Если агент предлагает изменение в конфиг NGINX, он сначала проверяет синтаксис:

bash
nginx -t

И только после этого применяет правку.

LazyCodex здесь играет роль «памяти». Он хранит историю всех изменений, зависимостей и состояний. Когда агент решает, что нужно обновить библиотеку, LazyCodex подсказывает: «Эта версия несовместима с ядром 5.10, последний раз такое ломало экспонат в «Сове»».

Это не про замену человека. Это про предсказуемость. Когда вы знаете, что система сама поймёт, что сломалось, и попытается починить — вы спите спокойно.

Использование языка Rust для надёжности

Rust здесь не для красоты. Он здесь, потому что экспонат работает 12 часов в день, 7 дней недели, и любой сбой — это не «приложение упало», а «посетитель стоит и смотрит на чёрный экран».

Вот почему я выбрал Rust для логики экспоната:

  • Нулевые runtime-паники. Если в C++ вы забыли проверить указатель, система упадёт. В Rust компилятор не даст собрать бинарник с dangling pointer.
  • Нулевые затраты на абстракции. Агент может писать высокоуровневый код, а Rust скомпилирует его в эффективный машинный код без накладных расходов.
  • Предсказуемое поведение. Никакого GC, никаких сборщиков мусора в критических секциях.

Пример простого обработчика события от датчика:

rust
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;

struct ExhibitState {
    visitor_nearby: bool,
    last_interaction: u64,
}

fn handle_sensor_event(state: Arc<Mutex<ExhibitState>>, event: SensorEvent) {
    let mut s = state.lock().unwrap();
    match event {
        SensorEvent::MotionDetected => {
            s.visitor_nearby = true;
            s.last_interaction = current_timestamp();
            trigger_display();
        }
        SensorEvent::Timeout => {
            if !s.visitor_nearby {
                return;
            }
            s.visitor_nearby = false;
            trigger_sleep_mode();
        }
    }
}

Агент не пишет это с нуля. Он использует Gajae-Code для генерации шаблонов и LazyCodex для проверки логики на соответствие требованиям безопасности. Например, перед тем как добавить новый обработчик, агент проверяет:

  • Нет ли data race на state.
  • Все пути выхода из функции корректно освобождают lock.
  • Нет ли вызовов, которые могут заблокировать поток надолго.

Если что-то не так — агент не компилирует. Он возвращается к черновику, исправляет и пробует снова.

Вот конфиг systemd-юнита для экспоната:

ini
[Unit]
Description=Exhibit Agent (Rust)
After=network.target

[Service]
Type=simple
User=exhibit
ExecStart=/opt/exhibit/bin/exhibit-agent --config /etc/exhibit/config.toml
Restart=always
RestartSec=5
WatchdogSec=30
NotifyAccess=all

[Install]
WantedBy=multi-user.target

WatchdogSec=30 — если процесс не посылает sd_notify("WATCHDOG=1") в течение 30 секунд, systemd перезапускает его. Это не «надёжность», это «гарантированное поведение».

Когда я вижу, что экспонат на Rust работает три месяца без единого падения — я понимаю, что выбор языка был не зря.

Интеграция Gajae-Code и LazyCodex в систему

Применение Gajae-Code для генерации интерфейсов

Gajae-Code в моей практике — это, скажем так, шаблонизатор с душой. Да, он похож на Jinja или Tera, но вырос из задачи не просто рендерить HTML, а генерировать интерфейсы целиком: конфиги под systemd, схемы для Grafana, даже пачки YAML-манифестов для k3s.

Вот типичный шаблон, который я использую для генерации юнитов systemd на лету:

jinja
# {{ service_name }}.service
[Unit]
Description={{ description }}
After=network.target

[Service]
ExecStart={{ exec_start }}
Restart={{ restart_policy | default("always") }}
User={{ user | default("root") }}
EnvironmentFile=/etc/{{ service_name }}/env

[Install]
WantedBy=multi-user.target

А вот как я вызываю Gajae-Code из агента на Rust:

rust
use gajae_code::Template;

let template = Template::from_file("templates/systemd-unit.j2")?;
let rendered = template.render(serde_json::json!({
    "service_name": "museum-display",
    "description": "Интерактивный экспонат управления",
    "exec_start": "/opt/museum/display-agent",
    "restart_policy": "on-failure",
    "user": "museum"
}))?;

Самое удобное — это то, что Gajae-Code умеет хотя бы на уровне документации, а значит, не надо выдумывать синтаксис. Шаблоны читаются как обычный код, а переменные подставляются предсказуемо.

Оптимизация с помощью LazyCodex

LazyCodex спасает, когда интерфейсов становится много. Особенно это заметно в музее: каждый экспонат — это отдельный сервис, и если у вас 50 дисплеев, то генерировать конфиги для всех сразу — пустая трата времени.

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

Пример конфигурации для LazyCodex:

yaml
components:
  display-agent:
    type: binary
    source: ./src/display-agent
    interfaces:
      - name: rest-api
        port: 8080
        template: api-server

  grafana-dashboard:
    type: config
    source: ./dashboards/exhibit.json
    depends_on:
      - display-agent

  systemd-unit:
    type: config
    template: templates/systemd-unit.j2
    depends_on:
      - display-agent

Интеграция с Rust выглядит так:

rust
use lazycodex::{Project, Component};

let project = Project::load("lazycodex.yaml")?;
let changed = project.changed_components()?;

for component in changed {
    match component.name.as_str() {
        "display-agent" => {
            // Пересобираем бинарник
            std::process::Command::new("cargo")
                .args(["build", "--release"])
                .status()?;
        },
        "systemd-unit" => {
            // Генерируем юнит через Gajae-Code
            generate_systemd_unit(&component)?;
        },
        _ => {}
    }
}

С практической точки зрения, LazyCodex устраняет одну боль: вы редактируете код одного экспоната, а система сама понимает, что нужно перезапустить только его, а не все 50 дисплеев. Это не просто удобно — это предсказуемость конфигураций в действии.

Если мы посмотрим на архитектуру целиком, то Gajae-Code отвечает за как генерировать, а LazyCodex — за когда. Вместе они дают то, что раньше требовало целого CI/CD-пайплайна.

Практические аспекты развёртывания агент-управляемого экспоната

Настройка окружения для беспрерывной работы

Сервер, на котором будет работать экспонат, не должен требовать ручного вмешательства. В моей лаборатории это Raspberry Pi 4, но схема та же для любого Linux-устройства. Главное — чтобы система сама восстанавливалась после сбоев питания и перезагружалась без вопросов.

Сначала включаю автологин для пользователя, под которым запускается агент. Это удобно, когда нужно быстро залезть по SSH и посмотреть, что происходит:

bash
# /etc/systemd/system/getty@tty1.service.d/override.conf
[Service]
ExecStart=
ExecStart=-/sbin/agetty --autologin alexey --noclear %I $TERM

Перезапускаю сервис:

bash
sudo systemctl daemon-reload
sudo systemctl restart getty@tty1

Далее настраиваю автозагрузку самого агента через systemd. Это не просто «запустить в фоне» — это управление процессом с перезапуском при падении:

ini
# /etc/systemd/system/museum-exhibit.service
[Unit]
Description=Museum Exhibit Agent
After=network.target

[Service]
Type=simple
User=alexey
WorkingDirectory=/opt/museum-exhibit
ExecStart=/opt/museum-exhibit/exhibit-agent
Restart=always
RestartSec=5
Environment=RUST_LOG=info
Environment=EXHIBIT_CONFIG=/opt/museum-exhibit/config.toml

[Install]
WantedBy=multi-user.target

Активирую:

bash
sudo systemctl enable museum-exhibit.service
sudo systemctl start museum-exhibit.service

Проверяю статус:

bash
sudo systemctl status museum-exhibit.service

Если агент падает — systemd поднимет его через пять секунд. Это лучше, чем крон с while true, который я видел в некоторых проектах. Systemd знает, как правильно перезапускать процессы, а не просто крутить их в кольце.

Также настраиваю journald для ротации логов. По умолчанию логи systemd растут до тех пор, пока не заполнит диск. Особенно это критично для SD-карт в Raspberry Pi:

bash
# /etc/systemd/journald.conf
[Journal]
SystemMaxUse=100M
SystemMaxFileSize=10M
MaxRetentionSec=7day

Перезапускаю journald:

bash
sudo systemctl restart systemd-journald

Мониторинг и логирование без вмешательства

Экспонат работает сам. Но мне всё равно нужно знать, что с ним происходит. Я использую Loki + Promtail для сбора логов и Prometheus для метрик. Всё это работает на отдельном сервере в моей сети — экспонату не нужно знать, куда отправлять данные.

Сначала настраиваю Promtail на стороне экспоната. Он читает логи journald и отправляет их на центральный Loki:

yaml
# /etc/promtail/config.yml
server:
  http_listen_port: 9080

positions:
  filename: /tmp/positions.yaml

clients:
  - url: http://192.168.1.10:3100/loki/api/v1/push

scrape_configs:
  - job_name: system
    journal:
      max_age: 12h
      path: /run/log/journal
      labels:
        job: museum-exhibit
    relabel_configs:
      - source_labels: [__journal__systemd_unit]
        target_label: unit

Запускаю Promtail через systemd:

ini
# /etc/systemd/system/promtail.service
[Unit]
Description=Promtail
After=network.target

[Service]
Type=simple
User=promtail
ExecStart=/usr/bin/promtail -config.file=/etc/promtail/config.yml
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

Активирую:

bash
sudo systemctl enable promtail.service
sudo systemctl start promtail.service

Для метрик настраиваю экспортёр, который будет отдавать данные в Prometheus. Агент на Rust может сам экспортировать метрики через HTTP-эндпоинт. Я использую metrics-exporter-prometheus в коде агента:

toml
# Cargo.toml
[dependencies]
metrics = "0.22"
metrics-exporter-prometheus = "0.16"

В коде агента:

rust
use metrics_exporter_prometheus::PrometheusBuilder;

fn main() {
    let builder = PrometheusBuilder::new()
        .with_http_listener("0.0.0.0:9100")
        .install()
        .unwrap();

    // ... остальной код агента
}

Теперь на экспонате доступен эндпоинт http://exhibit.local:9100/metrics, откуда Prometheus может забирать данные.

Настраиваю Prometheus на центральном сервере:

yaml
# /etc/prometheus/prometheus.yml
scrape_configs:
  - job_name: "museum-exhibit"
    static_configs:
      - targets: ["exhibit.local:9100"]
    scrape_interval: 30s

Перезапускаю Prometheus:

bash
sudo systemctl restart prometheus

Теперь можно строить дашборды в Grafana. Например, мониторинг количества запросов к экспонату за минуту или времени работы агента без перезапусков.

Главное здесь — предсказуемость. Все компоненты работают по стандартным протоколам, их легко заменить или перенести. Если Loki перестанет работать — Promtail будет буферизовать логи локально. Если Prometheus упадёт — метрики просто не будут собираться, но экспонат продолжит работать.

И ни одного «надейся, что сработает» — только проверенные решения с понятным поведением при сбоях.

Уроки, извлечённые из опыта создания автономного экспоната

Почему Rust подходит для подобных задач

Спустя пять лет работы с Docker и Kubernetes на продакшене я так часто слышу: «ну просто напиши на Rust», что уже почти привык. Но когда речь зашла об экспонате музея — действительно, Rust оказался не просто удобным, а почти единственным разумным выбором.

Во-первых, нулевые затраты на runtime. Моя «Сова» (да, именно так я назвал Raspberry Pi, которая держит экспонат) работает на батарейке и солнечной панели. Если бы я писал на Python или Node.js, половина ресурсов ушла бы на интерпретатор, а остальное — на сборщик мусора, который может решить «зачистить» память в самый неподходящий момент. С Rust я собираю бинарник размером 2,3 МБ, который съедает 8 МБ ОЗУ в покое.

Во-вторых, borrow checker спас меня от целой категории багов, о которых я даже не задумывался. Вот, к примеру, фрагмент кода, который управляет дисплеем:

rust
fn update_display(&mut self, frame: &FrameBuffer) -> Result<()> {
    let display_handle = self.display.borrow_mut()?;
    display_handle.draw(frame)?;
    Ok(())
}

Компилятор не позволил мне случайно захватить display дважды — и это на этапе компиляции, а не через три дня после деплоя, когда кто-нибудь постучит в дверь с жалобой, что экспонат завис.

В-третьих, экосистема. embedded-graphics для дисплеев, rppal для GPIO — всё это работает на bare metal, без ОС, если нужно. А если ОС всё же нужна (как в моём случае с Raspberry Pi), std даёт доступ к файловой системе, сети и процессам без лишних абстракций.

С практической точки зрения, если мы посмотрим на альтернативы: C++ требует ручного управления памятью (и я устал от этого в 2015-м), Go всё ещё имеет GC, а Zig… ну, Zig молодой, и я не доверяю стеку из трёх человек.

Как ИИ-агенты меняют подход к разработке

Обычно я пишу код, тестирую его, деплою. ИИ-агент тоже пишет код, но он не «обычно» — он постоянно. Мой агент (на базе LazyCodex, кстати, отличная вещь) не просто генерирует коммиты. Он анализирует логи, ловит аномалии, пишет патчи, тестирует их в эмуляторе и деплоит по расписанию.

Вот как это выглядит на практике. Агент заметил, что экспонат «Сова» каждую ночь в 03:17 теряет соединение с MQTT-брокером. Он не стал просто рестартовать сервис — он проанализировал логи, нашёл, что в это время запускается фоновый процесс резервного копирования, который временно переполняет сокет-буфер. Результат — пул реконнектов с экспоненциальной задержкой:

rust
async fn resilient_mqtt_connect(
    config: &MqttConfig,
    max_retries: u32,
) -> Result<MqttClient> {
    let mut delay = Duration::from_secs(1);
    for attempt in 0..max_retries {
        match MqttClient::connect(config).await {
            Ok(client) => return Ok(client),
            Err(e) => {
                warn!("MQTT connect attempt {} failed: {}", attempt + 1, e);
                tokio::time::sleep(delay).await;
                delay = (delay * 2).min(Duration::from_secs(60));
            }
        }
    }
    Err(anyhow!("Failed to connect after {} attempts", max_retries))
}

Агент также сам пишет тесты. Вот фрагмент, который он добавил в CI после того, как заметил, что экспонат зависает при низком уровне батареи:

rust
#[tokio::test]
async fn test_low_battery_shutdown() {
    let mut mock_power = MockPowerModule::new();
    mock_power.set_battery_level(5); // 5%
    
    let result = handle_power_event(&mock_power).await;
    
    assert!(result.should_shutdown());
    assert_eq!(mock_power.last_action(), PowerAction::GracefulShutdown);
}

С практической точки зрения, ИИ-агенты меняют разработку не столько тем, что пишут быстрее, сколько тем, что не устают. Они работают 24/7, ловят баги в 3 часа ночи, когда человек уже давно спит. И если вы думаете, что это про «автоматизацию» — нет, это про надёжность. Мои серверы не падают годами не потому что я волшебник, а потому что за ними ухаживает система, которая не зевает.

Часто задаваемые вопросы

Как ИИ-агент диагностирует сбои в работе датчиков без участия человека?

Агент использует встроенный цикл наблюдения‑анализа‑действия: он сравнивает текущие метрики с историческими, проверяет журналы systemd и ловит аномалии в поведении процессов. Если датчики перестают отвечать, агент сначала пытается перезапустить сервис, а при повторном сбое — откатывается на последнюю стабильную прошивку из резервной копии. Все действия логируются в Loki с метками, чтобы человеческому оператору было удобно отследить цепочку событий позже.

Можно ли доверять автоматическому обновлению прошивки, или всё же нужен человек для подтверждения?

Обновление прошивки происходит строго по расписанию или по тревоге, но каждое изменение проходит три стадии: проверка подписи, сухой прогон в эмуляторе и постепенный rollout на одну ноду. Если тесты падают, агент откатывается и отправляет уведомление в Alertmanager. Человек вовязывается только если срабатывает сторожевой таймер — например, если за 30 минут не удалось восстановить работу.

Что делать, если ИИ-агент сам начал вносить изменения, которые кажутся странными?

Все правки, которые вносит агент, проходят код‑ревью не в реальном времени, а через механизм «пост‑фактум»: изменения попадают в Git‑репозиторий с пометкой agent-commit, и человеческий коллега может их пересмотреть. Если что‑то выглядит подозрительно, достаточно откатить коммит и временно перевести агента в режим наблюдения. Главное правило: агент не удаляет конфиги без резервной копии.

Как обеспечить предсказуемость поведения агента, если он сам пишет и меняет код?

Поведение агента ограничено жёстким набором эвристик и тестов: перед применением любого патча он проверяет синтаксис (nginx -t, rustc --edition 2021), запускает юнит‑тесты и сверяется с политиками безопасности из OPA. Кроме того, LazyCodex хранит полную историю изменений, поэтому любой откат возможен в один клик. Таким образом, «свобода» агента существует только внутри чётко заданных рамок.

А если вдруг перестанет работать сам агент — кто тогда будет поддерживать экспонат?

Агент работает как systemd‑сервис с автоматической перезагрузкой и мониторингом через Prometheus. Если процесс падает трижды подряд, система переводит экспонат в «ручной режим» и отправляет критический алерт. В этот момент человек получает доступ к полной истории действий агента, последним логам и резервным конфигам, что позволяет быстро восстановить работу без потери данных.

Поделиться:TelegramX / TwitterVK