ConstructiCat Logo
CodeBust.
Browse section ▾

Дублирование вместо извлечения.

ИИ-ассистенты склонны вставлять свежую, слегка подправленную копию существующей логики вместо переиспользования или извлечения общей функции, раздувая дублированный код, тогда как рефакторинг тихо исчезает.

##Signs and Symptoms

Ревьюер распознаёт этот запах, когда созданный ИИ дифф добавляет блок, который уже где-то существует в репозитории, слегка переименованный, вместо вызова уже имеющейся функции. Характерная форма — два или более почти идентичных тела, различающихся лишь существительным (user/order), эндпоинтом или константой — та самая копия, которую человек вынес бы в общий код.

Типичные сигналы:

  • Новые обработчики/сервисы, дословно повторяющие один и тот же шаблон валидации, ретраев, отображения ошибок или загрузки данных.
  • Магические строки и литералы конфигурации, перепечатанные в каждом новом блоке вместо ссылки из одного места.
  • Каждый ход агента заново реализует логику, которую предыдущий ход уже написал (агент не помнит, что вспомогательная функция существует).
  • git log показывает последовательные ИИ-коммиты, добавляющие строки, но почти без "перемещённых"/переименованных строк — добавления без консолидации.
// ИИ добавил это для нового маршрута — но 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;
}

Это классический запах Duplicate Code (Дублирующийся код), но порождаемый систематически, а не случайно.

##Reasons for the Problem

Почему модели это порождают

  • Локальность следующего токена. LLM дописывает текущий фрагмент по наиболее похожему тексту в своём окне контекста. Воспроизведение близлежащего, заведомо рабочего блока — продолжение с наивысшей вероятностью; "остановись, объяви вспомогательную функцию, затем вызови её" — это более длинный, менее вероятный окольный путь, охватывающий несколько файлов.
  • Нет модели всего репозитория. Ассистент редко знает, что подходящая вспомогательная функция уже существует за пределами его контекста. Без извлечения он не может переиспользовать то, чего не видит, поэтому генерирует заново. Практики описывают агентов, которые "воссоздают похожую логику с нуля для каждой новой задачи", а Eno Reyes (Factory) представляет само качество кода как главный предиктор того, помогает ИИ кодовой базе или вредит.
  • Амнезия от хода к ходу + объём промпта. Каждый запрос трактуется как самодостаточная единица ("заставь это работать"), поэтому агент переобучается на узкое, одноразовое решение вместо обобщения. OX Security называет это Сверхспецификацией (встречается в 80–90% ИИ-кода) и Избеганием рефакторингов (80–90%): модель выдаёт функциональный код под непосредственный промпт, но никогда не консолидирует.
  • Подхалимство / путь наименьшего сопротивления. Модели оптимизируют вывод, который зримо удовлетворяет запрос, не трогая несвязанные файлы. Дублирование "безопасно" и локально; рефакторинг рискует сломать что-то, чего модель не видит, поэтому она его избегает.
  • Инструментарий делает это дешёвым. Как отмечает GitClear, принятие по Tab делает вставку свежего блока почти бесплатной, убирая трение, которое раньше подталкивало разработчиков к переиспользованию.

Почему это вредит

  • Измеренная эрозия качества. Анализ GitClear 2025 года (211 млн изменённых строк, 2020–2024) показал, что доля скопированных строк выросла с 8,3% до 12,3%, тогда как доля "перемещённых" (отрефакторенных) строк упала с ~24% до 9,5% — 2024 год стал первым, когда скопированные строки превысили перемещённые, а число блоков из 5+ дублированных строк подскочило примерно в 8 раз.
  • Долг корректности — "дежавю багов". OX Security обнаружила это в 70–80% ИИ-кода: ошибка, исправленная в одном клоне, молча выживает во всех остальных, поэтому идентичные дефекты повторяются и каждый требует избыточного исправления. Дублирование превращает одну ошибку в N ошибок.
  • Сопровождаемость и нагрузка на ревью. Каждое изменение поведения нужно найти и отредактировать во многих местах; диффы раздуваются почти идентичными блоками, которые ревьюерам приходится сравнивать построчно, чтобы убедиться, что они действительно идентичны (а не тонко и опасно различны).
  • Поверхность атаки. Уязвимый паттерн (отсутствующая проверка прав, неэкранированный ввод) распространяется в каждую копию, а позднее укрепляющее исправление может пропустить клоны — дублирование умножает поверхность для заплаток.

##Treatment

Тактики ревью и промптинга

  • Скажите модели, куда смотреть в первую очередь: "Прежде чем писать новый код, найди в репозитории существующую вспомогательную функцию, которая это делает, и переиспользуй её; не дублируй". Вставьте нужный модуль, чтобы он был в контексте.
  • Явно требуйте консолидации: "Если два блока различаются лишь параметрами, извлеки одну параметризованную функцию (Extract Function) и вызывай её из обоих".
  • Заставьте её запускать инструменты: "Запусти линтер / jscpd / проверку дублирования и устрани любой клон, о котором они сообщат, прежде чем возвращать дифф". Агенты хорошо самокорректируются, когда им дан детерминированный сигнал.
  • На ревью относитесь к блоку, похожему на вставленный, как к поводу для grep: найдите одну отличительную строку; если она уже существует, верните блок на извлечение.

Рефакторинг — Extract Function (Извлечение функции) + параметризация

// после: один источник истины, вызывается из обоих
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}`);

Теперь исправление обработки ошибок или разбора ответа вносится один раз. Там, где дубликаты различаются шагом, а не значением, предпочтите Form Template Method (Формирование шаблонного метода) или передавайте варьирующийся шаг как колбэк. Канонические приёмы здесь — Extract Function (Извлечение функции), Extract Variable/Constant (Извлечение переменной/константы, для перепечатанных литералов) и Pull Up Method (Подъём метода), когда клоны живут в дочерних классах — все стандартные лекарства от Duplicate Code (Дублирующийся код).

Защитный барьер, а не только уборка: встройте детектор клонов в CI с низким порогом, чтобы следующий сгенерированный ИИ дубликат проваливал сборку, а не вливался. Сочетайте это с однострочным внутренним правилом в CLAUDE.md/AGENTS.md ("переиспользуй существующие вспомогательные функции; никогда не вставляй почти-дубликат"), чтобы это ограничение ехало в контексте модели на каждом ходу.

##Detected by