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: преимущество получит тот, кто научится использовать его как инструмент, а не как протез.