Как я пишу задачи для автономных AI-агентов (когда человека нет в контуре)
Когда я пишу задачу для агента, который будет запускаться по cron в 3 ночи — я думаю о формулировках иначе.
«Проверь inbox и сгенерируй темы для блога» — это не задача. Это начало разговора. Агент уйдёт в неопределённость: что именно проверять, сколько тем, что делать если файлов 200, как решить конфликт источников. В интерактивном режиме я бы ответил. Но в 3 ночи меня нет.
За последние полтора года я написал 13 таких задач и запустил их в production. Каждая из них должна работать без уточняющих вопросов, без петли обратной связи, без моего участия. После нескольких итераций сформировался формат, который перестал удивлять.
Промпт и task-спек — разные вещи
Промпт рассчитан на диалог. Ты пишешь, модель отвечает, ты уточняешь. Всё нормально, это как чат с умным коллегой.
Task-спек — это другой жанр. Ближе к deploy-скрипту, чем к чату. Deploy-скрипт не спрашивает «а что делать если директория уже существует?» — он либо обрабатывает этот случай явно, либо падает с понятной ошибкой. Хороший task-спек работает так же.
Это меняет то, как ты его пишешь. Меньше «объясни ситуацию», больше «если X — сделай Y, если Z — выйди с кодом 0».
Структура, которая работает без вопросов
Сейчас каждая моя задача содержит несколько обязательных секций.
Первое — контекст окружения: абсолютные пути, рабочая директория, первая bash-команда. Звучит банально, но агент, запущенный через launchd, стартует не из project root. Без явного cd первая команда идёт не туда.
Второе — цель с явным anti-scope. Что делается и что не делается — списком. Без anti-scope агент расширяет задачу по своему усмотрению. Это не злой умысел, модель просто пытается помочь. Но я не просил.
Третье — детерминированные шаги. Step 1, Step 2, Step 3 с явными ожидаемыми результатами. Не «обработай файлы», а «прочитай файл X, извлеки поле Y, запиши в Z». Чем конкретнее — тем меньше пространства для интерпретации.
Четвёртое — exit conditions. Это часть, которую я сначала пропускал. Платил за это.
Exit conditions — то, что чаще всего забывают
Агент без явных стоп-правил — это процесс без автомата защиты. Не опасно в тривиальных случаях. Рискованно, когда условия выполнения нестандартные.
Первый раз, когда я запустил задачу генерации тем без ограничения на количество файлов в inbox, она прошлась по всему архиву — 73 файла вместо ожидаемых пяти новых. Задача формально была выполнена. Результат меня не устраивал.
Сейчас типичный exit condition выглядит так: если pending-слотов нет — залогируй no_pending_slots, выйди с exit 0. Или: если inbox содержит больше 50 файлов — пропусти прогон, запиши в лог warning. Это простые условия, но они экономят время на отладке.
Важная деталь: не рассчитывай на «здравый смысл» модели. Языковая модель не обладает чувством предела. Она продолжает, пока задача не сформулирована как завершённая. Твоя работа — сформулировать.
Список разрешений как контракт
В каждой задаче я теперь пишу явный раздел: что агент может делать, а что — нет.
Может: читать файлы, писать файлы в рабочую директорию, запускать bash-команды из белого списка, делать HTTP-запросы к конкретным эндпоинтам.
Не должен: делать git push, удалять production-данные, отправлять сообщения в каналы, не указанные в задаче, изменять конфигурационные файлы без явного разрешения.
Первый раз, когда я не прописал явный запрет на git commit, агент сделал commit — потому что задача включала редактирование файлов, а commit казался логичным следующим шагом. С его точки зрения. Я не просил.
Принцип наименьших привилегий работает здесь так же, как в системном администрировании. Давай ровно столько доступа, сколько нужно для задачи.
Логирование — обязательная часть контракта
Агент, который не оставляет следов, невозможно отлаживать.
Каждая моя задача пишет результат в JSONL-файл: timestamp, название задачи, статус, ключевые идентификаторы обработанных объектов. Один файл на задачу, по одной записи на прогон. Если прогон упал — в логе есть запись с status: failed и описанием шага, на котором остановился.
Когда задача падает в 3 ночи, утром я открываю лог и вижу картину за 30 секунд. Без лога — это работа детектива: git history, bash history, изменения файлов по timestamp.
О том, как именно я организую логирование и мониторинг автономных агентов, писал отдельно — в статье про мониторинг AI-агентов в production.
Как задача эволюционирует после первого запуска
Хорошая задача редко получается с первого раза. Не потому что я плохо думаю наперёд — а потому что реальные данные всегда богаче, чем мысленный эксперимент.
После первого запуска я смотрю лог и нахожу три типа проблем: edge case, который я не предусмотрел («что если файл уже существует?»), permissions, которые оказались нужны («задача пытается записать в директорию, которой нет»), слабые exit conditions («задача прошла до конца, хотя должна была остановиться раньше»).
Потом правлю спек. Иногда — один раз. Иногда — три-четыре итерации, пока задача не перестаёт удивлять.
Я отношусь к task-спекам как к коду: они живут в git, меняются по PR, и у меня есть история изменений. Это дисциплина, которая окупается, когда что-то идёт не так — и ты понимаешь, что именно менял и когда.
Связанная тема, если интересно глубже: я раньше писал про разницу между cron-задачами и AI-агентами — когда что выбирать, и почему «скучное решение» часто надёжнее.
Что это меняет на практике
Автономный агент — это не «более умный cron». Это инструмент, который интерпретирует задачу. А интерпретация — это зона неопределённости, если задача написана неточно.
Когда я перешёл от «напиши хороший промпт» к «напиши детерминированный контракт» — количество сюрпризов в production сократилось. Не исчезло: модель всё ещё может принять неожиданное решение в нештатной ситуации. Но нештатных ситуаций стало меньше, потому что большинство из них теперь явно обработаны в спеке.
Task-спек — это документ, который работает вместо тебя. Пиши его как документацию к критическому сервису. Потому что это и есть критический сервис — просто без пользовательского интерфейса.