2026-09-10dev/agent/plan-mode.mdplan-mode.md

Сначала запретить, потом планировать

Серия «Как устроен мой агент»часть 5 из 9

≈ 7 минут6 разделов

Содержание, 6 разделов
  1. Зачем вообще разделять
  2. Что значит «только читать»
  3. План это файл
  4. Когда спрашивать
  5. Кто решает, что план готов
  6. Что из этого следует

В режиме планирования мой агент не имеет права менять код. Он читает, ищет, думает и пишет план в файл. Когда я делал этот режим, мне казалось, что главная работа это объяснить модели, что такое хороший план. Оказалось, главная работа это сделать так, чтобы «не имеет права» было правдой.

Зачем вообще разделять

Цикл «рассуждение, действие, наблюдение» не различает «понять задачу» и «делать задачу». Модель начинает править файлы на втором обороте, потому что правка это тоже действие, и ничто её не останавливает. На маленьких задачах это нормально. На задаче, которая трогает десять файлов, это заканчивается тем, что половина изменений сделана до того, как агент понял, как устроена вторая половина.

Идея разделить фазы старше агентов. В мае 2023 года Ван с соавторами показали, что модель, которую попросили сначала составить план из подзадач и потом выполнять его, рассуждает лучше. Метод назвали Plan-and-Solve, он на десяти наборах обошёл обычный «думай по шагам», а на математических задачах сравнялся с промптом на восемь примеров. Разбор ошибок в той работе показателен: у пошагового рассуждения без плана типичная беда это пропущенные шаги. Модель не забывает, как делать. Она забывает, что ещё надо сделать.

В том же месяце Сюй с соавторами предложили ReWOO: отделить рассуждение от наблюдений полностью. Модель сначала пишет весь план с заглушками для результатов инструментов, потом инструменты выполняются, потом модель сводит ответ. Аргумент был про экономику. В чередующемся цикле каждый вызов инструмента заставляет заново отправлять весь накопленный контекст, и авторы получили пятикратную экономию токенов на HotpotQA при росте точности на 4%.

Целиком по пути ReWOO я не пошёл: агент, который строит план вслепую, для реального репозитория непригоден, он не знает, что там лежит. Взял оттуда главное. Планирование и исполнение это разные режимы с разной экономикой, и переключение между ними должно быть явным.

Что значит «только читать»

Режим планирования включается командой, и с этого момента набор инструментов агента меняется. Запись и правка работают только для файла плана, любого .md прямо в каталоге планов сессии, без подкаталогов. Всё остальное, исходники, конфиги, что угодно, отклоняется. ssh исчезает из списка инструментов целиком. Инструменты внешних MCP-серверов остаются для исследования, с инструкцией не трогать внешнее состояние. И bash становится инспекционным.

Вот здесь начинается интересное. «Инспекционный bash» звучит просто: разреши ls, cat, grep, git log, запрети остальное. Первая версия так и работала, и первая же проверка показала, что список разрешённых команд не защищает ни от чего.

Командная строка это язык, и в ней есть конструкции, которые выполняют произвольный код, не будучи командой. Подстановка $(...) внутри аргумента. Перенаправление > в файл. Heredoc. Префикс переменной окружения: LD_PRELOAD=/tmp/evil.so cat file проходит как чтение файла и загружает библиотеку, PAGER='sh -c ...' git log проходит как просмотр истории и запускает оболочку. Сами разрешённые программы тоже умеют запускать других. У rg есть --pre, который вызывает указанную команду для каждого найденного файла. У git есть --ext-diff, --textconv, -O и --exec-path, и каждый даёт способ выполнить чужой код через легальную read-only подкоманду.

Поэтому проверка сейчас устроена иначе. Команда разбирается настоящим парсером bash через tree-sitter, синтаксическим деревом вместо регулярок. В дереве запрещены подстановки команд и процессов, heredoc, любые файловые перенаправления кроме перенаправления дескрипторов и /dev/null. Из префиксов окружения разрешены только локаль и часовой пояс. Каждая команда в конвейере должна быть в списке read-only программ, у git дополнительно проверяется подкоманда и запрещены флаги, запускающие внешние драйверы. Ни один из этих пунктов я не предусмотрел заранее. Каждый это закрытая дыра. Две нашёл сам, одну нашёл уже после того, как считал список полным.

Урок тот же, что в «Инструкции по недоверию»: инструкция «не меняй код» это пожелание. Граница это парсер, который отклоняет команду до её запуска.

План это файл

Второе решение, которое я считаю правильным: план живёт на диске, в .cast/plans/<сессия>/<имя>.md, и пишется теми же write и edit, что и обычный код.

Сначала было иначе. У плана были свои инструменты, plan_write и plan_edit, со своим редактором секций по заголовкам. Я их убрал, потому что модель, приученная описанием обычных инструментов предпочитать точечную правку полной перезаписи, для плана раз за разом выбирала полную перезапись. Отдельный инструмент это отдельная калибровка, и она оказалась хуже. Общие инструменты дают модели тот же рефлекс, который у неё уже работает на реальных файлах, а режим планирования просто вырезает для них одно разрешённое место.

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

Во-вторых, файл переживает сессию. Можно закрыть терминал, вернуться завтра и продолжить с того же плана. Можно открыть его в редакторе, поправить руками, положить в git. В-третьих, у плана есть структура, которую читает машина: каждый шаг это строка - [ ], и в режиме исполнения эти строки проецируются в список задач агента. Когда шаг выполнен и проверен, галочка ставится в файле. Когда все галочки стоят, план перестаёт управлять работой и остаётся лежать для справки.

И требование к содержанию, которое я сформулировал жёстче всего остального: план это спецификация исполнения. Проектный документ с обзором вариантов планом не считается. Планка такая: исполнитель, который никогда не видел этого разговора, свежий агент после компакции или сам пользователь, проходит файл сверху вниз и не принимает ни одного содержательного решения. Каждый выбор уже сделан. Документ с разделами «альтернативы» и «риски», в котором осталось одно открытое решение, это проваленный план. Разделов без решений, Non-Goals, Alternatives Considered, Risks, в плане быть не должно. Ссылок на «как мы обсуждали» тоже: у читателя этого разговора нет.

Когда спрашивать

Есть место, где планирование неизбежно упирается в человека: предпочтение или компромисс, на который файлы не отвечают. Для этого у агента есть инструмент вопроса. Он показывает два-четыре конкретных варианта с рекомендацией по умолчанию, открывает выбор пользователю и заканчивает ход.

Первая формулировка правила была: «спрашивай, когда файлы не могут ответить». На тестовом сценарии агент нашёл существующий план, понял, что надо добавить шаг, и вместо этого потратил ход на вопрос, кто должен владеть последующим созвоном. Формально файлы на это не отвечали. Фактически ответ ничего не менял в шагах плана.

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

Кто решает, что план готов

Когда план сошёлся, агент вызывает инструмент plan_done, и это открывает пользователю диалог: продолжить уточнять, реализовать в текущем контексте или реализовать в чистом контексте, сохранив видимую переписку. Переключить режим сам агент не может.

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

Отдельная возможность, которая выросла из экономики: у фазы планирования может быть своя модель. Планирование это много чтения и мало письма, исполнение наоборот. Дорогая рассуждающая модель для плана и быстрая дешёвая для исполнения по готовому чеклисту это разумное сочетание, и оно возможно только потому, что фазы разделены явно.

Что из этого следует

Режим планирования оказался не про планы. Он про границу: три четверти работы ушло на то, чтобы «только читать» означало только читать: парсер командной строки, список подкоманд git, запрет префиксов окружения, исчезновение ssh. Сам план при этом стал проще: обычный markdown-файл, обычные инструменты записи, чеклист, который читается и человеком, и кодом.

Разделение фаз, о котором писали в 2023-м, в агенте с реальными инструментами принимает форму режима с другими правами. Другого промпта для этого мало. Модель может думать что угодно. Что она может сделать, определяет режим.