AI (искусственный интеллект, в нашем случае — генеративные модели вроде ChatGPT, Claude или GitHub Copilot) пишет всё больше кода в продакшен-репозиториях. По разным оценкам, через AI-ассистент уже проходит от трети до половины строк, которые джуны коммитят в проекты. На спринт-демо это выглядит как магия: тикет закрывается за два часа вместо двух дней.
А через полгода в репозитории просыпается мина. Код, который год назад прошёл ревью и продакшн, начинает падать на пустяках, и никто в команде не понимает, что под капотом, потому что под капотом нет автора — есть автогенерация. Это и есть новая разновидность технического долга, которую индустрия пока не научилась считывать.
Что такое AI-долг и почему он не похож на обычный технический
Технический долг — это метафора, которую придумал ещё в 90-х Уорд Каннингем, автор первой вики. Логика простая: ты ускоряешь работу сейчас за счёт срезанных углов и берёшь обязательство расплатиться чистотой кода потом. Пока долг небольшой — проценты терпимые. Когда долг копится годами без рефинансирования (то есть без рефакторинга), — проценты съедают команду: каждая правка идёт втрое дольше, потому что ты сначала продираешься через старые костыли.
Обычный технический долг ты осознаёшь, когда его берёшь. Разработчик думает: «Сейчас приделаю заглушку, потом перепишу нормально». Это плохо, но честно — у долга есть автор и адрес.
AI-долг устроен иначе. Код, который выдаёт нейросеть, выглядит образцово: имена переменных грамотные, отступы ровные, есть JSDoc-комментарии (это формат документации функций), линтер не ругается. Ревью пропускает такой PR (pull request, запрос на вливание изменений в основную ветку) почти автоматически. А внутри — устаревший паттерн, фантомный метод или гонка состояний, которую не видно на стейдже.
Долг есть, а признаков долга — нет. Поэтому проценты по нему капают тихо.
Пять паттернов, которыми AI копит долг
Дальше — конкретные виды AI-кода, которые ломаются не сразу. Все примеры — реальные выхлопы популярных ассистентов, описанные понятным языком.
1. Галлюцинированные API
Самый частый и самый опасный паттерн. Языковая модель не помнит, какие именно методы есть у объекта, — она по статистике достраивает имя, которое должно быть. Чаще всего такой метод выглядит правдоподобно и даже работает в IDE (среде разработки, например VS Code) на уровне автодополнения. А потом падает в рантайме (во время реального запуска кода в браузере).
// AI предложил «убрать дубли из массива объектов»
const users = [
{ id: 1, name: 'Аня' },
{ id: 2, name: 'Боря' },
{ id: 1, name: 'Аня' },
];
const unique = users.uniqueBy('id'); // TypeError
Метода Array.prototype.uniqueBy() в стандарте JavaScript (язык программирования, на котором написан фронтенд) нет. Есть похожий groupBy(), есть библиотечный lodash.uniqBy() — но нативный uniqueBy просто выдуман. Тесты на пустом массиве и на коллекции из одного элемента такой код пропустит (исключение бросает только обращение к несуществующему методу).
Лечение: любой метод, который ты не помнишь наизусть, проверяй в MDN (Mozilla Developer Network, официальная документация по вебу) или спецификации — до того, как закоммитить.
2. Устаревшие практики, потому что их в обучающей выборке больше
Языковая модель училась на коде, которому в среднем 5–10 лет. Значит, в её «памяти» решений на jQuery больше, чем на современном fetch, классовых компонентов React больше, чем функциональных с хуками, а var больше, чем let и const. Если ты не уточнил стек, ассистент часто выдаёт «центр тяжести» выборки, а не лучшую современную практику.
// AI на просьбу «достань данные с сервера и покажи в списке»
$.ajax({
url: '/api/users',
success: function(data) {
var html = '';
for (var i = 0; i < data.length; i++) {
html += '<li>' + data[i].name + '</li>';
}
$('#list').html(html);
}
});
Это работает. Но для современного проекта такой код — долг сразу из трёх источников: лишняя зависимость от jQuery (библиотека, которую почти везде заменили нативные API), var с его странной областью видимости, конкатенация HTML строкой — готовая дыра под XSS (Cross-Site Scripting, внедрение чужого скрипта через данные).
Современный эквивалент в два раза короче и без всех трёх мин:
const res = await fetch('/api/users');
const users = await res.json();
const list = document.getElementById('list');
list.replaceChildren(
...users.map(u => {
const li = document.createElement('li');
li.textContent = u.name;
return li;
})
);
Чтобы AI выдавал такое — в промпте (запросе к модели) явно указывай стек и ограничения: «мой проект на нативном JS без jQuery, ES2022, без сторонних библиотек».
3. «Вроде работает»: гонки и пропущенные зависимости
Самый коварный класс: код проходит ручную проверку, проходит тесты, доезжает до стейджа — и ломается на продакшене, где пользователь делает что-то быстрее или в другом порядке.
// AI: «загружай список товаров при смене категории»
function ProductList({ categoryId }) {
const [items, setItems] = useState([]);
useEffect(() => {
fetch('/api/products?cat=' + categoryId)
.then(r => r.json())
.then(setItems);
}); // ← забыли массив зависимостей
return items.map(p => <Item key={p.id} {...p} />);
}
В React-хуке useEffect второй аргумент — список зависимостей, при изменении которых эффект надо запустить заново. Без него эффект запускается после каждого рендера: каждое нажатие кнопки в соседнем компоненте провоцирует новый запрос к серверу, ответы приходят вразнобой, и на экране оказывается список товаров из старой категории.
На разработческом стенде с быстрой сетью и кешем это не воспроизводится. На продакшене с 3G у пользователя — воспроизводится через раз. Найти такой баг по тикету «иногда показывает не те товары» занимает дни.
Похожих ловушек у AI много: async-функция без await, обработчик клика, который теряет this, regex (регулярное выражение для поиска по строке), который проходит десять валидных кейсов и падает на одиннадцатом. Все эти ошибки объединяет одно: они не очевидны на happy path, то есть на типовом сценарии.
4. Over-engineering «на вырост»
Модель училась на кодовых базах гигантских компаний, где паттерны строятся на десятилетие вперёд. Когда ты просишь у неё «маленькую утилиту, которая склеит два объекта», ты получаешь фабрику стратегий с интерфейсом и конфигом.
// Просили: объединить настройки пользователя с дефолтами
class MergeStrategy {
constructor(rules) { this.rules = rules; }
apply(base, override) { /* ... 40 строк ... */ }
}
class UserSettingsMerger {
constructor(strategy) { this.strategy = strategy; }
merge(defaults, user) {
return this.strategy.apply(defaults, user);
}
}
const merger = new UserSettingsMerger(
new MergeStrategy({ deep: true, arrays: 'replace' })
);
const settings = merger.merge(defaults, userPrefs);
Та же задача в нативном JavaScript — одна строка:
const settings = { ...defaults, ...userPrefs };
Долг здесь не в том, что «много кода». Долг в том, что через полгода другой джун будет читать UserSettingsMerger и думать: «Это серьёзная архитектурная конструкция, тут наверняка важная логика, не буду трогать». Он не удалит лишнее — он добавит сверху ещё один слой. Так разрастаются монстры, которых никто не звал.
5. Неконсистентность стиля внутри одного проекта
Языковая модель не помнит весь твой проект целиком — она видит контекст того окна, что ты ей дал. Если в одном файле ты попросил «сделай запрос», в другом — «добавь скачивание», в третьем — «дёрни API», ты получишь три разных стиля сетевых запросов в одной кодовой базе.
// auth.js
const data = await fetch('/api/auth').then(r => r.json());
// orders.js
const { data } = await axios.get('/api/orders');
// reports.js
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/reports');
xhr.send();
Три разных инструмента для одной задачи — три разных способа обрабатывать ошибки, три места, где можно забыть про CORS (Cross-Origin Resource Sharing, политика разрешений на запросы между доменами), три набора моков в тестах. Через полгода новый человек в команде честно спросит: «а как у вас принято делать запросы?» — и не получит однозначного ответа.
Лекарство — единый клиент сетевых запросов, обёрнутый в утилиту, и явное правило в CONTRIBUTING.md или внутренней доке. Тогда даже сгенерированный код будет тянуть из этой утилиты — если ты её ему показал.
Почему джун влетает в AI-долг первым
Скорость, с которой джун (начинающий разработчик) собирает рабочий компонент с помощью AI, не имеет ничего общего со скоростью, с которой он его понимает. Раньше, чтобы написать форму с валидацией и запросом на сервер, нужно было примерно понимать, что такое DOM (Document Object Model, древовидное представление страницы, с которым работает JavaScript), как работает событийная модель, как ловить ошибки fetch. Сейчас можно собрать ту же форму за 15 минут, не зная ничего из перечисленного.
Проблема включается не сегодня, а через два спринта. Форма ломается — и джун идёт чинить. Но он не понимает, какие именно три строки делают валидацию, потому что не он их писал. Он идёт обратно к AI и просит починить. AI выдаёт правку, которая закрывает один симптом и открывает три новых. Цикл закручивается.
Это похоже на то, что психологи называют выученной беспомощностью: человек перестаёт пытаться разобраться сам, потому что у него работает другой способ. Способ ненадёжный, но быстрый, поэтому привычку он ломает плохо. И именно джун страдает больше всех: у сеньора есть база, на которую он может опереться, когда AI ошибся, — у джуна базы пока нет, и каждая ошибка AI превращается в его персональный пробел в знаниях, который ничем не заполняется.
Параллельная история — собеседования. Когда на интервью спрашивают «объясни, как работает this в стрелочной функции», AI с тобой рядом не сядет. И разница между тем, кто понимает свой код, и тем, кто его сгенерировал, видна за первые пять минут.
Как работать с AI и не копить долг — чек-лист для джуна
AI — не зло и не магия. Это инструмент, которым можно пользоваться по-разному. Ниже — пять привычек, которые превращают AI из генератора долга в ускоритель обучения.
1. Читай каждую строку перед коммитом и проговаривай вслух. Если ты не можешь объяснить, что делает строка и почему именно так, — она не уходит в ветку. Произнесённое вслух обнажает дыры в понимании быстрее, чем перечитывание глазами.
2. MDN-фильтр. Любой метод, имя которого ты видишь впервые, гугли в MDN или официальной доке инструмента до того, как сохранить файл. Это занимает 30 секунд и ловит 80% галлюцинированных API.
// AI: «отсортируй пользователей по дате регистрации»
users.toSorted((a, b) => b.createdAt - a.createdAt);
Метод toSorted() существует, в отличие от uniqueBy() из первого примера — но поддержка в старых браузерах ограниченная. Проверка в MDN покажет это за секунды (страница про toSorted), и ты сразу выберешь — ставить полифил, заменить на [...users].sort() или поднять минимальную версию браузеров в browserslist.
3. Проси маленькие куски, а не модули целиком. Запрос «напиши мне форму регистрации» — гарантированный путь к долгу: ты получаешь 200 строк, в которых уже не отличить, где обработка ошибок, а где валидация. Запрос «напиши функцию, которая проверяет email на соответствие RFC 5322» — путь к коду, который ты сможешь прочитать и осознанно встроить.
4. AI как rubber duck, а не комбайн. Известный приём в разработке — объяснять задачу резиновой уточке на столе: половина багов находится, пока ты формулируешь. AI можно использовать так же — не «напиши за меня», а «вот моя реализация, найди в ней слабые места». Так ты держишь авторство кода у себя, а AI используешь как ревьюера.
Промпт-плохо: Напиши функцию debounce на JavaScript.
Промпт-хорошо: Вот моя реализация debounce [код].
Найди в ней проблемы: что будет при быстрых вызовах,
что с this, что с типизацией аргументов.
5. Тест перед генерацией. Если ты сначала пишешь тест, а потом просишь AI реализацию — ты получаешь контракт, который AI обязан выполнить. И любая галлюцинация ловится первым же запуском тестов, а не через полгода в продакшене. Это адаптация TDD (Test-Driven Development, разработка через тестирование) под эпоху AI.
Code review как фильтр AI-долга
На уровне команды правила те же, но с поправкой на масштаб. Ключевая мысль: AI-PR — это PR без автора, готового защищать каждую строку. Значит, ревью обязано задавать вопросы, на которые обычный PR отвечает сам.
- Спрашивай «почему именно так». Не «что делает этот код» (это и так видно), а «какие альтернативы рассматривал и почему выбрал это решение». Если автор не может ответить — код в main не идёт, идёт обратно на доработку.
- Тестируй edge-кейсы вручную. AI хорошо покрывает happy path и плохо — пустые входы, тысячные коллекции, отрицательные числа, разрывы сети. Ревьюер обязан мысленно прогнать через код хотя бы три неочевидных кейса.
- Помечай AI-PR-ы. Маленькая практика — тег ai-assisted на PR-ах с высокой долей сгенерированного кода. Через квартал ты увидишь в статистике, сколько AI-долга команда впустила в репозиторий и сколько багов из этих PR-ов вернулось.
- Не мерж до объяснения. Если ревьюер задаёт вопрос, а ответ «так предложил Copilot» — это не ответ. Это сигнал, что автор сам не понял, что он коммитит. Такой код в ветку не уходит.
Это не саботаж AI-инструментов — это та же дисциплина, что у команд была до их появления. Просто раньше код без понимания было физически тяжело написать — сейчас он появляется одной кнопкой Tab.
Проценты уже капают
Технический долг — не «когда-нибудь потом». Это процентная ставка, которая уже работает: каждый день, когда в репозиторий уходит непонятый сгенерированный код, ты потратишь на день больше времени, чтобы его потом починить.
Через год индустрию накроет волной legacy-кода (унаследованного кода, который никто не хочет трогать), который никто не писал — и поэтому никто не сможет починить без переписывания. Кто-то будет нанимать сеньоров на разгребание AI-долгов по цене сеньоров. Кто-то прогорит и закроется. А кто-то заранее научился жить с AI на трезвую голову — читать каждую строку, спрашивать «почему именно так», помечать AI-PR-ы и не путать скорость с прогрессом.
Разница между джуном до AI и джуном после не в том, что один пишет руками, а второй промптами. Разница в том, кто к году опыта остался джуном, а кто стал автором кода, за который не стыдно. AI это решение не делает за тебя — он только усиливает то, что ты выбрал.
Подобный сдвиг индустрия уже проходила: десять лет назад спорили, нужен ли типизированный JavaScript — и команды, которые осваивали TypeScript заранее, оказались в выигрыше. Та же история повторяется с AI: преимущество получит тот, кто научится использовать его как инструмент, а не как протез.
Комментарии (0)