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. Никогда не существовало. Код скомпилируется (TypeScript типы не поймают runtime property access через optional chaining), упадёт только в production.
Пример из Flutter:
// AI написал для кастомного маркера на Mapbox
final marker = mapboxMap.annotations.createSymbolAnnotation(
SymbolAnnotationOptions(
geometry: Point(coordinates: Position(lng, lat)),
iconImage: 'custom-marker',
iconAnchor: IconAnchor.BOTTOM,
),
);
IconAnchor.BOTTOM - правильное название в документации на сайте. В версии SDK, которая установлена в проекте (mapbox_maps_flutter: ^2.5.0), перечисление называется IconAnchor.bottom. Разница в регистре. Ошибка компиляции, но AI не знал, какая версия 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
FAQ
Как AI slop накапливается по-разному в сольных проектах и командных кодовых базах — влияет ли размер команды на стратегию обнаружения?
В сольных проектах slop накапливается незаметно — нет ревью-гейта, и только автор видит код, нередко написанный по тому же малоконтекстному промпту. В командных кодовых базах slop чаще ловится на PR-ревью, но и распространяется активнее: каждый разработчик независимо генерирует код, не знающий о работе коллег на прошлой неделе. Для команд pre-commit хуки и CI-скрипты поиска дублей из этой статьи важнее, чем для сольной работы, где качество CLAUDE.md имеет несопоставимо больший эффект.
Есть ли порог накопления AI slop, когда полный аудит выгоднее инкрементного ревью?
Триггер — примерно когда 3 или более из 4 паттернов slop появляются в каждом значимом PR без срабатывания pre-commit хуков. В этом случае целевой аудит директорий core/, _shared/ и utils/ — с каталогизацией существующих утилит, которые AI продолжает дублировать — занимает 4–8 часов, но окупается снижением review overhead за 2–3 спринта. Результат аудита — референсный документ, который вы подаёте AI в начале каждой сессии: «существующие утилиты: [список с путями к файлам и назначением]».
Разные LLM генерируют разные виды slop, или 4 паттерна одинаковы для Claude, GPT-4o и Gemini?
Все 4 паттерна встречаются во всех основных моделях, но с разной частотой. Избыточная абстракция (паттерн 3) чаще у GPT-4o — он сильнее тяготеет к иерархиям классов. Happy-path-only код (паттерн 1) стабилен у всех моделей и коррелирует с конкретностью промпта: чем явнее требования к обработке ошибок, тем реже проявляется. Уверенные галлюцинации (паттерн 4) варьируются по домену: Gemini галлюцинирует меньше на хорошо задокументированных публичных API, но больше на нишевых версиях SDK. Мульти-агентное ревью (описано в связанной статье по AI code review) ловит слепые пятна конкретных моделей.
Как с этим жить
AI-агенты становятся умнее с каждым обновлением и уже неплохо учитывают контекст проекта. Но пока что slop всё ещё появляется, особенно в крупных кодовых базах со своими соглашениями, нестандартными паттернами и историей решений.
На практике это сдвигает фокус ревью. Логику AI обычно пишет правильно. Проблемы в том, как новый код ложится в существующую систему: какие зависимости использует, не дублирует ли то, что уже есть, правильно ли обрабатывает ошибки.
Что помогает мне: перед сложной задачей я явно даю Claude список файлов и паттернов, которым нужно следовать. На ревью смотрю в первую очередь на интеграционные точки, а не на логику внутри функции. И не принимаю код без запуска - галлюцинации обнаруживаются только при выполнении.
Чеклист и хуки выше покрывают большую часть того, с чем я сталкивался. Остальное - специфика проекта: ваши архитектурные решения, пакеты, соглашения. Их стоит добавить в чеклист по мере обнаружения.
Часто задаваемые вопросы
Что такое AI slop в коде?
AI slop — это код, который компилируется и проходит тесты, но ни один человек его бы так не написал. Лишние абстракции, дублирование утилит, несогласованная обработка ошибок, фантомные зависимости — паттерны, накапливающиеся когда AI генерирует код без полного контекста проекта.
Как обнаружить проблемы AI-сгенерированного кода на ревью?
Фокус на точках интеграции, не на внутренней логике. AI обычно пишет логику правильно, но промахивается с существующими паттернами проекта. Проверяй: использует ли существующие утилиты? Следует ли паттерну обработки ошибок? Импортирует ли правильные зависимости?
Можно ли автоматизировать ревью AI-кода?
Частично. Pre-commit хуки ловят дублирование и неиспользуемые импорты. LLM-as-Judge оценивает код по конвенциям проекта. Но архитектурное соответствие — вписывается ли новый код в существующую систему — требует человеческого суждения.