Очистка кода от «мусора» ИИ, часть 1: Код

Источник: Anaconda•

Очистка кода от «мусора» ИИ, часть 1: Код

Узнайте о восьми признаках кода, сгенерированного ИИ — от констант одноразового использования до дублирующихся вспомогательных функций, — а также о линтерах и настройке AGENTS.md, которые помогут избавить ваш репозиторий от «мусора».

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

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

Сколько вы нашли?

  • Комментарий и обе строки документации (docstrings) повторяют то, что уже сказано в коде.
  • QUERY_TIMEOUT — это константа, которая используется только один раз.
  • QueryRunner находится выше константы и обеих функций, а import re расположен внутри функции.
  • Конструкция try/except Exception оборачивает код, который не может вызвать ошибку, а return None скрывает факт срабатывания таймаута.

Это то, что видно сразу. Если посмотреть на остальную часть репозитория, найдутся еще два признака:

  • В файле utils.py репозитория уже есть функция with_timeout.
  • В файле sources/duckdb.py репозитория уже выполняется разбор этого сообщения об ошибке с помощью того же регулярного выражения.

Люди тоже совершают все эти ошибки, но агент делает их быстрее, чем кто-либо успеет провести построчную проверку. На рисунке 1 отмечена каждая из них, а в разделе с типичными ошибками ниже они разобраны более подробно.

Типичные ошибки

  • Комментарии, которые говорят «что», а не «почему». В приведенном фрагменте комментарий повторяет имя константы, а обе строки документации — имена функций. Код и так говорит, что он делает, поэтому комментарий должен объяснять, почему это сделано: например, указать ограничение, обходной путь или причину выбора конкретного числа.
  • Константы и вспомогательные функции, используемые только один раз. В фрагменте QUERY_TIMEOUT — это константа уровня модуля, используемая ровно один раз в качестве аргумента по умолчанию. В другом модуле происходит то же самое с EMPTY_COMPLETION, строкой, которая используется единожды. Если что-то используется только один раз, оставьте это внутри функции, например: timeout_seconds: float = 60. Выносите это, только когда потребуется второй вызов или когда имя объясняет то, что не может объяснить само значение.
  • Код не на своем месте. В фрагменте QueryRunner находится выше константы и обеих функций, а import re без всякой причины помещен внутрь функции. Решение заключается в том, чтобы упорядочить каждый файл: сначала импорты, затем константы, функции и классы. Тогда агент найдет существующие импорты и определения, прежде чем добавлять дубликаты. Импорт внутри функции для предотвращения циклической зависимости может быть допустим (с пометкой # noqa, указывающей на цикл), но часто это означает, что функция должна находиться в другом модуле.
  • Защитные конструкции, которые не могут сработать, и ошибки, которые они скрывают. В фрагменте try/except Exception оборачивает код, который не может вызвать ошибку, а run_with_timeout возвращает None при таймауте, поэтому вызывающая сторона не может отличить таймаут от пустого результата. В других местах репозитория import numpy находится внутри try/except ImportError, хотя numpy является обязательной зависимостью. Для каждой такой проверки задавайтесь вопросом: как она может не сработать и что увидит вызывающая сторона в этом случае?
  • Изобретение велосипеда. В фрагменте это run_with_timeout. В репозитории уже есть функция with_timeout(coro, timeout_seconds=10, default_value=None), которая выполняет ту же задачу. У нее есть тот же недостаток, так как она по умолчанию возвращает None при таймауте. Передайте timeout_seconds=60, чтобы сохранить ограничение из фрагмента, и исправьте одну вспомогательную функцию вместо того, чтобы поддерживать две.
  • Дублирование знаний в двух местах. В фрагменте это регулярное выражение, которое уже есть в sources/duckdb.py. Две копии лишают агента единого источника истины, и со временем они начинают расходиться. Используйте этот парсер повторно.
  • Исправление ошибки там, где она проявляется, а не там, где она возникает. Когда трассировка указывает на вызывающий код, агент склонен исправлять именно его — например, добавлять .strip() везде, где используется вывод вспомогательной функции, вместо того чтобы один раз исправить саму функцию. Худший вариант — изменение ожидаемого значения в тесте, чтобы оно соответствовало неверному результату.
  • Файлы, слишком длинные для поиска. Например, файл utils.py в репозитории может содержать 2209 строк и 65 функций верхнего уровня, а модуль для работы с большой языковой моделью (LLM) — 3659 строк. Агент читает файл такого размера по частям, поэтому вспомогательную функцию на строке 1551 в utils.py, где находится with_timeout, легко пропустить, а нечеткое или непоследовательное имя делает это еще проще. Разбивайте длинные модули по их назначению и используйте имена, которые уже приняты в соседнем коде (font_size, а не fontsize).

Почему модели пишут код именно так

Модель, работающая над изменением, видит задачу, открытые файлы и упавший тест, а не весь репозиторий целиком, и написать что-то новое дешевле, чем найти существующее. Вероятная причина: во время обучения люди оценивают ответы модели, и код, который выглядит «старательным», получает более высокие оценки. Поэтому модель учится добавлять видимость старательности, например блоки try/except, именованные константы и строки документации, даже там, где они бесполезны. Она также склонна писать собственные тесты, которые проверяют, что код делает то, что модель предположила, а не то, что нужно вам.

Как очистить код от мусора

Джулиан, коллега, который начал этот разговор, дал ссылку на PostHog’s AGENTS.md, где инструкции для агента сочетаются с правилами линтинга и непрерывной интеграции (CI), доказательствами работоспособности изменений и человеком, ответственным за слияние (merge). Другой коллега, Мариус, возразил, что общекорпоративный стандарт устареет через несколько обновлений модели, и что долговечными являются только специфичные для проекта проверки, такие как линты и архитектурные тесты, а файл AGENTS.md должен оставаться лишь краткой картой репозитория. Мариус уже видел, как агенты игнорируют новый AGENTS.md, а агент может пропустить инструкцию, но не может выполнить слияние при провале программной проверки.

Перед генерацией

Пусть AGENTS.md будет коротким и служит маршрутизатором с разделами, которые направляют каждый тип задачи к файлу с подробностями, чтобы агент читал только то, что применимо. Навык contributing-to-holoviz работает именно так. Маршрутизатор загружается при каждой задаче, а агенты могут перестать читать длинный файл на полпути. Он все равно устаревает при перемещении файлов, поэтому добавьте проверку того, что каждый путь в нем существует, и обновляйте его в том же коммите, в котором перемещаете код. Matt Pocock’s guide to AGENTS.md — хорошее место для начала.

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

Дайте агенту возможность искать по смыслу, например, используя индекс кодовой базы в Kilo by Anaconda или Cursor, чтобы он мог искать «вспомогательную функцию, которая запускает корутину с таймаутом», вместо того чтобы угадывать имя для grep.

Просите найти первопричину, а не просто добиться прохождения теста. Ограничьте каждый запрос на слияние (PR) одним изменением, которое рецензент может удержать в голове и откатить независимо. В одном из моих ревью восемь отдельных исправлений были объединены в один коммит, поэтому не было чистого способа отменить одно из них, не затронув остальные.

Проверки и доказательства

Если названия правил ниже вам ни о чем не говорят, попросите своего агента включить эти проверки и исправить то, на что они укажут.

Начните с тех проверок (lints), которые у вас уже есть. Конфигурация Ruff в этом репозитории игнорирует PLC0415 — правило, которое помечает импорты внутри функций, — и не включает BLE001, которое помечает except Exception. Таким образом, для двух проблем из начального фрагмента кода правило просто отключено. Чтобы включить их, достаточно одной строки, плюс удаление PLC0415 из списка игнорируемых, если оно там есть.

В Ruff нет правила для констант, определенных после первой функции или класса. Навык очистки (cleanup skill), описанный ниже, проверяет порядок в файле, или же это можно сделать с помощью короткого скрипта через pre-commit.

Относитесь к каждому # noqa как к вопросу для ревью. В одном PR было подавлено правило B904, требующее использовать raise ... from err, с комментарием о том, что вспомогательная функция повторных попыток (retry helper) иначе проследит цепочку до исходной ошибки базы данных и покажет её модели. Это обоснованный комментарий, но реальная проблема кроется в обходном пути, который он описывает: вспомогательная функция считывает не ту ошибку. Даже в качестве временного решения, raise ... from None удовлетворяет правилу и разрывает цепочку без необходимости в # noqa.

Правило вроде «utils не должны импортировать модули UI» может быть контрактом, который помечает такие импорты независимо от того, отложены они в функцию или нет:

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

После генерации

Сначала удалите: неиспользуемые проверки (dead guards), константы для однократного использования, имена которых ничего не добавляют, и обертки, которые лишь перефразируют ошибку.

Затем выполните проверку по всему репозиторию. Хорошо работает такой промпт:

Навык очистки в holoviz-skills охватывает несколько таких проверок, например, порядок файлов, комментарии, дублирующие код, и исправления, которым место в другом модуле. Установите его командой pip install holoviz-skills, затем holoviz-skills install, что позволит обнаружить Kilo, Claude Code, Cursor и другие инструменты. Затем попросите своего агента проверить ветку с помощью навыка contributing-to-holoviz, который направляет запросы на ревью к контрольному списку очистки. Этот список работает с любым репозиторием на Python, а не только с HoloViz.

Раскрытие информации о том, какой инструмент и модель внесли изменения, подсказывает ревьюеру, где искать внимательнее, но ответственность за слияние (merge) всё равно несет человек.

Когда одна и та же находка повторяется постоянно, превратите её в правило, линт или запись в навыках. Затем с помощью оценок (evals) проверяйте, что каждое правило сохраняет свою актуальность после выхода новой версии модели.

Чего делать не стоит

Не переусердствуйте. Ошибка, которая случилась лишь однажды, не нуждается в правиле, а растущий список запретов делает файл AGENTS.md длиннее, из-за чего агент с большей вероятностью перестанет читать его на полпути. То же самое касается линтов: включайте правило только тогда, когда проблема повторяется.

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

Выводы

  • Убирайте «мусор» до того, как он распространится. Вы платите за каждую строку, которую читает агент, а агент копирует то, что находит, в следующее изменение.
  • Проводите ревью по всему репозиторию, а не только по diff. Дублирующая вспомогательная функция и дублирующее регулярное выражение из фрагмента не видны в diff.
  • По возможности оформляйте правила в виде проверок, а не письменных инструкций. Агент может пропустить инструкцию, но не сможет обойти падающую программную проверку.
  • Сделайте репозиторий удобным для поиска: используйте небольшие файлы, понятные имена и файл AGENTS.md, в котором указано, где что находится, чтобы агент находил существующие вспомогательные функции, а не переписывал их заново.
  • Делайте каждое изменение небольшим, с доказательством того, что оно было запущено.

О чём эта статья

Ещё в разделе «Разработка ПО»

Все →

Ещё от Anaconda