У нас в работе несколько проектов, где код каждый день пишут агенты на разных моделях, а люди ставят задачи, принимают результат и отвечают за то, что уезжает на сервер. За это время набралось достаточно граблей, чтобы записать правила. Ниже то, что у нас реально работает, и случаи, из которых эти правила выросли.
Сразу оговоримся: это не рассказ о том, какая модель лучше пишет код. Модели меняются каждые пару месяцев. Порядок вокруг них меняется гораздо медленнее, и именно он решает, будет ли от агентов польза или будет поток правдоподобных изменений, которые никто толком не проверял.
У каждой задачи своя рабочая копия
Несколько агентов в одной папке с кодом мешают друг другу так же, как два человека за одной клавиатурой. Один переключил ветку, у второго посреди работы поменялись файлы. Поэтому каждая задача получает отдельное рабочее дерево через git worktree: общий репозиторий, но своя папка и своя ветка. Агент работает только в ней.
Чтобы не ставить зависимости в каждое дерево заново, мы сначала делали node_modules ссылкой на соседнее дерево. Это сэкономило место и однажды очень неприятно выстрелило. При уборке диска удалили одно старое дерево, а на него ссылались ещё три. Тесты в этих трёх деревьях после этого запускались, находили ноль тестов и завершались успешно. Зелёный результат, ни одной ошибки, ни одной проверки. Заметили случайно.
Выводов два. Ссылки на зависимости теперь ведут только на основную копию, которую не удаляют, а перед удалением любого дерева проверяем, не ссылается ли на него кто-то ещё. И второе, более общее: «тесты прошли» без числа тестов ничего не значит. Прогон, в котором ничего не проверялось, должен считаться провалом.
Одна задача, одна ветка, один коммит
Агент сдаёт работу веткой ровно с одним коммитом поверх текущей основной ветки. Так ревьюер видит цельное изменение, а откатить его можно одной командой.
Есть и менее очевидное правило: ветка должна быть основана на самой свежей версии основной. Как только в неё что-то влили, все остальные ветки в очереди становятся устаревшими, и автор каждой перебазирует свою ветку сам. Проверка перед вливанием это отслеживает и не пускает ветку, у которой старое основание. Поначалу это раздражало, потому что после каждого вливания очередь заново шла на перебазирование. Зато так не бывает, что два по отдельности проверенных изменения вместе ломают то, чего не ломало ни одно из них: проверяется ровно то, что окажется в основной ветке.
После перебазирования сверяем, что изменение не поплыло: разница между старой основой и старой веткой должна совпадать с разницей между новой основой и новой веткой. Если не совпадает, значит, при разрешении конфликтов агент что-то поправил «заодно», и это надо смотреть отдельно.
Тесты, которые проверяют тесты
Агенты охотно пишут тесты, и тесты почти всегда зелёные. Вопрос в том, проверяют ли они то, что заявлено. Мы для этого делаем порчи: берём защиту, ради которой писалась задача, намеренно её ломаем и смотрим, покраснел ли хотя бы один тест. Потом возвращаем как было и ломаем следующую. Если защиту можно убрать, а тесты остаются зелёными, тест есть только на бумаге.
На такой проверке регулярно что-то находится. Например, тест на «бот не предлагает прошедшее время» проверял слот двухчасовой давности, и ошибка «показывать всё, что было меньше часа назад» через него спокойно проходила. Сам тест был, название правильное, а границу он не держал.
Отдельная история с функциями, где внутри работает языковая модель. Там тесты гоняются на подставной модели, которая отвечает заранее известным текстом. Они хорошо проверяют обвязку, но ничего не говорят о том, что ответит настоящая модель на живых данных. Поэтому любая ИИ-функция перед выкладкой проходит хотя бы один живой прогон, и результат смотрит человек.
Автор не вливает свой код сам
Ревью делает другой агент, обычно на другой модели, плюс человек принимает результат. Правило жёсткое и проверяется автоматически: тот, кто собрал ветку, не может её влить. Однажды техлид сам собрал пачку изменений, прогнал проверку, она была зелёной девять минут, и в последний момент сторож отказал: сборщик и тот, кто вливает, совпадают. Пришлось пересобрать пачку другим участником и прогнать заново. Обидно, но правило осталось. Если его можно обойти «потому что тут всё очевидно», его будут обходить именно тогда, когда не очевидно.
От ревьюера требуем не мнение, а проверку. Он сам ставит ветку в отдельное дерево, гоняет тесты, делает свои порчи и только потом пишет вердикт.
Долгие команды не должны зависеть от соединения
Полная проверка перед выкладкой у нас идёт минут десять. Первое время её запускали прямо в SSH-сессии, и примерно на девятой минуте соединение рвалось, а с ним и проверка. Агент видел обрыв и не всегда понимал, прошла она или нет.
Теперь всё долгое запускается отвязанным от сессии: через setsid и nohup, вывод в файл, номер процесса записываем. Дальше короткие запросы раз в минуту: жив ли процесс, что в конце журнала. Соединение может падать сколько угодно, проверка доходит до конца.
С остановкой процессов отдельная тонкость. pkill -f с куском командной строки кажется удобным, пока не выясняется, что этот кусок есть и в командной строке того, кто ждёт. Ожидание через pgrep -f в такой ситуации не заканчивается никогда, а pkill -f убивает заодно и собственную оболочку. Поэтому процессы мы ищем один раз, дальше работаем только по номеру: ждём через kill -0, останавливаем через kill с конкретным номером. pkill -f у нас под запретом.
Агенты уверенно выдумывают
Это не про код, а про всё вокруг. Мы попросили агента собрать список музыкальных треков со свободной лицензией для видео. Список пришёл аккуратный, с названиями, авторами и типом лицензии. Часть треков не существовала. В другой раз так же выглядели «найденные» контакты компаний.
Правило простое: всё, что агент принёс как факт из внешнего мира, сверяется с источником. Особенно лицензии, ссылки, имена и цифры. Модель не врёт специально, она достраивает правдоподобное, и выглядит это так же, как настоящее.
Похожая история с автоматическими находками. Агент, которому поручили искать ошибки, найдёт их всегда, и часть из них будет про ситуации, в которые ни один живой пользователь не попадёт. Мы перестали брать такие находки в работу, пока человек не повторил проблему руками. Иначе агенты начинают чинить то, что придумали другие агенты.
После выкладки
Выкладка идёт только с проверенной ревизии, а номер предыдущей записан заранее, чтобы откат занимал минуту. Сразу после выкладки отдельный скрипт проходит по основным сценариям на живом сервере. Он однажды поднял тревогу, которая оказалась вообще не про новый код: перезапуск соседнего сервиса без сохранённого секрета обнулял пользователям входы. Без проверки после выкладки это выглядело бы как свежая ошибка в релизе, и её искали бы не там.
Что из этого стоит спросить у подрядчика
Если вам делают продукт с помощью ИИ-агентов, а сейчас так делают почти все, вот вопросы, по ответам на которые многое понятно:
- Кто проверяет код, который написал агент, и может ли автор влить его сам.
- Как вы убеждаетесь, что тесты действительно что-то проверяют и зелёные не для вида.
- Как проверяются функции, внутри которых работает языковая модель: на заготовленных ответах или вживую.
- Что происходит, если агент принёс факт: лицензию, цену, контакт. Кто его сверяет.
- Сколько времени занимает откат, если после выкладки что-то пошло не так.
Если ответ на большинство из них «агент сам всё проверяет», это повод насторожиться. Агенты пишут код быстро и неплохо, но отвечать за результат по-прежнему должны люди.
Нужна помощь с настройкой такого процесса у себя или проект, который сделают по этим правилам? Посмотрите страницу услуги или напишите нам.
Craft Podium