Дисклеймер — эта статья отчасти написана с помощью ИИ. Отчасти. Вот в этом вся проблема.

Мы внедрили нейронку Claude. Потом добавили ИИ-ревью. Потом обнаружили, что ИИ ревьюит код, который сам же и написал — по тем же паттернам, с теми же слепыми пятнами. Ура — мы построили идеальную машину для создания иллюзии качества!
Но сначала — давайте поиграем.
Угадай, кто написал код
Ниже — три пары функций. В каждой паре одна написана человеком, другая нагенерена ИИ. Задача простая: найди ИИ-код. Ответы — под спойлерами.
Важное условие: не подглядывайте!
Пара #1 — функция валидации email
Вариант А:
import re def validate_email(email: str) -> bool: """ Validates whether the provided string is a valid email address. Args: email: The email address string to validate. Returns: True if the email is valid, False otherwise. """ if not email or not isinstance(email, str): return False email = email.strip() # RFC 5322 compliant regex pattern for email validation pattern = r'^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$' if not re.match(pattern, email): return False # Additional checks for edge cases local_part, domain = email.rsplit('@', 1) if len(local_part) > 64 or len(domain) > 255: return False return True
Вариант Б:
import re def validate_email(email): if not email: return False return bool(re.match(r'[^@]+@[^@]+\.[^@]+', email))
👉 Показать ответ
Вариант А — ИИ. Выдают его:
Подробный docstring с Args и Returns — человек в реальном проекте так не пишет
Комментарий # RFC 5322 compliant — попытка выглядеть «правильно»
Проверка длины local_part и domain — ИИ добавляет edge cases, о которых никто не просил
Идеально структурированный код без единой живой черты
Вариант Б — человек. Короткий, прагматичный, без лишних слов. Да, regex не идеальный — зато написан за 30 секунд и решает задачу.
Пара #2 — функция получения данных из API
Вариант А:
async def fetch_user_data(user_id, session): # TODO: добавить retry логику когда-нибудь resp = await session.get(f'/users/{user_id}') if resp.status != 200: raise Exception(f'что-то пошло не так: {resp.status}') data = await resp.json() return data['user'] # надеюсь там всегда есть 'user'
Вариант Б:
async def fetch_user_data(user_id: int, session: aiohttp.ClientSession) -> dict: """Fetches user data from the API by user ID.""" try: async with session.get(f'/api/v1/users/{user_id}') as response: response.raise_for_status() data = await response.json() return data.get('user', {}) except aiohttp.ClientError as e: logger.error(f"Failed to fetch user {user_id}: {e}") raise UserFetchError(f"Could not retrieve user data for ID {user_id}") from e <details>
👉 Показать ответ
Вариант Б — ИИ. Признаки:
Кастомный exception UserFetchError — откуда он взялся? ИИ генерирует код, предполагая, что всё вокруг уже идеально устроено
logger.error — ИИ всегда добавляет логирование, даже если логгер нигде не объявлен
data.get('user', {}) вместо data['user'] — безопасно, но безмолвно глотает ошибку
Вариант А — человек. Комментарий # TODO: добавить retry логику когда-нибудь — это настоящий боевой код. «Надеюсь там всегда есть 'user'» — вы это тоже писали, не отрицайте.
Пара #3 — обработка конфигурационного файла
Вариант А:
def load_config(path="config.json"): with open(path) as f: cfg = json.load(f) # без этих ключей всё упадёт позже, пусть падает сразу assert 'db_host' in cfg assert 'db_port' in cfg cfg['db_port'] = int(cfg['db_port']) # в json иногда строка приходит return cfg
Вариант Б:
def load_config(config_path: str = "config.json") -> dict: """ Loads and validates the application configuration from a JSON file. Args: config_path: Path to the configuration file. Defaults to 'config.json'. Returns: A dictionary containing the validated configuration parameters. Raises: FileNotFoundError: If the configuration file does not exist. ValueError: If required configuration keys are missing. json.JSONDecodeError: If the file contains invalid JSON. """ required_keys = ['db_host', 'db_port', 'db_name', 'secret_key'] if not os.path.exists(config_path): raise FileNotFoundError(f"Configuration file not found: {config_path}") with open(config_path, 'r', encoding='utf-8') as config_file: try: config = json.load(config_file) except json.JSONDecodeError as e: raise json.JSONDecodeError(f"Invalid JSON in config file: {e.msg}", e.doc, e.pos) missing_keys = [key for key in required_keys if key not in config] if missing_keys: raise ValueError(f"Missing required configuration keys: {missing_keys}") config['db_port'] = int(config['db_port']) return config
👉 Показать ответ
Вариант Б — ИИ. И это самый показательный пример. Смотрите:
Огромный docstring с перечислением каждого exception — это документация ради документации
required_keys включает secret_key — откуда? ИИ дополнил список «по логике», которой нет в задаче
raise json.JSONDecodeError(...) — перехватить исключение только чтобы перебросить то же самое. Зачем?
encoding='utf-8' — явно, правильно, никто так не делает в реальном проекте без причины
Вариант А — человек.assert вместо raise ValueError — не best practice, но честно. Комментарий # в json иногда строка приходит — это реальный баг из реального продакшена.
Пара #4 — для тех, кто угадал все три
Вариант А:
def calculate_user_discount(user: User, order: Order) -> Decimal: if user.is_premium and order.total > 1000: return order.total * Decimal('0.15') elif user.is_premium: return order.total * Decimal('0.10') elif order.total > 1000: return order.total * Decimal('0.05') else: return Decimal('0')
Вариант Б:
def calculate_user_discount(user: User, order: Order) -> Decimal: if user.is_premium and order.total > 1000: return order.total * Decimal('0.15') elif user.is_premium: return order.total * Decimal('0.10') elif order.total > 1000: return order.total * Decimal('0.05') else: return Decimal('0')
👉 Показать ответ
Вариант Б — ИИ. Но вы, скорее всего, не угадали. И правильно — по коду это невозможно определить.
Оба варианта идентичны по поведению. Оба корректны синтаксически. Единственная разница — if vs elif. ИИ-ревьюер одобрит оба. Человек-ревьюер тоже.
Настоящая ошибка не здесь. Она в том, чего нет в обоих вариантах.
В реальном проекте скидки не считаются в момент запроса — они фиксируются в момент создания заказа и больше не пересчитываются. Потому что завтра user.is_premium может измениться, порог 1000 может поменяться в конфиге, а у клиента на руках будет подтверждение заказа с другой суммой. ИИ написал функцию, которая правильно считает скидку — но вызывать её при каждом рендере страницы заказа категорически нельзя.
Ни один статический анализатор это не найдёт. ИИ-ревьюер — тоже. Потому что ошибка не в коде. Она в том, где этот код вызывается. А это уже другой файл, другой контекст, и понимание того, как работает ваш конкретный бизнес.
Вот про это и была вся статья.
Так в чём, собственно, проблема
Поугадывали? Может, угадали всё, может нет — в комментариях расскажите. Но вот в чём штука: то, что вы делали руками последние пять минут — анализировали стиль, искали паттерны, чувствовали «это слишком правильно» — ваш ИИ-ревьюер этого не делает.
Claude написал код → CodeRabbit его проверил → всё зелёное → отлично, пускаем в продакшн.
Проблема не в том, что ИИ пишет просто плохой код. Проблема в том, что ИИ пишет код, который подозрительно хорошо выглядит: Docstrings есть, типизация есть, Edge cases обработаны. А то, что UserFetchError нигде не определён, что secret_key добавлен из воздуха, что молчащий data.get('user', {}) будет отлаживаться три часа — это ИИ-ревьюер как-то упустил. Потому что он обучен на том же коде, по тем же паттернам.

Это архитектурная проблема всего процесса: синтетика проверяет синтетику.
И как же быть?
Три вещи, которые реально работают:
Не убирать человека из ревью критических участков. ИИ хорошо ловит очевидное — опечатки, неиспользуемые импорты, простые race conditions. Архитектурные и логические ошибки — нет.
Тестировать ИИ-ревьюер намеренно сломанным кодом. Возьмите пять реальных багов из вашего прошлого, дайте ИИ их поревьюить. Сколько он нашёл? Это ваш реальный coverage.
Мы провели этот эксперимент на пяти реальных багах из нашего продакшена.
Результат: ИИ-ревьюер нашёл 2 из 5. Оба — простые: неиспользуемая переменная и отсутствие проверки на null. Три пропущенных — логическая ошибка в расчёте, race condition при параллельных запросах и неправильный порядок миграций. Все три потребовали понимания контекста проекта.
Разделять «написано правильно» и «делает правильное». ИИ отлично проверяет первое. Второе — всё ещё ваша работа.
А теперь главный вопрос: сколько из четырех пар вы угадали с первого раза?
И если у вас есть свои примеры ИИ-кода, который прошёл ревью и лёг в продакшн — это отдельная тема для разговора.
