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

Анатомия ИИ-оверинжиниринга: как нейросети создают мертвый код и бетонируют его тестами

В современной индустрии программной инженерии сейчас царит атмосфера всеобщей эйфории. Менеджеры радостно рапортуют о росте скорости закрытия продуктовых задач в три раза, разработчики хвастаются в социальных сетях, что за один вечер собрали рабочий прототип сервиса на Claude Code или Cursor, а аналитики всерьез предрекают скорую смерть классического программирования. Кажется: нажал клавишу Tab — и готовая функция уже лежит в репозитории вместе с типами, интерфейсами, юнит-тестами и пространным описанием в pull request.

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

Эту острейшую проблему детально препарировал признанный эксперт по архитектуре и создатель образовательного проекта HowProgrammingWorks Тимур Шемсединов в своем фундаментальном видеодокладе Тимура Шемсединова. Запись вызвала колоссальный резонанс в профессиональном сообществе, потому что открыто затронула то, о чем многие ведущие инженеры пока боятся говорить вслух: генеративный ИИ без жесткого архитектурного надзора и фильтрации способен превратить развивающийся проект в радиоактивные руины всего за несколько месяцев интенсивной разработки.

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

Фабрика фабрик: почему языковые модели обожают оверинжиниринг

Чтобы понять глубинные корни этой проблемы, нужно вспомнить, на каких именно материалах обучались современные большие языковые модели (LLM). В их колоссальных обучающих корпусах лежат миллионы строк кода корпоративных enterprise-систем на Java и C# начала 2010-х годов, классические учебники по паттернам объектно-ориентированного проектирования Банды четырех (GoF) и бесконечные учебные репозитории, создатели которых пытались искусственно втиснуть Abstract Factory, Visitor, Bridge и Strategy в простейший консольный калькулятор.

Когда разработчик дает нейросети вполне жизненную задачу: «Напиши функцию валидации email-адреса и сохранения нового пользователя в базу данных», модель действует не как прагматичный инженер, а по принципу максимизации статистической правдоподобности тяжелого корпоративного кода. Вместо того чтобы написать три строчки чистой и понятной бизнес-логики на TypeScript, Go или Python, агент мгновенно генерирует целую иерархию файлов:

  • Абстрактный интерфейс IEmailValidatorStrategy;
  • Класс RegexEmailValidatorServiceImpl;
  • Фабрику создания фабрик EmailServiceFactoryProvider;
  • Адаптер интеграции NotificationDispatchAdapterBridge;
  • DTO-обертку UserEmailPayloadValueObject.

Смотри, что происходит на самом деле. Исходная прикладная задача легко решалась обычной чистой функцией из десяти строк. Но ИИ породил пять отдельных файлов, полдюжины типов и три уровня косвенности. Зачем? Потому что в обучающей выборке энтерпрайзный код с десятком интерфейсов выглядит «солидно», «масштабируемо» и «профессионально». Модель оптимизирует не простоту поддержки, а формальную сложность текста.

Давай посмотрим на наглядный пример такого ИИ-оверинжиниринга на TypeScript:

// То, что сгенерировал ИИ на простой запрос сохранения пользователя:
export interface IUserRepositoryPort {
  save(user: UserEntity): Promise<UserResultDto>;
}

export interface IUserDataMapper<TSource, TTarget> {
  mapToDomain(source: TSource): TTarget;
  mapToPersistence(target: TTarget): TSource;
}

export class PostgresUserDataMapperImpl implements IUserDataMapper<RawDbRecord, UserEntity> {
  mapToDomain(source: RawDbRecord): UserEntity {
    return new UserEntity(source.id, source.email_address, source.created_timestamp);
  }
  mapToPersistence(target: UserEntity): RawDbRecord {
    return { id: target.id, email_address: target.email, created_timestamp: target.createdAt };
  }
}

export class UserPersistenceOrchestratorService {
  constructor(
    private readonly repositoryPort: IUserRepositoryPort,
    private readonly mapperService: IUserDataMapper<RawDbRecord, UserEntity>
  ) {}

  async executeUserRegistrationFlow(payload: RegisterUserCommandDto): Promise<UserResponseEnvelopeDto> {
    // Вся реальная логика утонула под слоями вызовов мапперов
    const domainEntity = this.mapperService.mapToDomain(payload.rawRecord);
    const saveResult = await this.repositoryPort.save(domainEntity);
    return new UserResponseEnvelopeDto(saveResult);
  }
}

Вся эта монументальная пирамида классов не несет абсолютно никакой бизнес-ценности. Проект никогда в жизни не будет менять PostgreSQL на Oracle на лету посреди ночи, у сервиса существует ровно одна реализация репозитория, а маппер просто перекладывает поля из одного объекта в другой с переименованием ключей из snake_case в camelCase. Но в кодовой базе уже появилось 40 строк бойлерплейта, которые теперь нужно читать, компилировать, индексировать и мысленно удерживать в голове.

Ловушка синтетических тестов: как зацементировать мертвый код

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

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

Разработчик видит, что агент нагенерировал сложную структуру классов, и дает следующую команду: «А теперь покрой все эти классы юнит-тестами со 100% покрытием кода». Агент с радостным энтузиазмом выполняет указание и генерирует двести строк тестов на базе Jest, Vitest или pytest.

Но как тестировать пустые интерфейсы и искусственные мапперы? Модель начинает мокать абсолютно всё подряд:

// Синтетический юнит-тест от ИИ, который тестирует сам себя:
describe('UserPersistenceOrchestratorService', () => {
  it('должен вызывать маппер и сохранять пользователя через порт', async () => {
    // Создаем бесконечные моки на каждый чих
    const mockMapper: jest.Mocked<IUserDataMapper<RawDbRecord, UserEntity>> = {
      mapToDomain: jest.fn(),
      mapToPersistence: jest.fn(),
    };
    const mockRepo: jest.Mocked<IUserRepositoryPort> = {
      save: jest.fn(),
    };

    const dummyUser = new UserEntity('u-1', 'test@example.com', new Date());
    mockMapper.mapToDomain.mockReturnValue(dummyUser);
    mockRepo.save.mockResolvedValue(new UserResultDto('u-1', 'SUCCESS'));

    const orchestrator = new UserPersistenceOrchestratorService(mockRepo, mockMapper);
    const result = await orchestrator.executeUserRegistrationFlow({ rawRecord: {} as any });

    // Проверяем, что мок вызвал мок с параметрами мока:
    expect(mockMapper.mapToDomain).toHaveBeenCalledTimes(1);
    expect(mockRepo.save).toHaveBeenCalledWith(dummyUser);
    expect(result.status).toBe('SUCCESS');
  });
});

Посмотри внимательно на этот тестовый сценарий. Что он реально проверяет? Поведение реальной системы? Корректность записи в базу данных? Работу бизнес-правил? Нет! Этот тест проверяет исключительно то, что метод A внутри вызвал мок B, который передал аргумент моку C. Если в реальной базе данных отвалится таблица, слетит схема или отключится сеть, этот юнит-тест продолжит бодро зеленеть в CI-пайплайне, создавая ложное чувство полной защищенности.

Но самое страшное происходит потом. Когда через три месяца живой инженер открывает этот файл и справедливо решает: «Какой кошмар, зачем здесь пять классов, я перепишу это в одну простую и понятную функцию!», он запускает тестовый набор — и получает пятьдесят красных ошибок!

Тесты намертво завязаны на внутреннюю реализацию фиктивных классов и сигнатуры моков. Любая попытка удалить бесполезный интерфейс ломает тестовый сьют. Инженер пугается: «Ого, тут столько проверок упало, видимо, это критически важная архитектура, лучше не трогать от греха подальше». И мертвый код остается в системе навсегда. ИИ буквально зацементировал собственные архитектурные галлюцинации синтетическими тестами.

Симбиоз мертвого кода и паразитные циклические связи

Тимур Шемсединов в своем докладе подробно разбирает следующий этап деградации: возникновение циклических связей между модулями мертвого кода.

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

Через несколько спринтов кодовая база наполняется паутиной дублирующих вызовов:

  • Модуль A импортирует тип из модуля B;
  • Модуль B импортирует вспомогательный интерфейс из модуля C;
  • Модуль C для внутренних нужд импортирует хелпер обратно из модуля A.

Возникает циклический импорт (circular dependency). Для современных сборщиков вроде Vite, Rollup или Webpack это тяжелейшая нагрузка: граф модулей безнадежно запутывается, механизмы оптимизации размера бандла (tree-shaking) перестают работать, а размер клиентского кода раздувается в разы. Ни один автоматический статический анализатор не может безопасно удалить модуль B, потому что на него формально ссылаются типы из модуля C, хотя в реальности ни одна строчка бизнес-логики через этот путь никогда не выполняется!

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

Скрытая цена владения: почему страдает производительность команды

Менеджеры часто возражают: «Ну и пусть код раздут, какая разница, ведь диск дешевый, а нейросеть пишет быстро?»

Это опаснейшее заблуждение. Каждый лишний класс, каждый синтетический файл и фиктивный интерфейс создают постоянную и растущую стоимость владения (Total Cost of Ownership):

  1. Нагрузка на языковой сервер (LSP): Языковые серверы TypeScript, Go gopls или Rust Analyzer вынуждены непрерывно парсить, типизировать и удерживать в оперативной памяти тысячи фиктивных символов. Твоя IDE начинает ощутимо подтормаживать, подсказки автокомплита открываются с задержкой в секунду, а индексация проекта намертво подвешивает процессор ноутбука.
  2. Замедление CI/CD пайплайнов: Прогон сотен бесполезных тестов-самострелов увеличивает время сборки с 2 до 25 минут. Команда тратит часы драгоценного рабочего времени на ожидание зеленого пайплайна.
  3. Когнитивный паралич инженера: Человеческий мозг не может удерживать бесконечное количество сущностей. Когда новый разработчик приходит на проект и видит семь уровней абстракции вокруг одной таблицы, он тратит недели на то, чтобы просто понять, где в этой системе реально исполняются бизнес-правила.

Как должен выглядеть здоровый код: радикальный рефакторинг

Давай посмотрим, как тот же самый функционал сохранения пользователя выглядит в исполнении опытного инженера, исповедующего фундаментальные принципы YAGNI (You Aren't Gonna Need It) и KISS (Keep It Simple, Stupid):

// Здоровая, читаемая и надежная реализация без грамма лишнего жира:
import { sql } from '@/database/client';

export interface UserRegistrationInput {
  email: string;
  fullName: string;
}

export interface UserRecord {
  id: string;
  email: string;
  fullName: string;
  createdAt: Date;
}

// Простая, чистая асинхронная функция с прямой валидацией и запросом
export async function registerUser(input: UserRegistrationInput): Promise<UserRecord> {
  if (!input.email.includes('@')) {
    throw new Error('Некорректный формат адреса электронной почты');
  }

  const [createdUser] = await sql<UserRecord[]>`
    INSERT INTO users (email, full_name, created_at)
    VALUES (${input.email.toLowerCase()}, ${input.fullName.trim()}, NOW())
    RETURNING id, email, full_name as "fullName", created_at as "createdAt"
  `;

  return createdUser;
}

Почувствуй колоссальную разницу:

  • Вместо пяти файлов и сложной паутины моков — один понятный модуль.
  • Логика абсолютно прозрачна: входные данные, базовая проверка, параметризованный SQL-запрос, возврат типизированного результата.
  • Такой код тестируется одним простым интеграционным тестом на реальной тестовой базе данных в Docker за 20 миллисекунд без единого мока! Если логика базы изменится, интеграционный тест честно упадет и покажет ошибку.

Инженерный регламент: как спасти проект от деградации

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

  1. Правило трех реализаций: Категорически запрещено создавать абстрактный интерфейс или фабрику, если в системе нет минимум трех принципиально разных реальных реализаций этого интерфейса. Если реализация одна — пишется конкретный класс или простая функция.
  2. Разделение контекстов при генерации: Никогда не проси одну и ту же модель генерировать и бизнес-логику, и тесты к ней в одном диалоге. Тесты должны писать другие люди или независимая модель, получающая на вход только спецификацию требований, а не внутренности сгенерированного кода.
  3. Лимит на размер пулл-реквеста: Если ИИ-ассистент сгенерировал PR на 1500 строк изменений, его запрещено принимать вслепую. Разработчик обязан разбить задачу на микро-шаги по 100–200 строк и лично проверить каждую строчку.
  4. Регулярные дни археологии (Dead Code Archaeology): Раз в месяц проект должен прогоняться через специализированные анализаторы мертвого кода (Knip, ts-prune, depcheck). Все неиспользуемые экспорты и промежуточные мапперы должны безжалостно вырезаться.
  5. Приоритет интеграционным тестам: Сокращайте долю мок-тестов в пользу сквозных и компонентных проверок на реальных изолированных базах (Testcontainers). Тест должен проверять результат работы системы, а не последовательность вызовов внутренних методов.

Главный вывод из этой истории прост и отрезвляющ: ценность инженера в эпоху ИИ заключается не в том, с какой скоростью он умеет нажимать кнопку генерации кода. Настоящее мастерство архитектора сегодня — это способность сказать коду твердое «нет» и защитить проект от лишних сущностей.