Дублирование вместо извлечения.
При написании нового кода ИИ-ассистенты склонны вставлять или заново выводить слегка подправленную копию уже существующей логики вместо того, чтобы вызвать или извлечь общую функцию, поэтому дублированный код растёт с каждой новой задачей.
##Признаки и симптомы
Ревьюер распознаёт этот запах, когда созданный ИИ дифф добавляет блок, который уже где-то существует в репозитории, слегка переименованный, вместо вызова уже имеющейся функции. Характерная форма — два или более почти идентичных тела, различающихся лишь существительным (user/order), эндпоинтом или константой — та самая копия, которую человек вынес бы в общий код.
Типичные сигналы:
- Новые обработчики/сервисы, дословно повторяющие один и тот же шаблон валидации, ретраев, отображения ошибок или загрузки данных.
- Магические строки и литералы конфигурации, перепечатанные в каждом новом блоке вместо ссылки из одного места.
- Каждый ход агента заново реализует логику, которую предыдущий ход уже написал (агент не помнит, что вспомогательная функция существует).
- Детектор клонов (jscpd, PMD CPD, метрики дублирования Sonar) сообщает о новых дублированных блоках — или о скачке
duplicated_lines_density— в PR, который добавил новую функциональность.
// ИИ добавил это для нового маршрута — но getUser уже существует 30 строками выше
async function getOrder(id: string) {
const res = await fetch(`${API}/orders/${id}`, { headers: authHeaders() });
if (!res.ok) throw new Error(`Request failed: ${res.status}`);
const json = await res.json();
if (!json?.data) throw new Error("Malformed response");
return json.data;
}
async function getUser(id: string) { // <-- тело идентично на 95%
const res = await fetch(`${API}/users/${id}`, { headers: authHeaders() });
if (!res.ok) throw new Error(`Request failed: ${res.status}`);
const json = await res.json();
if (!json?.data) throw new Error("Malformed response");
return json.data;
}
Это классический запах «Дублирование кода», только порождаемый систематически, а не случайно.
Смежный, но другой запах — «Избегание рефакторинга»: там модель никогда не перестраивает уже существующий код, а прикручивает новое поведение сверху, вместо того чтобы менять дизайн. Этот запах уже и возникает в момент написания кода: модель заново выводит логику, у которой уже есть своё место, вместо того чтобы вызвать её или извлечь.
##Причины проблемы
Почему модели это порождают
- Локальность следующего токена. LLM дописывает текущий фрагмент по наиболее похожему тексту в своём окне контекста. Воспроизведение близлежащего, заведомо рабочего блока — продолжение с наивысшей вероятностью; "остановись, объяви вспомогательную функцию, затем вызови её" — это более длинный, менее вероятный окольный путь, охватывающий несколько файлов.
- Нет модели всего репозитория. Ассистент редко знает, что подходящая вспомогательная функция уже существует за пределами его контекста. Без извлечения он не может переиспользовать то, чего не видит, поэтому генерирует заново. Эно Рейес (Factory) связывает пользу от ИИ с качеством самой кодовой базы: «чем выше качество кодовой базы, тем сильнее ИИ ускорял организацию» (интервью в блоге Stack Overflow), — а клоны, уже лежащие в репозитории, становятся образцом, который следующая генерация скопирует снова.
- Амнезия от хода к ходу + объём промпта. Каждый запрос трактуется как самодостаточная единица ("заставь это работать"), поэтому агент переобучается на узкое, одноразовое решение вместо обобщения. OX Security называет это сверхспецификацией (Over-Specification) — «узкоспециализированные одноразовые решения, которые нельзя приспособить для других целей» — и находит её в 80–90% изученного ИИ-кода (отчёт OX).
- Подхалимство / путь наименьшего сопротивления. Модели оптимизируют вывод, который зримо удовлетворяет запрос, не трогая несвязанные файлы. Копия блока остаётся внутри редактируемого файла, а вызов общего хелпера — или его извлечение — требует залезть в другой модуль, который модель толком не видела, поэтому побеждает локальная копия.
- Инструментарий делает это дешёвым. Как отмечает DevClass в обзоре отчёта GitClear, принятие подсказки клавишей Tab делает вставку свежего блока почти бесплатной, убирая трение, которое раньше подталкивало разработчиков к переиспользованию (DevClass).
Почему это вредит
- Измеренная эрозия качества. Анализ GitClear 2025 года (211 млн изменённых строк, 2020–2024) показал, что доля строк, связанных с рефакторингом («перемещённых»), упала с 25% изменённых строк в 2021 году до менее чем 10% в 2024-м, а доля скопированных/вставленных строк за тот же период выросла с 8,3% до 12,3%; 2024 год стал первым, когда скопированных строк оказалось больше, чем перемещённых, а число блоков из пяти и более дублирующихся строк за 2024 год выросло примерно в восемь раз (GitClear 2025).
- Долг корректности — "дежавю багов". OX Security обнаружила это в 70–80% ИИ-кода: ошибка, исправленная в одном клоне, молча выживает во всех остальных, поэтому идентичные дефекты повторяются и каждый требует избыточного исправления. Дублирование превращает одну ошибку в N ошибок.
- Сопровождаемость и нагрузка на ревью. Каждое изменение поведения нужно найти и отредактировать во многих местах; диффы раздуваются почти идентичными блоками, которые ревьюерам приходится сравнивать построчно, чтобы убедиться, что они действительно идентичны (а не тонко и опасно различны).
- Поверхность атаки. Уязвимый паттерн (отсутствующая проверка прав, неэкранированный ввод) распространяется в каждую копию, а позднее укрепляющее исправление может пропустить клоны — дублирование умножает поверхность для заплаток.
##Лечение
Тактики ревью и промптинга
- Скажите модели, куда смотреть в первую очередь: "Прежде чем писать новый код, найди в репозитории существующую вспомогательную функцию, которая это делает, и переиспользуй её; не дублируй". Вставьте нужный модуль, чтобы он был в контексте.
- Явно требуйте консолидации: "Если два блока различаются лишь параметрами, извлеки одну параметризованную функцию и вызывай её из обоих". Это «Извлечение метода», за которым следует «Параметризация метода».
- Заставьте её запускать инструменты: "Запусти линтер / jscpd / проверку дублирования и устрани любой клон, о котором они сообщат, прежде чем возвращать дифф". Агенты хорошо самокорректируются, когда им дан детерминированный сигнал.
- На ревью относитесь к блоку, похожему на вставленный, как к поводу для grep: найдите одну отличительную строку; если она уже существует, верните блок на извлечение.
Рефакторинг — Извлечение метода + Параметризация метода
// после: один источник истины, вызывается из обоих
async function getResource<T>(path: string): Promise<T> {
const res = await fetch(`${API}/${path}`, { headers: authHeaders() });
if (!res.ok) throw new Error(`Request failed: ${res.status}`);
const json = await res.json();
if (!json?.data) throw new Error("Malformed response");
return json.data as T;
}
const getOrder = (id: string) => getResource<Order>(`orders/${id}`);
const getUser = (id: string) => getResource<User>(`users/${id}`);
Теперь исправление обработки ошибок или разбора ответа вносится один раз. Там, где дубликаты различаются шагом, а не значением, предпочтите «Создание шаблонного метода» или передавайте варьирующийся шаг как колбэк. Канонические приёмы здесь — «Извлечение метода», «Извлечение переменной» или «Замена магического числа символьной константой» (для перепечатанных литералов) и «Подъём метода», когда клоны живут в соседних классах одной иерархии, — всё это стандартные лекарства от запаха «Дублирование кода».
Защитный барьер, а не только уборка: встройте детектор клонов в CI с низким порогом, чтобы следующий сгенерированный ИИ дубликат проваливал сборку, а не вливался. Сочетайте это с однострочным внутренним правилом в CLAUDE.md/AGENTS.md ("переиспользуй существующие вспомогательные функции; никогда не вставляй почти-дубликат"), чтобы это ограничение ехало в контексте модели на каждом ходу.
##Обнаружение анализаторами
- jscpd --min-tokens / --min-lines / --threshold — Дублирование при копировании и вставке (настраиваемые пороги min-tokens / min-lines; поддерживает проверку --threshold в CI)
- PMD CPD — CPD — детектор копипасты на основе токенов для более чем 30 языков
- SonarQube / SonarSource S4144 — Методы не должны иметь одинаковую реализацию
- SonarQube / SonarSource S1192 — Строковые литералы не должны дублироваться
- SonarQube duplicated_lines_density — Дублированные блоки / метрика duplicated_lines_density (встроенный механизм поиска копий; превышение порога не проходит проверку качества)
- eslint-plugin-sonarjs sonarjs/no-identical-functions
- eslint-plugin-sonarjs sonarjs/no-duplicate-string