← Code Review in the AI Era: Trust, Verify, Merge
01 / 07 · 45 min

Almost Right: a Failure Taxonomy of AI Code

Why AI code compiles, passes tests, and still breaks production: confident API hallucinations, copy-pasted legacy patterns, and silent contract changes — with a checklist for each class.

Lesson content is currently available in Russian.

What you will learn

  • →Объяснять, чем режим отказов AI-кода отличается от человеческих ошибок и почему классическое ревью его пропускает.
  • →Распознавать три главных класса дефектов: галлюцинации API, legacy-паттерны и тихие изменения контрактов.
  • →Применять чек-лист «почти правильно» к каждому сгенерированному диффу.

Почему AI-код ломается не так, как человеческий

Ошибки человека предсказуемы: опечатки, off-by-one в цикле, забытый крайний случай, недописанная ветка после переключения контекста. Годы код-ревью откалибровали нас именно на такие ошибки — мы ищем места, где автор устал, торопился или не знал. AI-код отказывает иначе. Модель не устаёт и не халтурит: она уверенно генерирует правдоподобный код, который компилируется, проходит поверхностные тесты, выглядит идиоматично — и при этом вызывает не тот метод, решает немного не ту задачу или тихо меняет поведение на границе модуля.

Это режим «почти правильно», и он опаснее откровенно сломанного кода. Сломанный код падает на первом запуске и чинится за минуту. «Почти правильный» переживает ревью, попадает в main и всплывает через две недели как инцидент, который никто не может связать с конкретным диффом. Исследование «Asleep at the Keyboard» показало, что около 40% фрагментов, сгенерированных Copilot в security-чувствительных сценариях, содержали уязвимости. Стэнфордская работа «Do Users Write More Insecure Code with AI Assistants?» добавила второй слой: разработчики с ассистентом писали менее безопасный код и были при этом сильнее уверены в его качестве.

Вывод для ревьюера: доверие к «выглядит нормально» нужно выключить. У человеческого кода корреляция между «выглядит аккуратно» и «работает правильно» есть; у сгенерированного — почти нулевая, потому что аккуратность модель выдаёт бесплатно всегда, независимо от корректности. Ревью AI-кода начинается с презумпции виновности: дифф считается неверным, пока классы отказов из этого урока не проверены явно.

Класс 1: уверенные галлюцинации API

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

// Сгенерировано: выглядит правдоподобно, но такого метода нет
const local = moment.tz.convert(input, "Europe/Berlin");

// Реальный API moment-timezone
const local = moment.tz(input, "Europe/Berlin");

Частые подвиды: выдуманные опции (JSON.stringify(data, { pretty: true }) вместо JSON.stringify(data, null, 2)), перепутанный порядок аргументов, методы из другой мажорной версии библиотеки, смешение API двух похожих пакетов (axios и fetch, lodash и ramda). Отдельный подвид — выдуманные пакеты целиком: модель импортирует несуществующий npm-модуль с убедительным именем. Это не только баг, но и дыра в безопасности — о slopsquatting поговорим в уроке про security.

Как ловить на ревью:

  • каждый незнакомый вызов API проверяй по документации, а не «по ощущению» — галлюцинации специально выглядят знакомо;
  • сверяй версию библиотеки в package.json с версией API, которую использует дифф;
  • новые импорты — в реестр: пакет существует? живой? тот самый?
  • запусти код: типы часто пропускают галлюцинации в динамических местах — рантайм-ошибка дешевле инцидента.

Класс 2: копипаст legacy-паттернов

Обучающие данные модели — это десятилетия чужого кода, и старых паттернов там на порядки больше, чем новых. Поэтому модель регулярно выдаёт код, который был нормой в 2015 году: var вместо const, колбэки вместо async/await, new Buffer() вместо Buffer.from(), устаревшие lifecycle-методы React. Хуже, когда legacy-паттерн небезопасен по современным меркам:

// Паттерн из обучающих данных десятилетней давности
const hash = crypto.createHash("md5").update(password).digest("hex");

// Современный минимум: медленный KDF с солью
const hash = await argon2.hash(password);

Коварство этого класса в том, что legacy-код чаще всего работает. MD5-хеш пароля не упадёт ни в одном тесте, new Buffer() выдаст только warning, колбэчная лапша пройдёт линтер. Ревьюер без чек-листа видит «рабочий код» и апрувит.

Второй подвид — копипаст паттернов из самого репозитория. Агент, которому показали старый модуль как пример, честно тиражирует его недостатки: тот же god-object, тот же прямой доступ к БД из контроллера, тот же обход слоя валидации. Модель усиливает существующие анти-паттерны кодовой базы, потому что для неё «так уже написано» — сильнейший сигнал «так правильно».

Как ловить: держи в репозитории список запрещённых паттернов (deprecated API, слабое крипто, прямые SQL-строки) и проверяй его линтером или Semgrep-правилами, а не глазами. Глаза привыкают, правила — нет.

Класс 3: тихие изменения контрактов — и общий чек-лист

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

// Было: вызывающий код различает «нет данных» и «пустой список»
function getOrders(userId: string): Order[] | null

// Стало после «рефакторинга» агента: null тихо превратился в []
function getOrders(userId: string): Order[]

Типизация тут не спасает — новый код корректен сам по себе, сломались вызывающие места в других сервисах, которых в диффе нет. Другие подвиды: расширенный catch, который теперь глотает и те исключения, что раньше пробрасывались; изменённый текст ошибки, который парсит клиент; переставленные значения enum, ушедшие в БД числами; поле, переименованное в JSON-ответе API.

Чек-лист «почти правильно» для любого AI-диффа:

  • каждый незнакомый API-вызов сверен с документацией и версией пакета;
  • новые зависимости существуют и проверены;
  • нет запрещённых паттернов: deprecated API, слабое крипто, строковый SQL;
  • публичные сигнатуры, форматы ответов и тексты ошибок совпадают до и после — проверено диффом по границе, а не «на глаз»;
  • каждый изменённый catch/return проверен: что раньше пробрасывалось или возвращалось в этом месте?

Этот чек-лист — каркас курса: дальше мы разберём, как прогонять его по большим диффам быстро, что из него отдать агенту-ревьюеру и что закрыть тестами.

Practice

Таксономия на своём материале

Возьмите 10 последних AI-сгенерированных диффов из своего проекта (или сгенерируйте 5 задач заново). Прогоните каждый по чек-листу урока и классифицируйте каждую находку: галлюцинация API, legacy-паттерн, тихое изменение контракта или иное.

Deliverable

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

Checkpoint

Объясните коллеге, почему ревью, откалиброванное на человеческие ошибки, пропускает дефекты AI-кода, и назовите три класса отказов с примером на каждый.

  • ✓ Сформулировано, что AI-код правдоподобен независимо от корректности, поэтому «выглядит аккуратно» перестаёт быть сигналом качества.
  • ✓ Названы все три класса: галлюцинации API, legacy-паттерны, тихие изменения контрактов — с конкретным примером на каждый.
  • ✓ Приведён хотя бы один способ автоматической проверки (документация/реестр, линтер или Semgrep-правило, дифф публичной границы).

Check yourself

Почему «почти правильный» AI-код опаснее откровенно сломанного?

01Почему «почти правильный» AI-код опаснее откровенно сломанного?

Что такое уверенная галлюцинация API?

02Что такое уверенная галлюцинация API?

Почему модель тиражирует legacy-паттерны вроде MD5 для паролей?

03Почему модель тиражирует legacy-паттерны вроде MD5 для паролей?

Функция возвращала Order[] | null, после рефакторинга агента — всегда Order[]. Почему это опасно, если новый код типобезопасен?

04Функция возвращала Order[] | null, после рефакторинга агента — всегда Order[]. Почему это опасно, если новый код типобезопасен?

Какой вывод следует из исследований «Asleep at the Keyboard» и стэнфордской работы об AI-ассистентах?

05Какой вывод следует из исследований «Asleep at the Keyboard» и стэнфордской работы об AI-ассистентах?

Sources

Almost Right: a Failure Taxonomy of AI Code · Code Review in the AI Era: Trust, Verify, Merge · VORCL