Сейчас на рынке много предложений разработать информационную систему с помощью вайбкодинга. Среди исполнителей есть люди без опыта в IT, которые хорошо разобрались, как работать с современными языковыми моделями. Они умеют получать от ИИ код и собирать из него приложения. Но кто проверит, что получилось внутри?
Мы много лет разрабатываем CRM и другие рабочие системы. В основных проектах наши программисты пишут код сами, а ИИ помогает им на разных этапах разработки и проверки. Об этом подходе мы уже рассказывали.
Для этой статьи мы разобрали результаты эксперимента длиной примерно в полгода: разрабатывали информационные системы, в которых весь код писал ИИ. Программисты не писали код, в том числе исправления. Для разработки и ревью использовали последние доступные на тот момент версии Fable, Opus и Sonnet, выбирая модель под задачу.
Хотим показать, к каким выводам пришли за эти полгода: насколько можно поручить разработку автономно языковым моделям, какие проблемы остаются в готовых продуктах и с чем мы столкнулись сами.
Как мы разрабатывали и проверяли код ИИ
Работа шла по цепочке:
ИИ пишет код → ИИ проверяет → ИИ исправляет → программист проверяет → ИИ исправляет замечания программиста → программист перепроверяет и допускает изменения к релизу.
Разработкой управлял специальный фреймворк. Он разбивает работу на этапы и задаёт порядок реализации и проверок. В нём предусмотрен TDD: сначала отдельный агент пишет тесты по требованиям, затем другой агент пишет код, который должен эти тесты пройти. После каждого внутреннего этапа код проходит ревью сильной моделью, например Fable, а замечания возвращаются на исправление. Фреймворк развивался вместе с проектами; TDD применялся к выбранным задачам.
Такой процесс заметно дольше обычной работы в чате. По нашей оценке, объём, который ИИ может реализовать за вечер в одной чат-сессии, по этому фреймворку может занять три-четыре дня. За это время появляется большой набор тестов, код несколько раз проходит ревью и исправления. И только после этих автоматических проходов его внимательно разбирает программист.
Какие проекты участвовали в эксперименте
Для статьи мы выбрали два проекта. Названия не раскрываем.
Первый — облачный SaaS-продукт с сотнями пользователей: личные кабинеты, несколько организаций, сотрудники с разными правами, клиенты, заказы и отчёты. В такой системе особенно важно не допустить, чтобы одна организация получила доступ к данным другой.
Второй — CRM и ERP для производственной компании. В ней есть заказы, остатки, резервы, счета, импорт данных и документы. По нашей оценке, разработка такой системы обычным подходом заняла бы около года.
Ниже собрали самые показательные замечания, которые наши программисты находили, проверяя код за ИИ.
Ошибки ИИ в коде: что нашли наши программисты
Это большой технический разбор: 20 примеров ошибок и неточностей. Основу составляют замечания программистов к коду, уже прошедшему автоматические проверки. Без их вмешательства эти проблемы на тот момент остались бы в системе. В нескольких примерах показали и продолжение: ИИ исправлял замечание, а автоматическое ревью находило новую ошибку в правке. Эти находки отдельно приписаны ИИ.
Если технические подробности вам не нужны, переходите к выводам. Все примеры свёрнуты. Нажмите на заголовок, чтобы увидеть проблему, возможные последствия и исправление.
Фатальными здесь названы ошибки, которые могли нарушить границы доступа или остановить работу. Мы описываем найденные дефекты и возможный ущерб, а не утверждаем, что у реальных клиентов уже украли данные или деньги.
Фатальные проблемы: доступ и остановка работы
Фатальная: администратор одной организации мог получить доступ к другой
Представим сотрудника, который работает с двумя организациями внутри сервиса. У него один аккаунт, но в каждой организации свои полномочия. Администратор первой компании управляет её сотрудниками. Из этого не следует, что ему можно управлять всеми доступами общего аккаунта.
В реализации ИИ через карточку сотрудника можно было менять пароль связанной учётной записи. Такое право сохранялось и тогда, когда аккаунт уже получил доступ ко второй организации. Администратор первой компании мог задать новый пароль и войти под этим человеком. Вместе с входом он получал доступы, которые вообще не относились к его компании.
Проверка «этот сотрудник принадлежит вашей организации» здесь ничего не решает. Сотрудник действительно принадлежит. Ошибка находится в объёме действия: редактирование общей учётной записи затрагивает больше, чем отношения сотрудника с одной компанией.
Программист указал на этот сценарий. В исправлении из карточки сотрудника убрали возможность менять пароль существующего аккаунта, отдельно ограничили изменение адреса почты и предусмотрели самостоятельное восстановление доступа. В журнале есть проверка сценария через серверный интерфейс, а не только скрытие кнопки на экране.
Без этой проверки управление персоналом могло стать способом попасть в чужую организацию. Для заказчика это означает, что изоляцию компаний нельзя принимать по демонстрации разных списков сотрудников: нужно проверить действия над сущностями, которые у них общие.
Фатальная: защитили администратора, но оставили путь к другим чужим правам
В другом проекте программист заметил возможность менять данные авторизации пользователя с более высокими полномочиями. Первое исправление защитило системного администратора. Казалось бы, самый опасный аккаунт закрыли.
На следующем проходе выяснилось, что привилегированный пользователь необязательно называется администратором. У обычной роли тоже могут быть разрешения, которых нет у проверяющего её сотрудника. Если сотрудник способен изменить доступ к такому аккаунту, он получает возможность действовать с чужими полномочиями.
Это показательный случай: исправление охватило названную роль, но общее ограничение осталось неполным. Важно не название аккаунта, а разница между его возможностями и правами человека, который пытается его изменить.
После повторного замечания правило расширили. Нельзя менять авторизацию пользователя, если у него есть права, отсутствующие у исполнителя действия. Потребовалось второе замечание программиста: первое исправление оставило обходной путь.
Для бизнеса такой дефект означает, что настроенная матрица прав может обходиться через управление чужой учётной записью. Ограничить сотруднику просмотр сведений недостаточно, если ему оставили другой путь получить нужные полномочия.
Фатальная: сбой уведомлений мог породить бесконечную цепочку новых задач
В системе были фоновые задачи. Они выполняются отдельно от открытой страницы: пользователь не должен ждать, пока завершится вся работа. Если задача окончательно падала, приложение отправляло сообщение об ошибке в мессенджер.
Отправка сообщения тоже выполнялась фоновой задачей. Когда канал связи был недоступен, эта задача могла упасть. Дальше срабатывал тот же обработчик: нужно сообщить об ошибке. Он создавал ещё одну задачу отправки через тот же неработающий канал.
Получалась цепочка: ошибка уведомления, новое уведомление об ошибке, ещё одна ошибка. При нескольких получателях количество создаваемых задач могло расти. Очередь начинала бы тратить ресурсы на собственные неудачные сообщения, конкурируя с полезной работой приложения.
Программист указал на этот цикл. В исправлении для задачи уведомления убрали повторное создание таких же сообщений. Сведения об отказе оставили в журнале. Отдельно предусмотрели запись ошибки, которая не зависит от работоспособности очереди.
Без исправления отказ внешнего канала мог перерасти в проблему самого приложения. Цикл пришлось разорвать в коде.
Кнопка «Отправить уведомление» этого не показывает. Чтобы найти проблему, нужно проверить, что происходит при отказе отправки, а затем при отказе сообщения об этом отказе.
Фатальная: после остановки переноса организация оставалась заблокированной
На время переноса данных приложение запрещало изменения в организации. Ограничение имело смысл: пока данные перемещаются, сотрудники не должны создавать новое состояние, которое не попадёт в перенос.
В коде были предусмотрены действия на случай ошибки: очистить промежуточное состояние, снять ограничение, сообщить о неудаче. Но они находились внутри той же фоновой задачи, которая выполняла перенос.
Процесс мог быть принудительно завершён, например по таймауту. В таком случае он не обязан успеть выполнить собственный код очистки. Перенос уже остановился, а запрет на изменения остался. Организация не могла нормально продолжать работу без восстановления или отдельного вмешательства.
Это неприятный класс ошибок: при чтении функции видно, что автор подумал об исключениях. Но обычная обработка исключения внутри программы и принудительная остановка процесса дают разные гарантии. Код, который больше не выполняется, не может убрать за собой.
После замечания добавили обработку аварийного завершения за пределами остановленной задачи. Пересмотрели настройки времени и повторов, очистку и уведомление о неудаче.
Для заказчика риск заключался в простое. Техническая операция обслуживания могла лишить сотрудников возможности менять данные. Работа возобновилась бы только после восстановления состояния.
Высокие риски: деньги и обязательства перед клиентами
Здесь приложение могло работать без сообщений об ошибках и при этом неправильно считать деньги. Проблемы возникали после обычных действий: смены клиента, изменения суммы заказа, повторного импорта.
Высокий риск: бонусы одного клиента возвращались другому
У клиента списали бонусы в счёт заказа. Позже сотрудник изменил клиента, к которому привязан заказ. Затем заказ отменили, и система должна была вернуть списание.
Реализация брала получателя возврата из текущего состояния заказа. В нём уже стоял другой клиент. В результате списание оставалось у первого человека, а возврат уходил второму.
Для примера возьмём 500 бонусов. Первый клиент потратил 500, но после отмены ничего не получил обратно. Второй получил 500, которых не тратил. Отдельно изменение клиента и отмена заказа могли выглядеть вполне рабочими функциями. Ошибка возникала при их последовательном использовании.
Программист указал на неправильного получателя. При исправлении возврат списания и отмену связанных начислений стали выполнять для прежнего клиента до изменения связи заказа. Новому клиенту старые бонусные операции автоматически не переносятся. Дополнительный сценарий с неправильным получателем отмены начисления нашли уже при разборе с ИИ.
Для бизнеса это могло обернуться неверными балансами, претензиями и ручным восстановлением истории. Бонусы могут не быть деньгами на банковском счёте, но они представляют обязательство компании перед клиентом. Системе нельзя решать, кому вернуть это обязательство, только по текущему значению поля.
Высокий риск: заказ уменьшили, а лишние списанные бонусы не вернули
Заказ стоил 2000. Клиент оплатил бонусами 800, остальное должен был внести другим способом. Потом состав заказа изменили, и его стоимость снизилась до 500.
Списанные 800 уже превышали новую стоимость. Приложение ограничивало итог к оплате нулём, чтобы на экране не появилось отрицательное число. При этом лишние 300 бонусов клиенту не возвращались.
Внешне результат выглядел правдоподобно: к оплате осталось ноль. Но баланс клиента был уменьшен на 800 за заказ стоимостью 500. Проверка допустимости одного поля скрывала нарушение общего учёта.
Исправление добавило сверку после пересчёта заказа. Если прежнее бонусное списание превышает новый итог, разница возвращается отдельной операцией. Такой сценарий закрепили в тестах.
Без замечания программиста изменение уже частично оплаченного заказа могло незаметно отнимать у клиента лишние бонусы. Пользователь интерфейса не обязан вычислять это вручную: он ожидает, что система согласует стоимость заказа и связанные списания.
Важная деталь: предшествующее автоматическое ревью бонусного этапа не выделяло критичных проблем. Это не помешало следующей проверке обнаружить оба описанных сценария с балансами.
Высокий риск: повторный импорт превращал счёт на 1000 в сумму 1300
Сотрудник загрузил счёт на 1000. Затем разделил его на две части: 700 и 300. Позже тот же исходный файл загрузили ещё раз.
При повторном импорте программа находила основную часть и обновляла её суммой из файла. В основную запись попадало 1000. Уже созданная дополнительная часть на 300 при этом сохранялась.
Итоговая последовательность выглядела так: 1000 → 700 + 300 → 1000 + 300. В системе оказывалось 1300 вместо 1000. Приложение не обязательно должно упасть или показать предупреждение, чтобы учёт стал неправильным.
Повторная загрузка файла вполне возможна в обычной работе. Пользователь может не помнить, что документ уже импортирован, или повторить действие после сомнительного результата. Если приложение умеет обновлять существующие документы, оно должно учитывать преобразования, которые с ними произошли после первой загрузки.
После замечания исправляли взаимодействие импорта и разделения. Затем дополнительно проверяли отображение частей счёта и ограничения видимости, чтобы после исправления суммы пользователю не пропадали необходимые сведения.
Последствие для бизнеса состояло бы в завышенном учёте и необходимости разбираться, почему сумма в системе не совпадает с исходным документом. Повторно загрузили тот же документ, получили лишние 30% в учёте.
Высокие риски: система обещает больше, чем есть
Один менеджер проверил остаток, другой тоже. Каждый по отдельности видит достаточно. Вместе они обещают клиентам больше, чем компания может предоставить.
Высокий риск: блокировка была, но при остатке 10 система могла зарезервировать 18
В системе доступно 10 единиц ресурса. Два пересекающихся заказа уже резервируют по одной. Два сотрудника почти одновременно увеличивают количество в своих заказах.
Первый меняет резерв с одной единицы на девять. Его проверка видит одну единицу во втором заказе: девять плюс один укладываются в доступные десять. Операция проходит.
Второй сотрудник тоже хочет поставить девять. Теперь проверка должна увидеть девять в первом заказе и отказать. Но в обнаруженном сценарии она могла прочитать прежнее состояние, в котором первый заказ занимал только одну единицу. Вторые девять также проходили. Система обещала 18 при наличии 10.
Особенно показательно, что блокировку к этому моменту уже добавили. Она заставляла одну операцию ждать другую. Проблема сохранялась из-за того, какие данные читаются после ожидания: транзакция могла использовать старый снимок, сформированный до чужого изменения.
Транзакция объединяет действия с базой в одну операцию, а режим изоляции определяет, какие изменения других операций ей видны. В этом случае замок на складской записи и чтение соседних заказов не обеспечивали нужного общего поведения. Наличие знакомого защитного приёма ещё не доказывало правильность всего сценария.
После замечания изменили режим чтения. По отчёту исправление проверяли на двух независимых соединениях с базой. Но у изменения был общий эффект: пришлось пересмотреть и другие проверки, которые рассчитывали на прежнее поведение блокировок. Дополнительные проблемы на этом этапе находило автоматическое ревью.
Без исправления сотрудники могли подтвердить обязательства, которые невозможно выполнить одновременно. Причина не относится ко всем базам данных или любому использованию блокировок. Она возникла в конкретном сочетании настроек и порядка чтения. Чтобы это увидеть, нужно понимать, что именно делает база во время параллельной работы.
Высокий риск: проверку резерва можно было обойти обычным изменением заказа
Допустим, при добавлении позиции система правильно проверяет доступное количество. Это ещё не значит, что ограничение сохранится после дальнейших действий.
У заказа можно поменять даты, и тогда его резерв начнёт пересекаться с другим. Закрытый заказ можно вернуть в работу. Складское списание способно уменьшить наличие ниже уже обещанного количества. Если резерв зависит от статуса заказа, изменение или удаление соответствующего статуса тоже затрагивает учёт.
В замечаниях программиста эти пути появлялись в разных сессиях. Проверки существовали, но были привязаны к отдельным операциям. Общее правило «нельзя обещать больше доступного» не было последовательно обеспечено во всех местах, где оно могло нарушиться.
Исправления включали общий механизм проверки состава и периода, а также защиту системных статусов от удаления. При этом в итоговом отчёте оставалась отдельная проблема количества запросов при массовом возврате заказов. Закрытие логической ошибки не делало все аспекты реализации автоматически завершёнными.
Для заказчика это риск невыполнимых обязательств. Сотрудники могут действовать совершенно добросовестно. Им достаточно перенести даты или возобновить заказ, чтобы ограничение перестало соответствовать действительности.
Программисту пришлось проверить все действия, которые меняют резерв.
Пароль поменяли. Старый доступ остался
Ещё два случая, в которых защита была предусмотрена, но работала не на всём пути.
Высокий риск: смена пароля не прекращала доступ через старые сеансы
Человек меняет пароль и рассчитывает прекратить нежелательный доступ. Например, он оставил открытый сеанс на чужом устройстве или подозревает, что кто-то уже вошёл в его аккаунт.
После успешного входа приложение обычно не просит пароль при каждом действии. Оно узнаёт пользователя по сессии или токену, то есть по ранее выданному подтверждению авторизации. Если изменить только пароль, такое подтверждение может продолжить действовать.
В обоих проектах обнаружились пути смены пароля, которые не отзывали старые сеансы. В одном из них восстановление по письму уже завершало прежний доступ, а обычная смена через профиль этого не делала. Поэтому проверить только восстановление пароля было бы недостаточно.
Исправление добавило отзыв сессий и токенов. Отдельно определили поведение текущего сеанса и проверили другие места изменения пароля.
Без этой доработки посторонний, уже получивший действующий сеанс, мог сохранить доступ после смены пароля. Смена пароля закрывала не все пути доступа.
Высокий риск: архив зашифровали, а секрет для его открытия оставался в данных задачи
При переносе данных создавался зашифрованный архив. Это полезная защита, но она зависит и от того, где хранится материал для расшифровки.
Секрет передавался в фоновую задачу. Очередь сохраняла данные этой задачи в базе, включая переданный секрет в открытом виде. После неудачного выполнения копия могла остаться и среди сведений об упавших задачах.
Чтобы воспользоваться таким дефектом, нужен доступ к соответствующим хранилищам. Он не означал, что любой посетитель сайта мог скачать всё содержимое. Но при доступе к архиву и сохранённым данным задачи шифрование могло не дать ожидаемой защиты: необходимый секрет лежал рядом в инфраструктуре приложения.
После замечания в очередь стали передавать секрет с дополнительной защитой ключом приложения. Также предусмотрели очистку промежуточных материалов при отказе.
Последствие для бизнеса состояло бы в снижении защиты экспортированных данных. Здесь нельзя оценивать безопасность только по наличию слова «шифрование» в описании функции. Нужно проследить путь секрета через выполнение, хранение и обработку ошибок.
Исправление защищает конкретное место хранения. Если одновременно с базой скомпрометирован и ключ приложения, дополнительное шифрование само по себе проблему не решает.
Почему проблемы не видны на короткой демонстрации
На демонстрационной базе все записи помещаются на одной странице, формы быстро отвечают. А потом данных становится больше, сотрудник нажимает чуть быстрее или меняет сразу несколько полей. И появляются проблемы, которых на показе не было.
Существенный риск: при обработке списка часть записей пропускалась
Система обрабатывала записи порциями. После первой порции она запрашивала следующую, используя смещение: пропустить столько-то строк и взять следующие.
Но обработка меняла порядок записей. Пока программа двигалась по списку, сам список уже перестраивался. Следующее смещение относилось к новому расположению элементов, хотя было рассчитано так, будто ничего не изменилось.
Для иллюстрации представим десять ожидающих записей. Обработали первые пять, и они перестали входить в начало выборки. Если после этого пропустить первые пять уже обновлённого списка, можно перескочить как раз через оставшуюся работу. В конкретном проекте проблема также была связана с сочетанием меняющегося порядка и перехода к следующей порции.
Программист указал на пропуски. Порядок обхода и получение следующих порций пересмотрели так, чтобы обработка не сдвигала ещё не пройденные элементы за границу выборки.
Для бизнеса это могло означать, что часть ожидаемой работы не будет выполнена. Записи при этом оставались в базе. Называть такой дефект удалением данных было бы неправильно: приложение пропускало их в конкретном процессе.
Существенный риск: поиск не находил заказ, который существовал за первой сотней
Большие списки загружались страницами. Это обычный способ не отправлять браузеру всю базу сразу. Но фильтрация работала только с уже полученными строками.
Если нужный заказ находился дальше первой сотни, поиск по текущему набору его не видел. Пользователь получал пустой результат, хотя запись существовала. На демонстрационной базе из нескольких десятков заказов такой сценарий вообще не возникал.
Была и связанная проблема с продолжением загрузки. Когда после фильтрации список становился коротким, исчезала прокрутка, от которой зависело получение следующих страниц. Интерфейс мог остановиться раньше, чем были просмотрены все подходящие данные. В другом проекте похожее ограничение скрывало позиции при добавлении в заказ.
Исправления переносили поиск на сервер, согласовывали фильтры и счётчики, восстанавливали подгрузку и сохранение выбора между страницами.
Последствие для сотрудников вполне практическое: можно потратить время на поиски, завести дубликат или решить, что нужной записи нет. При этом сама база не повреждена. Неверное представление о её содержимом создаёт интерфейс.
На маленькой базе поиск выглядел рабочим.
Накопительный риск: ради одного счётчика приложение перечитывало большую историю
На странице нужно показать небольшое число. Например, сколько записей требуют внимания. Для пользователя это второстепенный элемент интерфейса, но за ним может скрываться большой объём работы сервера.
В проекте счётчик использовал тяжёлый путь подготовки списка. В ходе исправлений сначала убрали лишнее построение представлений, затем обнаружили чтение многолетней истории. Позже программист снова вернулся к стоимости этого счётчика после дальнейшего развития приложения.
В замечании приводился расчёт: 50 заказов в день за три года дают около 55 тысяч заказов. Это иллюстрация возможного объёма, а не результат нагрузочного испытания. Сам принцип понятен: действие, которое быстро выполняется на сотне записей, может стать дорогим на десятках тысяч, особенно если его часто повторяют разные сотрудники.
Исправления сужали выборку на уровне базы, сокращали загружаемые поля и объединяли повторные запросы интерфейса. При этом позднее замечание подтвердилось частично: полные представления уже не строились, но лишние тяжёлые данные ещё читались.
Для бизнеса такой дефект проявляется постепенно. Новая система кажется быстрой, затем обычное открытие страниц становится всё дороже. История этого счётчика также показывает, зачем возвращаться к уже проверенному месту после изменений соседних функций.
Высокий риск: форма могла сохранить настройки одного объекта в другой
Администратор открыл настройки одного сервера, затем переключился на другой. Новые сведения ещё загружались, поэтому в полях оставались прежние значения. При этом действие сохранения уже относилось к новому выбранному серверу.
Если в этот промежуток нажать «Сохранить», настройки первого объекта могли попасть во второй. Дополнительную путаницу создавали сетевые ответы, приходящие не в том порядке, в котором пользователь переключал объекты.
Человек имел право редактировать оба сервера. Проблема не сводилась к проверке полномочий: интерфейс показывал одно состояние, а действие выполнялось в другом контексте. Аналогичный путь нашли и в карточке организации.
После замечания форму стали блокировать до успешной загрузки. Сохранение связали с идентификатором объекта, чьи данные действительно показаны в полях. Устаревшие ответы перестали применять к новому состоянию.
Без исправления обычное быстрое переключение могло привести к ошибочному изменению конфигурации. Масштаб последствий зависел бы от конкретных настроек. Для проверки нужно было воспроизвести задержку и смену контекста, а не просто убедиться, что каждую форму можно открыть и сохранить.
Существенный риск: разные части приложения по-разному определяли время и день
В проекте понадобилось согласовать работу с часовым поясом. Первоначальное изменение настройки не охватило все места, через которые дата проходила из базы в интерфейс.
Часть преобразований продолжала выдавать время в UTC, а интерфейс интерпретировал некоторые строки иначе. В результате время операции и ожидаемый рабочий день могли расходиться. Особенно заметно это на границе суток: для пользователя уже наступил новый день, а одна из частей приложения относит событие к предыдущему.
Само хранение времени в UTC не является ошибкой. Проблема возникает, когда стороны по-разному понимают переданное значение или преобразуют его несогласованно. Простая смена общего параметра не обязательно исправляет все такие места.
После проверки ввели единый формат передачи времени. По историческому отчёту потребовалось заменить 41 вызов форматирования и подключить общее правило к 55 моделям данных. Отдельно решили, что делать с уже сохранёнными демонстрационными датами: их не мигрировали, а существующий сдвиг описали.
Для бизнеса несогласованность могла означать неправильный день в отчёте или неожиданное время операции. Для разработки она обернулась правкой десятков мест. Первоначальный положительный отчёт не завершил задачу: проверка стенда показала, что изменение нужно продолжить.
Существенный риск: при сохранении полей прикреплённый файл не загружался
Пользователь менял поле карточки и одновременно прикреплял документ. Ожидание простое: после сохранения в карточке будут и новое значение, и файл.
Приложение сначала сохраняло поля. Сервер возвращал обновлённую карточку, и интерфейс применял её к текущему состоянию. При этом очищался список файлов, которые ещё ожидали отправки. Когда наступала очередь загрузки, отправлять было уже нечего.
Если прикреплять файл отдельно, ошибка не проявлялась. Если только редактировать поля, они тоже сохранялись. Не работало сочетание двух исправных по отдельности действий.
После замечания список ожидающих файлов стали сохранять до запроса изменения полей и отдельно передавать загрузчику. Результат загрузки добавлялся в карточку после выполнения операции.
Для сотрудника это могло выглядеть как успешное сохранение, после которого документ почему-то отсутствует. Уже загруженные файлы на сервере эта ошибка не уничтожала: терялось намерение отправить новый файл. Разницу важно сохранить и при оценке серьёзности бага, и при его объяснении заказчику.
Этот случай хорошо показывает, зачем проверять формы так, как ими пользуются люди: за один раз менять несколько связанных вещей.
Почему тесты и ответ «исправлено» не заканчивают проверку
«А тесты на что?» Они были. Но тест может проверять удобный пример, на котором ошибка не проявляется. А правка, которая наконец его удовлетворит, может сломать соседнюю функцию.
Существенный риск: тест проходил, потому что его данные скрывали ошибку
В историческом отчёте автоматического ревью одного этапа были положительный результат и 1019 пройденных тестов. На следующий день программист указал, в частности, на неправильную сортировку номеров заказов.
Номера сравнивались как текст. Для строк естественен порядок «1, 11, 2»: сравнение идёт по символам. Пользователь же ожидает числовой порядок «1, 2, 11».
Тест этого не обнаруживал, потому что в его данных использовались номера с ведущими нулями. У значений вроде «01, 02, 11» текстовый и числовой порядок совпадают. Тест исправно проверял удобный пример, на котором ошибка не проявлялась.
В другом замечании нашлась похожая проблема с форматом данных. Тест ссылки использовал полный адрес, хотя реальная обработка входных данных сохраняла только имя контакта. Проверка работала в придуманном для неё мире, который отличался от рабочего пути.
Исправления меняли и реализацию, и исходные данные тестов. Сортировку стали проверять на номерах, которые различают два порядка сравнения. Данные ссылки привели к формату, который действительно создаёт приложение.
Риск здесь шире неправильного порядка строк. Команда могла опираться на успешную автоматическую проверку, которая не подтверждала ожидаемое поведение. Число 1019 описывает результат конкретного исторического запуска, а не меру покрытия всех возможных ошибок.
Существенный риск: исправление дат в Excel сломало чтение денежных сумм
Импорт не распознавал настоящие ячейки Excel с датами. Программист сообщил об этом, и ИИ изменил способ чтения: начал учитывать формат ячейки.
Для дат это было нужно. Но тот же путь использовался и для денежных значений. После правки число в ячейке с денежным форматированием могло превратиться в строку с разделителями и знаком валюты. Обработчик суммы такую строку отвергал.
То есть файл проходил дальше в месте, на которое пожаловались, но мог остановиться на другом корректно заполненном поле. Чтобы обнаружить регрессию, нужно было проверить не только дату из исходного замечания, но и остальные типы данных, проходящие через изменённый механизм.
Эту дополнительную проблему нашёл ИИ-ревьюер. В следующем исправлении для дат сохранили распознавание формата, а остальные числовые значения стали читать именно как числа.
Для бизнеса последствием был бы отказ импорта допустимого файла. Для процесса разработки это пример, почему поручение «исправь замечание» не заканчивается первым положительным ответом исполнителя.
Здесь обе проверки оказались полезны. Человек обнаружил исходный дефект. Модель заметила ошибку в правке. Если приписать всю историю только программисту, получится менее точное объяснение того, как улучшался код.
Высокий риск: одна заявка могла превратиться в два заказа
Заявку уже преобразовали в заказ. Система сохранила связь между ними, но позволяла вернуть заявку в промежуточное состояние. После этого преобразование можно было выполнить снова.
Повторное действие создавало новый заказ и меняло ссылку в заявке. Первый заказ при этом оставался в системе, но уже без ожидаемой обратной связи. По одной заявке получались две записи, и их происхождение становилось сложнее проследить.
Программист указал на повторное преобразование. Первая правка запретила выход из конечного состояния и создание нового заказа при существующей связи.
Затем автоматическое ревью нашло конкурентный сценарий: две операции могли одновременно пройти проверку до того, как одна из них завершит изменение. Последовательный повтор уже был запрещён, но одновременные действия требовали общей блокировки. Её добавили следующим шагом.
Для бизнеса дубликат мог означать повторную обработку одной потребности и несогласованную историю обращения. Дальнейшие последствия зависят от того, какие действия сотрудники запускают по новому заказу.
У исправления оказалось два условия: повтор не должен проходить ни после завершённой операции, ни одновременно с ней. Первое замечание пришло от человека, второе от ИИ.
Выводы: можно ли доверить ИИ разработку системы?
По опыту этих двух проектов мы бы не доверили ИИ разработку рабочей системы, код которой не проверяет сильный программист. Слишком много рисков с доступами, данными, учётом и восстановлением после сбоев. В нашем случае они оставались даже после тестов и нескольких проходов автоматического ревью.
Особенно рискованно, когда услугу оказывает человек без опыта разработки. Он может хорошо управлять моделью и получать работающие приложения. Но если он не понимает код, то не сможет самостоятельно оценить ни решение ИИ, ни его ответ «всё исправлено». Заказчик узнает о пропущенных ошибках уже в работе.
Именно этого мы бы опасались в предложении «навайбкодим вам любой сервис». Получить реализацию с помощью ИИ можно. Понять, безопасно ли доверить ей работу компании, без инженерной проверки нельзя.
До ИИ программисты тоже писали код с ошибками, и даже ревью тимлидом ловило не всё. Сегодня совместная проверка человеком и сильной моделью позволяет находить больше: в наших проектах полезные замечания приходили от обеих сторон. Поэтому мы продолжаем использовать ИИ. Но исключать программиста из процесса по результатам этого эксперимента не готовы.
При этом в основных проектах программисты по-прежнему пишут код сами с помощью ИИ. Когда человек создаёт систему, он принимает решения и понимает связи между ними с самого начала. При ревью сгенерированного кода ему приходится восстанавливать эти решения постфактум, а иногда пересматривать сразу несколько готовых функций. Человеческое ревью снижает риски вайбкодинга, но не делает эти два способа разработки равноценными.
Описанный опыт актуален на октябрь 2026 года.