AI-slop в коде: систематический подход к ревью AI-сгенерированного кода
Что такое AI slop в коде?
AI slop — это код, который компилируется и проходит тесты, но ни один разработчик так бы не написал: только happy path без обработки ошибок, утилиты, дублирующие существующий код проекта, несогласованная обработка ошибок и неверные абстракции. Накапливается, когда AI генерирует код без полного контекста проекта, оптимизируя под 'компилируется и проходит тесты', а не под архитектурное соответствие.
TL;DR
- -AI slop: код, который компилируется и проходит тесты, но накапливает архитектурный долг — 4 паттерна: только happy path, фантомные утилиты, несогласованная обработка ошибок, неверные абстракции
- -У AI нет контекста проекта: он не знает о вашем HttpClientService, паттерне Either<Failure,T> или DI через get_it
- -Проверяйте точки интеграции, а не внутреннюю логику — AI обычно правильно пишет логику, но промахивается с паттернами проекта
- -10-пунктный чеклист: повторное использование утилит, согласованность обработки ошибок, правильные импорты, отсутствие лишних зависимостей
- -Частичная автоматизация: pre-commit хуки ловят дублирование кода; LLM-as-Judge оценивает соответствие конвенциям проекта
Я пишу мобильное приложение на 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 пишет логику основного сценария и останавливается. Таймауты, ошибки сети, пустые ответы, гонки состояний - либо отсутствуют, либо обработаны формально.
Пример из реального кода (упрощён):
// 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
- Нет таймаута
- Нет разграничения между «пользователь без поездок» и «ошибка запроса»
Как должно выглядеть: без магии, просто явная обработка:
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 написал:
// В 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 уже было:
extension DateTimeExtensions on DateTime {
String toDisplayFormat() => '${day.toString().padLeft(2, '0')}.${month.toString().padLeft(2, '0')}.$year';
}
Теперь в кодовой базе два разных форматтера дат. В одних виджетах DateFormatter.format(date), в других date.toDisplayFormat(). Через месяц приходит дизайнер и говорит «покажите дату в новом формате», и ты обнаруживаешь, что нужно менять в двух местах.
Серьёзнее, когда дрейф касается инфраструктуры:
// AI написал в новой Edge Function
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_ANON_KEY')!
);
А в _shared/supabase-client.ts уже был:
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 предложил:
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;
}
}
Реальная задача решается одной строкой:
CachedNetworkImage(imageUrl: photoUrl)
Потому что cached_network_image уже в pubspec.yaml. Уже используется в 12 других виджетах. AI не знал об этом. Он решил задачу в вакууме.
4 класса вместо 1 строки. Каждый из этих классов нужно тестировать, поддерживать, объяснять новым разработчикам.
Что искать: цепочки наследования для задач, которые решаются стандартной библиотекой или уже используемым пакетом. Паттерны Factory/Strategy/Builder там, где достаточно функции. abstract class с единственной реализацией и без видимой причины для полиморфизма.
Паттерн 4: Уверенная галлюцинация
Внешне код выглядит нормально, но AI придумал вызов API, метод SDK или конфигурацию, которых не существует. Или они были в старой версии.
Пример из TypeScript Edge Functions:
// 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:
// 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.
Бывает ещё хуже - галлюцинация поведения:
// 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-хук, который ловит самые грубые признаки:
#!/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 позволяет написать правила, специфичные для твоего проекта:
// 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
Простой скрипт для проверки что новые файлы не создают дублирующие утилиты:
#!/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.
Часто задаваемые вопросы
Что такое AI slop в коде?
Как обнаружить проблемы AI-сгенерированного кода на ревью?
Можно ли автоматизировать ревью AI-кода?
Как AI slop накапливается по-разному в сольных проектах и командных кодовых базах — влияет ли размер команды на стратегию обнаружения?
Есть ли порог накопления AI slop, когда полный аудит выгоднее инкрементного ревью?
core/, _shared/ и utils/ — с каталогизацией существующих утилит, которые AI продолжает дублировать — занимает 4–8 часов, но окупается снижением review overhead за 2–3 спринта. Результат аудита — референсный документ, который вы подаёте AI в начале каждой сессии: «существующие утилиты: [список с путями к файлам и назначением]».