# AI-slop в коде: систематический подход к ревью AI-сгенерированного кода

> 4 паттерна AI-slop с реальными примерами из production, 10-пунктный чеклист и инструменты автоматизации для ревью AI-сгенерированного кода.
> Author: Roman Belov · Published: 2026-03-09 · Source: https://futurecraft.pro/ru/blog/ai-slop-code-review-methodology/

Я пишу мобильное приложение на Flutter, бэкенд на Supabase Edge Functions, и активно использую Claude Code для генерации кода. Работает хорошо, но есть нюанс: AI-код проходит тесты и компилируется, а через пару месяцев ты понимаешь, что в кодовой базе накопился слой кода, который никто бы так не написал вручную.

Этот слой я для себя называю AI-slop. Ниже - 4 конкретных паттерна, которые я научился распознавать на ревью, чеклист на 10 пунктов и несколько способов автоматизировать отлов.

## Почему AI вообще генерирует slop

Тут три вещи работают одновременно.

Во-первых, у модели нет проектного контекста. Когда просишь написать функцию, она видит промпт и несколько файлов. Она не знает, что у тебя уже есть `HttpClientService` в `core/services/`, что ошибки обрабатываются через `Either<Failure, T>` из dartz, что DI настроен через `get_it`. Пишет код как для нового проекта.

Во-вторых, модель оптимизирует на «компилируется и проходит тесты», а не на «удобно жить с этим через полгода». Код, который дублирует существующую логику или нарушает архитектурные соглашения, для неё такой же успешный, как и чистый.

В-третьих, каждый промпт - чистый лист. Даже с большим контекстным окном модель не помнит, что три недели назад ты решил подход X заменить на подход Y.

## Паттерн 1: Happy path only

Самый частый. AI пишет логику основного сценария и останавливается. Таймауты, ошибки сети, пустые ответы, гонки состояний - либо отсутствуют, либо обработаны формально.

**Пример из реального кода** (упрощён):

```dart
// AI написал так
Future<List<Trip>> fetchTrips(String userId) async {
  final response = await supabase
    .from('trips')
    .select('*')
    .eq('user_id', userId);

  return (response as List)
    .map((json) => Trip.fromJson(json))
    .toList();
}
```

Код компилируется, тест с моком проходит. Что пропущено:

- `response` может быть `null` если RLS не пустил запрос
- `Trip.fromJson` бросит исключение при неожиданной схеме
- Нет обработки PostgrestException
- Нет таймаута
- Нет разграничения между «пользователь без поездок» и «ошибка запроса»

**Как должно выглядеть:** без магии, просто явная обработка:

```dart
Future<Either<Failure, List<Trip>>> fetchTrips(String userId) async {
  try {
    final response = await supabase
      .from('trips')
      .select('*')
      .eq('user_id', userId)
      .timeout(const Duration(seconds: 10));

    final list = response as List? ?? [];
    return Right(list.map((json) => Trip.fromJson(json as Map<String, dynamic>)).toList());
  } on TimeoutException {
    return Left(TimeoutFailure());
  } on PostgrestException catch (e) {
    return Left(ServerFailure(message: e.message, code: e.code));
  } catch (e) {
    return Left(ServerFailure(message: e.toString()));
  }
}
```

**Что искать на ревью:** функции, которые возвращают `Future<T>` вместо `Future<Either<Failure, T>>`. Блоки `try/catch` с пустым `catch (e)` или `catch (e) { print(e); }`. Приведения типов без проверки (`as List` вместо `as List?`).

## Паттерн 2: Архитектурный дрейф

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

Мне нужна была функция для форматирования дат. AI написал:

```dart
// В features/trip/presentation/widgets/trip_card.dart
class DateFormatter {
  static String format(DateTime date) {
    final day = date.day.toString().padLeft(2, '0');
    final month = date.month.toString().padLeft(2, '0');
    return '$day.$month.${date.year}';
  }
}
```

Проблема: в `core/utils/date_utils.dart` уже было:

```dart
extension DateTimeExtensions on DateTime {
  String toDisplayFormat() => '${day.toString().padLeft(2, '0')}.${month.toString().padLeft(2, '0')}.$year';
}
```

Теперь в кодовой базе два разных форматтера дат. В одних виджетах `DateFormatter.format(date)`, в других `date.toDisplayFormat()`. Через месяц приходит дизайнер и говорит «покажите дату в новом формате», и ты обнаруживаешь, что нужно менять в двух местах.

Серьёзнее, когда дрейф касается инфраструктуры:

```typescript
// AI написал в новой Edge Function
const supabase = createClient(
  Deno.env.get('SUPABASE_URL')!,
  Deno.env.get('SUPABASE_ANON_KEY')!
);
```

А в `_shared/supabase-client.ts` уже был:

```typescript
export function createSupabaseClient(req: Request) {
  return createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_ANON_KEY')!,
    {
      global: { headers: { Authorization: req.headers.get('Authorization')! } },
      auth: { persistSession: false }
    }
  );
}
```

Новая функция работает, но пропускает `Authorization` заголовок, то есть выполняется с правами anon, игнорируя JWT пользователя. RLS не сработает. Баг обнаружится не на тестах, а когда пользователь увидит чужие данные или не увидит свои.

**Что искать на ревью:** новые классы/функции, которые дублируют логику из `core/`, `_shared/`, `utils/`. Прямые `import 'package:http/http.dart'` там, где должен быть централизованный клиент. Инициализация SDK (Supabase, Mapbox, HTTP-клиента) внутри feature-кода вместо DI.

## Паттерн 3: Избыточная абстракция

AI любит абстракции. В обучающих данных абстрактные классы, интерфейсы и фабрики встречаются в «хорошем коде», и модель их воспроизводит не задумываясь, нужны ли они тут.

Задача была простая - загрузить фото для POI и показать его. AI предложил:

```dart
abstract class ImageLoader {
  Future<Uint8List?> load(String url);
}

class NetworkImageLoader implements ImageLoader {
  @override
  Future<Uint8List?> load(String url) async {
    // ...
  }
}

class CachedImageLoader implements ImageLoader {
  final ImageLoader delegate;
  final Map<String, Uint8List> _cache = {};

  CachedImageLoader({required this.delegate});

  @override
  Future<Uint8List?> load(String url) async {
    if (_cache.containsKey(url)) return _cache[url];
    final result = await delegate.load(url);
    if (result != null) _cache[url] = result;
    return result;
  }
}

class ImageLoaderFactory {
  static ImageLoader create({bool cached = true}) {
    final loader = NetworkImageLoader();
    return cached ? CachedImageLoader(delegate: loader) : loader;
  }
}
```

Реальная задача решается одной строкой:

```dart
CachedNetworkImage(imageUrl: photoUrl)
```

Потому что `cached_network_image` уже в `pubspec.yaml`. Уже используется в 12 других виджетах. AI не знал об этом. Он решил задачу в вакууме.

4 класса вместо 1 строки. Каждый из этих классов нужно тестировать, поддерживать, объяснять новым разработчикам.

**Что искать:** цепочки наследования для задач, которые решаются стандартной библиотекой или уже используемым пакетом. Паттерны Factory/Strategy/Builder там, где достаточно функции. `abstract class` с единственной реализацией и без видимой причины для полиморфизма.

## Паттерн 4: Уверенная галлюцинация

Внешне код выглядит нормально, но AI придумал вызов API, метод SDK или конфигурацию, которых не существует. Или они были в старой версии.

**Пример из TypeScript Edge Functions:**

```typescript
// AI написал для получения геолокации по IP
const location = await supabase.functions.geo.lookup(clientIp);
```

`supabase.functions.geo` не существует в Supabase JS SDK. Никогда не существовало. Строго типизированный клиент поймает это на компиляции — опасность в динамических вызовах: импорт без типов (обычное дело в Deno Edge Functions), `as any` или обращение через optional chaining. Тогда код спокойно собирается и падает уже в production.

**Пример из Flutter:**

```dart
// AI написал для кастомного маркера на Mapbox
final manager = await mapboxMap.annotations.createPointAnnotationManager();
final marker = await manager.create(
  PointAnnotationOptions(
    geometry: Point(coordinates: Position(lng, lat)),
    iconImage: 'custom-marker',
    iconAnchor: IconAnchor.bottom,
  ),
);
```

`IconAnchor.bottom` выглядит правдоподобно: значения enum в Dart принято писать lowerCamelCase. Но в `mapbox_maps_flutter` (в том числе в `^2.5.0` из проекта) перечисления сгенерированы из style spec и пишутся капсом — правильно `IconAnchor.BOTTOM`. Разница в регистре. Ошибка компиляции: AI воспроизвёл конвенцию языка, а не реальный API установленного SDK.

Бывает ещё хуже - галлюцинация поведения:

```typescript
// AI написал: "при ошибке rate limit Supabase автоматически ретраит через 5 секунд"
// Это неправда. Supabase не делает автоматических ретраев.
// AI просто уверенно соврал в комментарии.
```

**Что искать:** вызовы методов, которые вы не видели раньше - проверить через документацию и `cmd+click`. Комментарии, описывающие поведение библиотеки («автоматически», «по умолчанию», «встроенная поддержка») - верифицировать. Enum-значения и константы - свериться с версией SDK в `pubspec.yaml` или `package.json`.

## Систематический чеклист для ревью AI-кода

Можно распечатать или держать открытым при ревью.

```
CODE REVIEW AI-КОДА - ЧЕКЛИСТ

КОНТЕКСТ
[] 1. Использует ли код существующие утилиты из core/ / _shared/?
     Grep по ключевым словам задачи перед анализом нового кода.

[] 2. Следует ли код архитектурным паттернам проекта?
     (DI, Either<Failure,T>, именование, слои)

[] 3. Импортирует ли код пакеты, которые уже есть в pubspec.yaml?
     Или создаёт новую реализацию того, что уже есть.

КОРРЕКТНОСТЬ
[] 4. Обработаны ли все пути ошибок?
     Network timeout, null response, некорректная схема данных.

[] 5. Проверены ли edge cases?
     Пустой список, отрицательные числа, одновременные запросы.

[] 6. Верифицированы ли API-вызовы и методы SDK?
     Метод существует? В нужной версии? С правильными параметрами?

КАЧЕСТВО
[] 7. Оправданы ли все абстракции?
     Каждый интерфейс/абстрактный класс имеет >= 2 реализаций или явную причину.

[] 8. Нет ли дублирования логики?
     Сравни с существующим кодом, особенно форматирование, валидацию, сетевые запросы.

[] 9. Правдивы ли комментарии?
     Особенно те, что описывают поведение библиотек или внешних сервисов.

БЕЗОПАСНОСТЬ
[] 10. Не обходит ли код auth/RLS?
      Прямые DB-запросы без user JWT, хардкод credentials, отсутствие проверки прав.
```

## Автоматизация: поймать slop до code review

Часть проблем можно отловить автоматически, ещё до ревью.

### Git pre-commit hook

Простой bash-хук, который ловит самые грубые признаки:

```bash
#!/bin/bash
# .git/hooks/pre-commit

STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(dart|ts)$')

if [ -z "$STAGED_FILES" ]; then
  exit 0
fi

ERRORS=0

for FILE in $STAGED_FILES; do
  # Прямая инициализация Supabase без shared client
  if grep -n "createClient(Deno.env" "$FILE" | grep -v "_shared" > /dev/null 2>&1; then
    echo "Warning: $FILE: прямой createClient вне _shared/"
    ERRORS=$((ERRORS + 1))
  fi

  # Flutter: прямые HTTP запросы без централизованного клиента
  if grep -n "import 'package:http/http.dart'" "$FILE" > /dev/null 2>&1; then
    echo "Warning: $FILE: прямой import http, используй DioClient из core/"
    ERRORS=$((ERRORS + 1))
  fi

  # Пустые catch блоки
  if grep -n "catch (e) {}" "$FILE" > /dev/null 2>&1; then
    echo "Warning: $FILE: пустой catch блок"
    ERRORS=$((ERRORS + 1))
  fi
done

if [ $ERRORS -gt 0 ]; then
  echo ""
  echo "Found $ERRORS potential AI-slop patterns."
  echo "   Check the files before committing."
  exit 1
fi
```

### Custom lint rules для Dart

`package:custom_lint` позволяет написать правила, специфичные для твоего проекта:

```dart
// tool/lints/lib/avoid_direct_supabase_init.dart
class AvoidDirectSupabaseInit extends DartLintRule {
  const AvoidDirectSupabaseInit() : super(code: _code);

  static const _code = LintCode(
    name: 'avoid_direct_supabase_init',
    problemMessage: 'Use SupabaseClientService from core/services instead of direct initialization',
  );

  @override
  void run(CustomLintResolver resolver, ErrorReporter reporter, CustomLintContext context) {
    context.registry.addMethodInvocation((node) {
      if (node.methodName.name == 'initialize' &&
          node.target?.toString() == 'Supabase') {
        reporter.reportErrorForNode(_code, node);
      }
    });
  }
}
```

Это поймает `Supabase.initialize(...)` в feature-коде, где должен использоваться DI.

### Проверка зависимостей в CI

Простой скрипт для проверки что новые файлы не создают дублирующие утилиты:

```bash
#!/bin/bash
# scripts/check_duplicates.sh

# Check that new dart files don't reimplement what already exists in core/utils/
NEW_FILES=$(git diff origin/main...HEAD --name-only --diff-filter=A | grep "\.dart$")

for FILE in $NEW_FILES; do
  # Look for date formatters
  if grep -l "DateFormat\|DateTime\|toDisplayFormat\|formatDate" "$FILE" > /dev/null 2>&1; then
    EXISTING=$(grep -rl "DateFormat\|toDisplayFormat\|formatDate" lib/core/ 2>/dev/null)
    if [ -n "$EXISTING" ]; then
      echo "Warning: $FILE creates a date formatter, but core/ already has:"
      echo "   $EXISTING"
    fi
  fi
done
```

## Как с этим жить

AI-агенты становятся умнее с каждым обновлением и уже неплохо учитывают контекст проекта. Но пока что slop всё ещё появляется, особенно в крупных кодовых базах со своими соглашениями, нестандартными паттернами и историей решений.

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

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

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

---

*Нужна помощь с настройкой ревью AI-сгенерированного кода? Я помогаю стартапам внедрять AI-решения и строить продукты — [belov.works](https://belov.works).*
