Почему ваш агент — бомба замедленного действия
Сначала всё мило: вы написали агента, он парсит CSV, делает запросы к LLM и возвращает красивый JSON. Потом другой агент — идёт в продакшен, читает переменные окружения, пишет в ту же папку. Третий агент запускает shell-команды. И вот вы уже не знаете, какой процесс испортил базу данных. Знакомая картина? 90% команд, которые хвастаются "мультиагенткой", на самом деле запускают монолитную кучу с разделением ролей через промпты. И только когда система падает из-за конфликта зависимостей или утечки памяти, приходит осознание: изоляция окружений — не опция, а фундамент.
В этой статье я расскажу, как мы строили архитектуру lifecycle изолированных окружений для оркестрового фреймворка, который держит сотни агентов в production. Без тупого «заверни всё в Docker». С реальными компромиссами, граблями и неочевидными решениями.
Предупреждение: если вы считаете, что изоляция — это просто «разные процессы», эта статья вас отрезвит. Контекст: я пишу на основе опыта, когда агент случайно удалил /tmp на продакшене. Теперь у нас детерминированный kill-switch (подробно описан в статье про kill-switch).
Три слоя изоляции: workspace, runtime, конфиг
Когда мы проектировали наш фреймворк (назовём его OrchAgent, версия 3.2 на июнь 2026), то отталкивались от простой идеи: окружение агента должно быть воспроизводимо уничтожено и создано заново без побочных эффектов. Для этого выделили три сущности:
| Компонент | Описание | Пример |
|---|---|---|
| Workspace | Изолированная файловая система агента: код, данные, артефакты. | /var/orch/agents/{agent_id}/workspace |
| Runtime | Процесс или контейнер, в котором исполняется агент. | Docker-контейнер с Python 3.12 + локальные пакеты |
| Конфиг | Набор параметров: переменные среды, лимиты, политики доступа. | YAML-файл с env, timeouts, allowed APIs |
Workspace — его часто путают с volume в Docker. Но у нас workspace — логическая единица, которая может быть как каталогом на хосте, так и удалённым mount (NFS, S3 через FUSE). Главное — он привязан к жизненному циклу агента. Когда агент уничтожается, workspace может быть либо заархивирован, либо очищен. На старте мы всегда создаём чистый workspace из шаблона. Это как git clone для окружения.
Runtime — здесь классическая дилемма: sandbox vs no-sandbox. Мы перепробовали оба подхода. Вписывание всех runtime в один процесс — путь к катастрофе (один агент сломал всю очередь). Сейчас юзаем изоляцию на уровне контейнеров с ограничениями cgroups v2. Но не через Docker daemon — слишком медленно для сотен агентов. Используем runsc (gVisor) с собственным runtime-менеджером. Этот опыт мы уже описывали в статье про песочницы для deepagents.
Lifecycle: не просто start/stop
Обычный цикл: INIT -> READY -> RUNNING -> STOPPED -> DESTROYED. Но дьявол в деталях. Давайте разложим каждый переход на атомы.
1 Фаза INIT: подготовка workspace
На этом этапе фреймворк берёт манифест окружения — JSON/YAML, где описано:
- базовый образ runtime (python:3.12-slim + специфические пакеты)
- шаблон workspace (список директорий, seed-файлы, конфиги)
- переменные окружения с защитой секретов (vault integration)
- политики: разрешённые сетевые вызовы, лимиты CPU/RAM, timeout
Важно: INIT — единственная точка, где можно безопасно загрузить зависимости. Если агент начнёт качать pip-пакеты в рантайме — вы потеряете контроль над версиями. Мы всегда проверяем хеш всего workspace перед стартом. Это даёт детерминированность, которая спасает при дебаге ошибок.
💡 Хорошая практика: сравнивать снимки workspace до и после выполнения агента. Это позволяет откатывать изменения, если агент сработал некорректно. У нас это встроено в lifecycle как опция snapshot_on_failure.
2 Фаза READY: прогрев runtime
Runtime не обязан стартовать сразу с агентом. Мы используем пул «тёплых» runtime (hot pool) — контейнеры с предзагруженным кодом, но без запущенного агента. Когда приходит задача, фреймворк назначает runtime на агента, копирует workspace (overlayfs, а не полный copy) и запускает процесс.
Это сокращает время старта с 5-10 секунд до 200-300 миллисекунд. Подход borrowed из архитектуры serverless, но адаптирован для долгоживущих агентов. Кстати, в статье про архитектуру без роутинга мы обсуждали похожие оптимизации для уменьшения latency.
3 Фаза RUNNING: здесь живут ошибки
Во время выполнения мониторинг должен ловить не только crash, но и дрейф конфигурации. Агент может изменить переменные среды, подменить файлы в workspace, запустить дочерние процессы. Мы фиксируем каждое изменение через auditd внутри runtime. Если что-то выходит за рамки политики (попытка записи в /etc, открытие неразрешённого порта) — runtime получает сигнал SIGTERM и переходит в QUARANTINED состояние.
Этот механизм похож на kill-switch, но более мягкий — агент не уничтожается, а изолируется. Потом мы можем либо откатить изменения, либо перезапустить с чистым workspace. Подробно про детерминированное завершение агентов я писал в руководстве по kill-switch.
4 STOPPED и DESTROYED: как не оставить мусор
Самая недооценённая часть lifecycle. Когда агент завершил работу, нужно:
- stop runtime: SIGTERM -> timeout -> SIGKILL. Не забудьте про orphan processes!
- snapshot workspace: если требуется сохранить артефакты, архивируем в object storage.
- destroy workspace: удаление каталога, освобождение inode. Наш фреймворк проверяет, не осталось ли файлов вне workspace (например, через bind mount).
- return runtime in pool: очистить overlayfs, сбросить state, вернуть в hot pool.
Мы используем двухфазное уничтожение: сначала агент переводится в SOFT_DELETED (архивация данных), а через TTL (default 24h) — в HARD_DELETED (полный cleanup). Это даёт окно для восстановления, если агент сделал что-то полезное, но потом понадобилось откатить.
Предостережение: никогда не полагайтесь на cleanup в runtime-процессе. Агент может зависнуть, и garbage collector не сработает. У нас был случай, когда агент породил 10 дочерних процессов, и после его убийства они висели как зомби. Теперь каждый runtime запускается внутри отдельного cgroup, и при уничтожении мы принудительно чистим всю иерархию процессов.
Как НЕ надо делать: типичные антипаттерны
Я видел реализации, где lifecycle агента выглядел так: start() -> run() -> stop(). Без workspace, без конфига, просто процесс. И это работало ровно до первого сбоя. Вот три главные ошибки:
- Один workspace на всех агентов. Кажется, что так проще — не надо копировать файлы. Но первый же агент, который случайно удалит shared файл, положит всю систему. Мы перешли на per-agent workspace после того, как один агент стёр общую папку с конфигами (ирония: он пытался почистить временные файлы).
- Игнорирование состояния runtime. Если runtime не умеет сообщать о своём состоянии (READY, HEALTHY, DEGRADED), вы не сможете корректно управлять lifecycle. У нас это привело к тому, что оркестратор пытался отправить задачу агенту, контейнер которого был в dead state, но это не было обнаружено.
- Ленивый cleanup. Когда вы не уничтожаете workspace после завершения агента, диск забивается мусором. В одном проекте мы нашли 500 ГБ stale workspace, которые никто не чистил. Пришлось писать отдельный cron-скрипт, хотя правильное место для cleanup — lifecycle.
Если вы хотите углубиться в управление конфигурациями в мультиагентных системах, рекомендую прочитать архитектуру on-prem AI стека — там обсуждаются похожие проблемы изоляции, но в контексте локальных LLM.
Конфиги: сердце lifecycle
Каждый lifecycle state должен читать конфиг. Но конфиг тоже живёт своей жизнью: он может обновляться, версионироваться, содержать ссылки на секреты. Мы сделали ConfigManager, который подписывается на изменения в etcd и автоматически применяет новую версию манифеста для агента без перезапуска runtime (для параметров окружения, которые не требуют restart).
Пример манифеста (упрощённый YAML):
apiVersion: orchagent.io/v3
kind: AgentEnvironment
metadata:
name: data-processor-agent
spec:
runtimeImage: agent-python:3.12.8
workspaceTemplate: /templates/data-pipeline
resources:
cpu: 500m
memory: 512Mi
ephemeral: 1Gi
policies:
network: [allow-outbound-https]
filesystem:
readOnlyPaths: ["/etc", "/usr"]
writablePaths: ["/workspace/output"]
lifecycle:
onStart: [validate-credentials]
onStop: [upload-artifacts]
onCrash: [snapshot-workspace, notify-admin]
Хитрость: onStart/onStop/crash — это хуки, которые выполняются внутри runtime, но не агентом, а init-процессом (мы используем tini с кастомным entrypoint). Это гарантирует, что даже если агент поломался, хуки отработают. Ведь потеря артефактов после краша — одна из самых болезненных проблем.
Интеграция с оркестром: как lifecycle вписывается в общую картину
Наш оркестровый фреймворк построен на event-driven архитектуре. Каждое изменение состояния агента (INIT -> READY -> RUNNING -> ...) публикует событие в Kafka. Другие сервисы (мониторинг, алертинг, планировщик) подписываются на эти события. Например, когда agent переходит в QUARANTINED, срабатывает триггер, который приостанавливает все задачи в очереди для этого агента.
Такой подход позволяет не блокировать оркестрацию ожиданием состояния. Это особенно важно, когда мы говорим о мультиагентных системах с десятками тысяч экземпляров. В руководстве по Strands Agents похожий паттерн используется для управления очередями.
Кстати, мы заметили, что lifecycle напрямую связан с BPMN-оркестрацией. Каждый state может быть представлен как task в BPMN-диаграмме. Если хотите увидеть, как старый стандарт помогает управлять агентами, почитайте статью про BPMN для AI-агентов.
Метрики и observability: как понять, что lifecycle не врёт
Без метрик вы не отличите вечно висящий в INIT агент от нормально работающего. Мы экспортируем следующие метрики для каждого этапа lifecycle:
- duration_seconds — сколько времени агент провёл в каждом state
- transitions_total — количество переходов между состояниями (с label причины)
- workspace_size_bytes — размер workspace (если слишком быстро растёт — алерт)
- runtime_cpu_usage + memory_usage — потребление ресурсов в RUNNING
- lifecycle_errors_total — ошибки на этапах init, stop, destroy
Однажды мы заметили, что у 30% агентов transition из INIT в READY занимал больше 5 секунд. Оказалось, что шаблоны workspace лежали на медленном NFS. Перешли на локальный SSD с периодической синхронизацией — latency упало до 300 мс. Без метрик мы бы не нашли причину.
Совет: добавьте healthcheck в runtime, который проверяет, что lifecycle-обновления доходят до агента. У нас был баг: из-за race condition агент думал, что он в RUNNING, а оркестратор считал его INIT. Healthcheck на основе heartbeat решил проблему.
Что дальше: мульти-рантайм и динамическая изоляция
Текущая версия нашего фреймворка (3.2) поддерживает изоляцию на уровне контейнеров. Но мы уже работаем над следующей итерацией — динамический lifecycle, когда агент может сам запрашивать дополнительные ресурсы (например, GPU для инференса) в рамках своего окружения, и lifecycle-менеджер адаптирует runtime без остановки. Это потребует пересмотра модели конфигов и добавления подсостояний.
Вдохновение черпаем из опыта с тремя агентами вместо одного — разделение обязанностей позволило локализовать проблемы. Если у вас ещё нет чёткого lifecycle — начните с него. Потому что без изоляции вы рано или поздно будете тушить продакшен по ночам.
И последнее: не пытайтесь скопировать архитектуру 1:1. Каждый фреймворк имеет свои грабли. Начните с малого: workspace, simple runtime, три состояния. Добавляйте complexity по мере роста. И обязательно прочитайте историю про то, как ИИ-агент снёс продакшен — это лучшая мотивация не лениться с lifecycle.