Дрейф соглашений.
Сгенерированный ИИ код, который тихо игнорирует устоявшиеся соглашения репозитория — переизобретает вспомогательные функции, выбирает не ту библиотеку и использует не принятое в проекте именование и обработку ошибок — из-за чего кодовая база дрейфует к обобщённому, усреднённому по интернету стилю.
##Signs and Symptoms
Ревьюер распознаёт дрейф соглашений, когда изменение от ИИ работает, но не выглядит как часть кодовой базы. Оно тянется к глобально усреднённому паттерну вместо локального:
- Новая вспомогательная функция пишется встроенно, хотя проверенная в бою утилита уже существует (
utils/date.ts,lib/apiClient, общий типResult). - Появляется библиотека или API, отличные от стандарта репозитория (
axiosв приложении, стандартизированном на обёртке надfetch;momentтам, где репозиторий используетdate-fns). - Именование, раскладка файлов и стиль обработки ошибок расходятся —
camelCaseтам, где модуль наsnake_case, голыйthrow new Error("...")там, где всё остальное возвращает типизированную ошибку, самодельное логирование вtry/catchвместо общего логгера. - Каждое новое требование получает свой свежеслепленный, узкоспециализированный блок вместо переиспользования абстракции ("Сверхспецификация" от Ox Security, встречается в 80–90% ИИ-кода).
- Симптоматическая сигнатура: дублированные блоки множатся. GitClear зафиксировал восьмикратный скачок числа блоков-клонов в 5+ строк в 2024 году, при этом скопированные строки впервые обогнали перемещённые (отрефакторенные).
// Внутренний стиль (уже в репозитории)
import { apiClient } from "@/lib/apiClient"; // оборачивает auth, ретраи, базовый URL
import { formatDate } from "@/utils/date"; // на всё приложение, с учётом локали
// Изменение от ИИ — отклоняется от обоих
import axios from "axios"; // не паттерн зависимостей проекта
async function getUser(id: string) {
try {
const res = await axios.get(`https://api.example.com/users/${id}`); // захардкоженный базовый URL
return { ...res.data, joined: new Date(res.data.joined).toLocaleDateString() }; // переизобретает formatDate
} catch (e) {
console.log("error", e); // не общий логгер; проглатывает ошибку
}
}
Признак — согласованность, а не корректность: три ревьюера находят каждый свой "неправильный, но работающий" выбор, и ни один из них не совпадает с соседним файлом.
##Reasons for the Problem
Почему модели это порождают
- Регрессия к среднему обучающей выборки. LLM обучены на огромном корпусе усреднённого интернет-кода, а не на вашем репозитории. Без подсказки они выдают статистически наиболее распространённую идиому (
axios,moment,console.log), а не вашу внутреннюю — ваши соглашения это крошечный сигнал вне распределения на фоне глобального среднего. - Самосогласованность следующего токена важнее охвата репозитория. Локально проще дописать самодостаточный блок (встроить форматтер дат), чем "знать", что
@/utils/dateсуществует, и импортировать идентификатор, который модель никогда не видела. Переиспользование требует контекста репозитория, которого у модели нет; переизобретение требует лишь текущего буфера. - Ограниченный / теряющий детали контекст репозитория. Каноническая вспомогательная функция, конфиг линтера и ADR, гласящий "используйте обёртку над fetch", обычно находятся вне промпта. Модель не может следовать соглашению, которое ей никогда не показывали. Как выразился Eno Reyes из Factory, значительная часть соглашений неявна — это паттерны, которые люди впитывают, читая кодовую базу, и которые агенты попросту никогда не видят.
- Подхалимство / буквальная фокусировка на задаче. На просьбу "добавь
getUser" модель делает ровно это и не вызовётся сказать "вообще-то у нас уже есть клиент для этого". Она оптимизирует выполнение поставленной задачи, а не вписывание в систему. "Фиксация на учебнике" (By-The-Book Fixation) от Ox Security (80–90% образцов) — та же сила: модель следует обобщённому учебниковому соглашению, а не оценивает собственное соглашение проекта. - Устаревание из-за порога обучения. Если репозиторий мигрировал на более новую библиотеку или паттерн после порога обучения модели, модель уверенно возвращает ту версию, на которой обучалась.
Почему это вредит
- Сопровождаемость и когнитивная нагрузка. Каждый отклонившийся выбор — ещё один способ делать одно и то же. Читателям приходится держать в голове N вариантов "как мы форматируем даты"; кодовая база теряет единый источник истины.
- Корректность при изменениях. Дублированная/переизобретённая логика молча расходится — ошибка, исправленная в общей вспомогательной функции, не исправлена в копии, созданной ИИ. GitClear напрямую связывает взрыв клонов с тем, что ИИ-ассистенты делают вставку по Tab дешевле переиспользования, тогда как доля рефакторинга в изменениях упала с 25% (2021) до менее 10% (2024).
- Безопасность. Переизобретённая валидация, аутентификация или построение запросов обходит укреплённый общий путь (захардкоженные базовые URL, проглоченные ошибки, самодельный строковый SQL). Совокупный эффект Ox называет "Армией джунов": быстро, функционально, без архитектурного суждения.
- Нагрузка на ревью и накопление техдолга. Дрейф не ловится тестами (код работает), поэтому он попадает в ревью или вообще никуда, накапливаясь в архитектурный запах "Разбросанная функциональность / Модульный мираж", где связанное поведение раздроблено по файлам без реальной связности.
##Treatment
Тактики промптинга / рабочего процесса
- Покажите соглашения. Разместите правила там, где агент их читает — файл
CLAUDE.md/AGENTS.md/правил, перечисляющий санкционированный клиент, логгер, тип ошибки, именование и "используй X, а не Y". Соглашениям, которых модель не видит, она следовать не может. - Укажите на канонический код. "Используй
@/lib/apiClientи@/utils/date; не добавляй новые HTTP-библиотеки. Повтори обработку ошибок изservices/orders.ts". Покажите идиому репозитория по принципу few-shot, вставив один образцовый файл. - Спрашивайте до того, как она пишет. "Какие существующие вспомогательные функции/абстракции это покрывают? Переиспользуй их; добавляй новый код, только если ни одна не подходит". Это заранее превращает переизобретение в переиспользование.
- Сделайте линтер вратарём. Требуйте, чтобы агент запускал форматтер, линтер и проверку типов и исправлял все находки до возврата результата. Совет Factory — систематизировать сигналы качества (lint/format/type-check/test), чтобы агент оптимизировал по ним, а не дрейфовал.
- Проверяйте на соответствие, а не только на работоспособность. Сравнивайте импорты на наличие несанкционированных библиотек; ищите grep-ом логику, которая должна была вызвать общую вспомогательную функцию; запускайте детектор копипасты на PR.
Рефакторинг — назовите классические приёмы: Remove Duplication (Устранение дублирования) / Replace Inline Code with Function Call (Замена встроенного кода вызовом функции), Substitute Algorithm (Замена алгоритма) и Extract Function (Извлечение функции), если нужной абстракции ещё нет.
// ДО — дрейф, переизобретает клиент + логику дат, захардкоженный URL, проглоченная ошибка
import axios from "axios";
async function getUser(id: string) {
try {
const res = await axios.get(`https://api.example.com/users/${id}`);
return { ...res.data, joined: new Date(res.data.joined).toLocaleDateString() };
} catch (e) {
console.log("error", e);
}
}
// ПОСЛЕ — переиспользует устоявшиеся соглашения
import { apiClient } from "@/lib/apiClient";
import { formatDate } from "@/utils/date";
async function getUser(id: string) {
const user = await apiClient.get<User>(`/users/${id}`); // базовый URL, auth, ретраи, типизированные ошибки обрабатываются здесь
return { ...user, joined: formatDate(user.joined) };
}
Если один и тот же отклонившийся блок появляется в нескольких PR, это сигнал, что соглашение необнаружимо — устраните первопричину, задокументировав его в файле правил и/или сделав доступным через единую очевидную точку входа, а не перепроверяя каждый случай.
##Detected by
- jscpd duplication threshold (min-lines / min-tokens) — Обнаружение копипасты
- PMD CPD CPD duplicated code blocks — Детектор копипасты
- ESLint no-restricted-imports — no-restricted-imports
- typescript-eslint @typescript-eslint/naming-convention — naming-convention
- SonarQube Source files should not have any duplicated blocks — Дублированные блоки