Что случилось, когда я выпустил несколько ИИ-агентов в один репозиторий
Я думал, что параллельные сессии агентов удвоят производительность. На деле за одну неделю посыпались реальные аварии: угон ветки, коммиты поверх чужой ветки, осиротевшие коммиты, спасённые через reflog, и двойная реализация одной и той же фичи. Типология аварий, которые устраивают параллельные агенты на общем чекауте, и правила, выстроенные на этом опыте.
Главное
Настоящий враг параллельных сессий агентов не способности модели, а общий рабочий каталог. HEAD - это глобальная переменная для всех, поэтому переключение ветки в одной сессии сажает коммит другой сессии не туда. Четыре типа аварий за неделю и истории восстановления, плюс три линии обороны: изоляция через worktree, перепроверка ветки перед коммитом и сужение области проверочного шлюза.
Содержание
Работая с ИИ-агентами, естественным образом входишь во вкус. Пока одна сессия чинит баг, другая пусть рефакторит, а третья расследует иссью, разве нет? Моделей можно запустить сколько угодно, значит, и производительность вырастет во столько же раз.
Я тоже так начал. И через неделю понял: настоящий враг параллельных агентов не способности модели, а рабочий каталог, который они делят.
HEAD - это глобальная переменная
Причина укладывается в одно предложение. Когда один git-чекаут делят несколько сессий, состояние «текущая ветка» становится глобальной переменной для всех.
Представь двух людей, работающих одновременно за одним компьютером, и странность видна сразу; но, запуская агентов, я об этом не подумал. В каждой вкладке терминала висит по сессии, и выглядят они изолированными друг от друга. Но файловая система одна, и HEAD один. В момент, когда одна сессия делает git checkout, почва уходит из-под ног у всех остальных.
У аварий той недели были отчётливые типы.
Угон ветки. Пока сессия A работала в топик-ветке, сессия B переключила ветку под свою задачу. A, не зная об этом, закоммитила, и коммит лёг поверх ветки B. Было и в обратную сторону: в момент, когда A собиралась коммитить, ветка оказалась переключённой на develop, и аварию предотвратил только хук, блокирующий прямые коммиты в защищённые ветки. Не будь хука, коммит прошёл бы как есть.
Осиротевшие коммиты. Топик-ветку сессии A сессия B удалила в ходе уборки. Коммиты A стали сиротами, не принадлежащими ни одной ветке; я перерыл reflog, нашёл хеши коммитов и вернул их через cherry-pick. Повезло, что спас: истеки reflog или не найди я их, работа испарилась бы целиком.
Загрязнение стейджинга. В момент, когда сессия A создавала коммит, в области стейджинга уже лежало удаление файла, застейдженное сессией B. Закоммить она как есть, удаление от B вмешалось бы в коммит A. Я отсёк это, заметив незнакомое изменение при просмотре diff; не глянь агент diff перед коммитом, никто бы и не узнал.
Двойная реализация. Самый обидный тип. Две сессии, не зная о существовании друг друга, реализовали одну и ту же фичу каждая по-своему. Обе сделали добросовестно и хорошо, и одну пришлось выбросить целиком. Время, сэкономленное параллелизацией, вернулось обратно в полном объёме.
Проверочные шлюзы тоже топчут друг друга
Проблема была не только в файлах и ветках. В нашем харнессе есть шлюз, который перед завершением сессии прогоняет статический анализ и тесты по всему репозиторию и не даёт закончить работу в сломанном состоянии. Для одиночной сессии это отличный механизм.
В параллельных сессиях он превратился в капкан, которым сессии хватают друг друга за ноги. Сессия A трогала только документацию, но её шлюз падает из-за ошибки компиляции в файле, который прямо сейчас правит сессия B. A тратит ходы на доказательство «это не моё изменение», а в тяжёлый день полчаса ждала, пока B приберётся. Худший инцидент: одна сессия, чтобы пройти шлюз, взяла и починила код, над которым работала другая. Шлюз сам стал стимулом лезть в чужую работу.
Сама проверка не виновата. Виноватой стала её область «весь репозиторий» - в тот момент, когда репозиторий перестал принадлежать одному.
Три линии обороны
Пережив аварии по типам, я выстроил оборону в три слоя.
- Изоляция worktree по умолчанию. Дай каждой сессии независимый рабочий каталог через
git worktree, и HEAD перестаёт быть общей переменной. Угон ветки, осиротевшие коммиты и загрязнение стейджинга исчезают в корне. Три из четырёх типов аварий выше снимаются одним этим ходом. Правда, он не бесплатный. В монорепо для каждого worktree придётся заново ставить зависимости и запускать кодогенерацию, а в репозитории с инструментами, плохо совместимыми с worktree, вроде git-crypt, изоляцию трудно сделать обязательной. У нас один репозиторий из-за этого ограничения так и живёт на общем чекауте, поэтому и нужны две оставшиеся линии. - Перепроверка ветки прямо перед коммитом. В начале работы сессия записывает «я работаю в этой ветке», а перед самым коммитом сверяет запись с текущим HEAD. Не совпало - коммит останавливается, сначала разбираемся в ситуации. Правило до смешного простое, но все аварии с угоном ветки сводились к одному: «HEAD в момент коммита не тот HEAD, который я знаю». Человек заметил бы имя ветки в приглашении терминала, а агент не проверяет, пока не скажешь явно.
- Скоуп проверочного шлюза сужается до моих изменений. Область, которую проверяет шлюз завершения сессии, сужается с целого репозитория до файлов, которые эта сессия реально изменила. Исчезает и блокировка моего завершения чужим WIP, и стимул трогать чужой код ради прохождения шлюза. Здоровье репозитория в целом всё равно перепроверит CI по закоммиченному состоянию. Сессионному шлюзу смотреть на всё и не требовалось.
Сверху я добавил одно операционное правило. Перед запуском сессии просматриваю открытые ветки и PR: нет ли пересекающегося скоупа. Двойная реализация - это не та проблема, которую способен остановить git, это проблема диспетчеризации, и закрывать её пришлось не инструментом, а привычкой.
Проблемы конкурентности существуют не только в базах данных
Разложив всё по полочкам, узнаёшь знакомую картину: общий ресурс, конкурентный доступ без локов, гонки и уровни изоляции. Та самая проблема, которую мы десятилетиями изучали на базах данных и многопоточном коде, разыгралась заново поверх рабочего каталога.
С одним агентом этой проблемы не существует. Когда человек и агент работают по очереди, человек неявно играет роль координатора. Проблема начинается в тот момент, когда агентов становится много, а человек выпускает координацию из рук. С этой точки рабочий каталог - общий ресурс, которому нужен контроль конкурентности, и если гонять его без изоляции, данные будут теряться так же неизбежно, как терялись в базах данных.
Это не призыв отказаться от параллельных агентов. Я и сейчас каждый день запускаю по несколько сессий. Изменилось одно: я больше не сгоняю их в одну комнату.
Похожие статьи
- 💻 Разработка
Пайплайн, где код, исправленный одним ИИ, опровергает другой ИИ
Попроси модель отревьюить собственный код, и она заново утвердит свои же выводы. Рассказ разработчика-одиночки, который пишет почти весь код вместе с ИИ, о том, как петля перекрёстной проверки, где одному ИИ поручают «опровергнуть» работу другого, закрепилась как постоянный пайплайн. Реальный случай, когда три раунда подряд находились баги, пропущенные само-ревью, и устройство промптов.
- 💻 Разработка
Когда ИИ говорит «готово», нужен механизм, который ему не поверит
ИИ-агенты для написания кода объявляют работу законченной, хотя не закончили её. Говорят, что тесты прошли, не запустив их; говорят, что починили баг, не воспроизведя его. Рассказ разработчика-одиночки, который пишет почти весь код вместе с ИИ, о проверочном шлюзе, заставившем систему признавать готовность только по доказательствам, а не по словам.
- 💻 Разработка
Я перестал верить агенту на слово «готово» - шлюз verify.sh, prove-it
ИИ-агент для кодинга докладывает «тесты проходят», даже не запустив их. Это не враньё, а структурная проблема. Чему я научился, делая prove-it - открытый шлюз, который превращает доклад в проверку, и четыре способа, которыми тихо ломается самописный хук на пять строк.