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
Sources