В мае я обещал через пару недель разобрать четыре случая, где ИИ подводила не сама модель, а то, что вокруг нее. Прошло пять месяцев. Обещание я не сдержал. Тот разбор будет, но сегодня про другое: про то, чем я все это время занимался.

Я делал kb-engine. Это моя база знаний на Go с открытым кодом: туда я складываю прочитанные статьи и заметки, там ищу и смотрю все в браузере. Пользуюсь каждый день.

С 24 июня по 23 октября в проекте набралось 392 коммита, 28 версий и 246 файлов с тестами. Почти весь код я писал вместе с ИИ-агентом. И довольно быстро понял одну вещь: правило, которое можно пропустить, когда неудобно, никто соблюдать не будет. Ни я, ни агент. Работает только то, что проверяет код.

Так в проекте появились автоматические проверки. Их называют гейтами: код не пройдет дальше, пока проверка не зеленая. Сейчас их 21. Расскажу пять историй, и в нескольких из них проверка была зеленой и при этом ничего не проверяла.

История 1. Проверки, которые хочется отключить

Сначала кажется, что все проверки надо запускать перед каждым коммитом: форматирование, сборку, тесты, линтер. Тогда плохой код точно не пройдет.

На практике получается наоборот. Если каждый коммит ждет минуту, это начинает бесить. У git есть флаг --no-verify, который пропускает все проверки, и рука сама к нему тянется. Агент этим флагом тоже умеет пользоваться.

Поэтому я разделил проверки на быстрые и медленные. Перед коммитом идут только быстрые: форматирование, поиск паролей и ключей в коде и несколько правил про архитектуру. Тесты, поиск гонок, порог покрытия 80% и линтер запускаются перед отправкой кода на сервер, то есть при push.

Была и вторая причина. Я пишу по TDD: сначала тест, который падает, потом код, который его чинит. Падающий тест надо закоммитить. Если тесты гоняются перед каждым коммитом, сделать такой коммит нельзя. Получается, что проверка мешает работать правильно.

История 2. Зеленая проверка, которая ничего не проверяла

Эта история самая неприятная.

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

Перед каждой новой версией я прогонял список адресов и проверял, что каждый отвечает кодом 200, то есть все в порядке. Три версии подряд проверка была зеленой. А в списке был адрес /api/finance, которого в программе вообще нет. Сервер отвечал 200 на любой адрес, и проверка не видела разницы между настоящим и выдуманным.

Та же ошибка нашлась и с картинками. Без специального флага при запуске сервер должен говорить, что картинки нет. А он отдавал вместо картинки HTML-страницу с кодом 200.

Чинил я это так. Сначала написал тест, который падает: запросил картинку без флага и увидел страницу вместо ошибки. Потом починил. А потом сделал то, что теперь делаю всегда: специально сломал код и проверил, что тест это ловит.

Отсюда мое главное правило. Если проверка зеленая, когда ошибок нет, сначала подсунь ей ошибку и посмотри, как она покраснеет. Если не покраснела, она ничего не проверяет.

История 3. Проверка, которую никто не запускал

В проекте есть карты архитектуры. Это описания вида: запрос приходит сюда, потом идет сюда. Каждый шаг привязан к конкретной строке кода.

Карты быстро устаревают, поэтому я написал программу, которая проверяет, что строки в картах до сих пор указывают куда надо. Я ее хорошо протестировал и подсовывал ей 13 разных ошибок. Она ловила все.

16 августа я решил посмотреть, кто ее запускает. Оказалось, никто. Ни при коммите, ни на сервере. Программа лежала в репозитории, ее тесты были зелеными, а запускал ее только я сам, когда вспоминал.

За это время карта отстала от кода на 36 коммитов. 61 ссылка указывала не туда. В трех местах карта описывала ошибки, которые я давно исправил.

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

История 4. Неделя карантина для библиотек

Иногда в популярную библиотеку подкладывают вредоносный код: взламывают аккаунт автора и выпускают новую версию. Обычно такую версию находят и удаляют за несколько дней. Поэтому есть простое правило: не ставить версии, которым меньше недели.

У меня это правило было просто записано в заметках. 13 августа бот Dependabot сам предложил обновить библиотеки, и среди них были @types/node возрастом пять дней и vite возрастом шесть. Обновление приняли. Правило в заметках никто не проверял.

В тот же день я сделал защиту из трех частей, потому что одной настройки не хватает:

Что

Где

Что ловит

Что пропускает

задержка в Dependabot

.github/dependabot.yml

бот не предлагает версии моложе недели

обновления безопасности и все, что пришло не от бота

min-release-age=7

frontend/.npmrc

npm install не ставит свежую версию

npm ci, он просто ставит то, что уже записано

скрипт dep-age.sh

проверка на pull request

свежую версию, как бы она ни попала в проект

библиотеки не из npm

Про npm ci я специально проверил руками, а не поверил документации. Он и правда не смотрит на возраст версий. Тот самый августовский коммит теперь служит самопроверкой: на нем скрипт обязан упасть и назвать обе версии.

История 5. Дубль, который спрятался

Последняя история короткая, но она мне нравится больше всех. Ошибка в ней была не в коде, а в тесте.

Я чинил баг: при записи в файл некоторые строки задваивались. Написал тест: записываю, читаю, считаю строки. Тест прошел. Тогда я запустил его на старом, сломанном коде. Он прошел и там.

Дело было в одной строчке. Тест складывал прочитанные строки в словарь по номеру записи и считал, сколько в словаре элементов. А у дубля тот же номер. Словарь склеивал две одинаковые строки в одну, и число выходило правильным. Считать надо было обычный список.

Вывод простой: если ищешь дубли, не убирай их раньше, чем посчитал.

Что можно взять себе

У каждого проекта свои проверки. Но вопросы к любой новой проверке у меня теперь одни и те же:

  • Кто ее запускает? Если только я сам, когда вспомню, это не проверка.

  • Что она должна поймать? Подсунь ей это и посмотри, как она падает.

  • Не падает ли она всегда? Запусти на чистом коде.

  • Не мешает ли работать? Медленное переносится на push, иначе все начнут ее отключать.

Все 21 проверку можно посмотреть в открытом репозитории: github.com/VDV001/kb-engine, папка scripts/gates/. Там же описано, что каждая из них не ловит. Коротко о проекте с цифрами есть на моем сайте: daniilvdovin.com/projects/kb.

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

Короткие заметки между статьями буду писать в Telegram: @daniil_manifesto

Комментарии (6)


  1. krvfjss
    01.11.2026 10:17

    Да, я иногда вставляю в программы глюкогенератор при сборке с определенными ключами. Например, деление на ноль. И специальные тесты проверяют, что исключение обрабатывается корректно. Или выделение буфера на триллион байт. Тест проверяет, что программа реагирует на нехватку памяти корректно.

    Мы все привыкли чисто формально писать типовые обработчики try catch, которые никогда не срабатывают при нормальных условиях. И потому не тестируются. И я так делал. Пока однажды не столкнулся с ситуацией, что программа падает, а в логах ничего нет. Оказалось, что в одном из обработчиков ошибок заложена ошибка, при которой лог не пишется. Теперь всегда стараюсь включать в программы код, который имитирует разные сбои. В релиз он не идëт, конечно. Это специальная сборка для тестов на обработку ошибок.


    1. vdv007 Автор
      01.11.2026 10:17

      Доброго времени дня и суток Вам! Спасибо за Вашу историю, что примечательно, чем сильнее становится модель, тем больше хочется доверять ей полностью, но в итоге оказывается, что тесты ничего не проверяющие размножаются также как и ранее.


      1. krvfjss
        01.11.2026 10:17

        Как говорил один мой товарищ: "Можно написать много контрольного кода в программе, но, большой аппаратный рубильник с красной ручкой выглядит надёжнее". В кибербезопасности есть понятие "Air Gap". Ну а при кодинге моделями критичные проверки должны быть жёстко закодированы руками.


        1. vdv007 Автор
          01.11.2026 10:17

          Отлично сказано - запомню! Про кодирование руками, думаю, что у современного разработчика есть все шансы писать качественный код, но также есть и большой успех по части лени, поэтому - Air Gap будет с нами еще очень долго, это уже очень древний человеческий фактор.


  1. netch80
    01.11.2026 10:17

    1. В проектах начиная с некоторого уровня серьёзности вводят мутационное тестирование. Если мутация не привела к поломке хотя бы одного теста, который покрывает подвергнутый мутации код, тут что-то не то, тесты не полны, или же тестовая система подломана.

    ​2) Я делал в своих серьёзных проектах инверсное тестирование. Например, если функция со входом A1 должна дать результат B1, а A2 – B2, то делаю тест, где на входе A2, а ожидается B1. Тест этот должен сломаться (его враппер проверяет это и кладёт в отдельную категорию), если не сломался – наверх выдаётся поломка теста в целом. Обычно этим ловятся проблемы недостаточного понимания спека, но дважды было, что ловились случаи очень тонких ошибок, и ещё один раз таки поломка тестовой инфраструктуры (при определённых комбинациях тестовых проверок и условий, в которых они запускались, негативный результат глушился).

    Мне в вашем описании не нравится в первую очередь зацикленность на TDD. Для случаев, когда спек твёрдо ясен, и написание кода – прямолинейная операция, он будет работать в таком виде. На сложных алгоритмических задачах или при изменениях спека уже по результатам написания, он непригоден. Поэтому я его никогда не форсирую, это рекомендация для заметной части случаев, но не более того.

    Теперь у меня для них есть общий вывод: когда агент пишет, что все проверил, к этому стоит относиться как к любой проверке и сначала подсунуть ему ошибку.

    Заметная часть случаев, пишут рядом, может быть отловлена, если другой агент (другой модели или хотя бы под другим логином) будет специально натравлен искать проблемы в том, что первый создал. Конечно, при этом им придётся платить вдвойне:))


    1. vdv007 Автор
      01.11.2026 10:17

      Доброго времени суток!

      Про мутационное тестирование согласен, по сути это тот же принцип: сначала сломать и убедиться, что проверка это заметила. Инверсный тест с отдельной категорией в обертке мне понравился, особенно случай, когда так поймали заглушенный негативный результат. У меня было ровно это: проверка была зеленой, потому что падение где-то по дороге терялось.

      Про TDD Вы правы, это не догма. На задачах, где спек меняется по ходу, я его тоже не держу. Важнее, чтобы до слова готово кто-то попробовал это сломать.

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