---
title: "Дрейф соглашений"
type: "ai-smell"
slug: "convention-drift"
url: "http://localhost:3000/ru/ai-smells/convention-drift.md"
category: "Сопровождение"
description: "Сгенерированный ИИ код, который тихо игнорирует устоявшиеся соглашения репозитория — переизобретает вспомогательные функции, выбирает не ту библиотеку и использует не принятое в проекте именование и обработку ошибок — из-за чего кодовая база дрейфует к обобщённому, усреднённому по интернету стилю."
---
# Дрейф соглашений

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

## 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 году, при этом скопированные строки впервые обогнали перемещённые (отрефакторенные).

```ts
// Внутренний стиль (уже в репозитории)
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_ (Извлечение функции), если нужной абстракции ещё нет.

```ts
// ДО — дрейф, переизобретает клиент + логику дат, захардкоженный 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)` — Обнаружение копипасты (https://github.com/kucherenko/jscpd)
- **PMD CPD** `CPD duplicated code blocks` — Детектор копипасты (https://pmd.github.io/latest/pmd_userdocs_cpd.html)
- **ESLint** `no-restricted-imports` — no-restricted-imports (https://eslint.org/docs/latest/rules/no-restricted-imports)
- **typescript-eslint** `@typescript-eslint/naming-convention` — naming-convention (https://typescript-eslint.io/rules/naming-convention/)
- **SonarQube** `Source files should not have any duplicated blocks` — Дублированные блоки (https://docs.sonarsource.com/sonarqube-server/latest/user-guide/code-metrics/metrics-definition/)
