Огляд масштабних переробок у штучному інтелекті: чому проходження тестів не виявляє видаленої мети
Дізнайтеся, чому алгоритми штучного інтелекту під час рефакторингу тихо видаляють код, який несе навантаження, чому зелені тести цього не виявляють, а також про п’ять практик перевірки, які роблять масштабні рефакторинги безпечними для випуску.
Джерела: I Let Claude Refactor 4,000 Lines. Here’s the Part I Had to Undo.
Асистенти з кодування на основі ШІ тепер настільки ефективні, що можуть впоратися з недоглянутим модулем за один день, і результат зазвичай виходить кращим, ніж те, що його замінило. Небезпека полягає не у недбалому коді, а у коді, який виглядає правильно, але приховано усуває функціонал, який ніхто не описав. У цьому посібнику розглядається реальний приклад такої проблеми, пояснюється, чому і набір тестів, і перевіряючий його пропустили, та наводиться конкретний процес перевірки під час масштабних оптимізацій за допомогою ШІ.
Оптимізація, яка покращила код та зламала систему
Уявіть собі модуль, у якому протягом двох років накопичувалися зміни: близько 4 000 рядків у чотирьох файлах — це та частина кодової бази, яку інженери намагаються не торкатися, коли тільки можливо. У ньому неоднорідне оброблення помилок, три різні способи парсингу дат та кілька функцій, чи все ще вони викликаються, ніхто не може з упевненістю сказати.
Розробник передає код AI-асистенту протягом двох сеансів та отримує значно кращий результат: однорідні шаблони, правильні підказки типів, видалені непотрібні частини коду, приблизно на третину менше рядків. Тести пройшли успішно. Розробник переглядає зміни або вважає, що це зробив, та розсилає код у використання.
Через шість днів під час інтеграції з партнером починають зникати записи. Не виникає жодних явних помилок; приблизно чотири відсотки записів просто перестають надходити. Причиною є рефакторинг, а саме зміна, яку розробник прочитав та схвалив.
У чому асистент справді виявив себе найкраще
Варто бути точним щодо того, що пройшло добре, адже висновок не полягає у тому, що «AI погано справляється з рефакторингом». Асистент чудово справився з кожною трансформацією, яка залежала лише від коду, що був перед ним:
- об’єднання трьох функцій для обробки дат у одну
Для механічних, локальних змін, які можна перевірити шляхом їх прочитання, цей підхід був швидшим та послідовнішим, ніж робота людини, і не втрачав фокусу на сотнях рядків коду, де зазвичай погіршується якість ручних оптимізацій. Коли завдання полягає у тому, щоб «зробити код більш зрозумілим у вираженні того самого поведінки», ці інструменти демонструють високу ефективність. Проблеми починаються тоді, коли стає неясно, що саме входить до категорії «того самого поведінки».
Чому код, що виконує ключові функції, видаляється
Код, який не має очевидної причини для існування, виглядає як зайвий сміття. Ось схвалена зміна:
# before
def normalise_timestamp(raw):
ts = raw.strip()
if ts.endswith("Z") and "." in ts:
ts = ts.split(".")[0] + "Z" # partner sends 7-digit microseconds
return datetime.fromisoformat(ts.replace("Z", "+00:00"))
# after
def normalise_timestamp(raw):
return datetime.fromisoformat(raw.strip().replace("Z", "+00:00"))
Видалена гілка схожа на класичний об’єкт для рефакторингу: окремий випадок, дивно специфічна перевірка суфікса, розділення рядка. Вона здається зайвою, оскільки fromisoformat вже парсить ISO-часові позначки.
Проблема полягає у вхідних даних. Один із партнерів надсилає часові мітки з сімома дробовими цифрами. До Python 3.11 функція fromisoformat розпарсювала лише ті формати, які сама функція isoformat() генерувала, тобто не більше ніж шість дробових цифр, тому значення з сімома цифрами викликало помилку ValueError. Python 3.11 та новіші версії підтримують значно ширший підмножину форматів ISO 8601 та є більш толерантними до таких випадків, тож те, чи порушить ця зміна функціонал, залежить від конкретної версії, що є ще однією причиною перевіряти, а не припускати. Цей гілочний розвиток виник через заявку на підтримку, надіслану вісімнадцять місяців раніше, і його короткий коментар не дозволив ні людині, ні моделі належним чином оцінити його значення порівняно з тим, наскільки легко його можна було б видалити. Також зверніть увагу, що стара версія робила більше, ніж здавалося: вона повністю ігнорувала дробову частину, тож навіть результат „нової“ версії відрізняється для даних, які все ж таки піддаються парсингу.
Основна проблема проста: модель могла бачити код, але не інцидент, який його створив. Заява про проблему, аналіз після виникнення проблеми, листування та години, витрачені на дебагування, — усе це було поза файлом. Виконання команди git blame виявило б повідомлення про зміну на кшталт „виправити обробку часових міток для потоку даних партнера“, що й було єдиним обґрунтуванням, але ніхто не просив асистента це перевірити. Рецензент схвалив видалення з тієї ж причини, що й асистент: обидва переглядали різницю версій, а не історію змін.
Чому набір тестів залишався „зеленим“
Тести перевіряли, що повертає функція для вхідних даних, про які хтось подумав, а проблемні вхідні дані за визначенням були такими, про які ніхто не думав.
У налаштуваннях були часові позначки з трьома десятковими цифрами, адже саме так люди вводять дані для тестування вручну. Валюти з сімома цифрами з’являлися лише у прямому потоці інформації одного з партнерів.
Саме тому масштабні рефакторинги є ризикованими, незалежно від наявності ШІ: пройшовший набір тестів доводить лише те, що поведінка, описана у тестах, залишилася незмінною. Він нічого не говорить про поведінку, яка не описана у тестах. Коли ви рефакторуєте тридцять рядків вручну, ви думаєте про кожен з них. Коли одночасно змінюється чотири тисячі рядків, набір тестів фактично виконує майже всю перевірку, хоча спочатку не був створений для такого навантаження.
Показники охоплення також не допомогли. Функція мала повне охоплення рядків: кожен рядок виконувався під час тестування. Однак одна з гілок просто ніколи не виконувалася з тими даними, які мали значення. Охоплення рядків вимірює, який код був виконаний, а не які формати даних оброблялися.
Сортування змін ШІ за рівнем ризику
Практичним рішенням є припинення однакового перегляду відмінностей, створених ШІ, та їх сортування за категоріями. Ці категорії мають дуже різні профілі ризику:
- Низький ризик: анотації типів, перейменування, форматування, передчасні повернення та подібні механічні переписування. Вони є локальними та легко піддаються перевірці шляхом читання.
- Середній ризик: об’єднання дубльованих функцій. Зазвичай це нормально, але переконайтеся, що об’єднана версія працює з вхідними даними кожного користувача.
- Високий ризик: видалення коду, який, здається, не використовується, усунення спеціальних випадків, які здаються зайвими, та уніфікація поведінки, яка здається непослідовною.
Усі випадки з високим ризиком мають спільну рису: у кожному випадку модель правильно помічає, що щось не так, і саме те, що щось не так, є причиною її існування.
Останній пункт також впливає більш тихо. Уявіть собі модуль, який форматує помилки інакше, ніж решта, та операцію рефакторингу, яка змушує його відповідати іншим. Ця різниця була навмисною, оскільки наступний користувач обробляє саме такий формат помилок. Прагнення до уніфікації було правильним інстинктом; проте застосування його тут стало помилкою.
П’ять практик для наступного масштабного рефакторингу
Вони впорядковані приблизно за тим, наскільки кожна з них могла б запобігти вищезазначеній проблемі.
Зробіть кожну зміну достатньо невеликою, щоб її можна було насправді переглянути
Чотири тисячі рядків — це зміна, яку неможливо переглянути. Після кількох сотень рядків більшість людей лише шукають очевидні проблеми, замість того щоб ретельно аналізувати кожну частину коду, як би вони собі не казали. Обмежте кожну сесію одним модулем, а кожен pull request — змінами лише одного модуля. Вузьке місце — це не продуктивність моделі, а увага переглядача.
Вимагайте обґрунтування для кожного видалення
Перш ніж погодитися з результатом, запитайте у асистента список усього, що він видалив, та для кожного елемента — чому він дійшов висновку, що видалення безпечне. Примусове викладення міркувань у тексті — це один із найцінніших кроків, які ви можете здійснити. Будь-яке обґрунтування на кшталт «це здавалося зайвим» слід розглядати як сигнал для ручного перевірки.
Надайте моделі історію змін
Попросіть асистента використати команду git blame для кожної рядка, яку він пропонує видалити, та надати повідомлення про зміни. Причина появи незвичайної гілки часто записується саме там, і модель добре використовує цей контекст, якщо він їй доступний; проте вона не буде шукати його самостійно без запиту.
Напишіть коментарі з поясненням «чому» перед рефакторингом
Спочатку додавайте пояснювальні коментарі, а не пізніше. Точний коментар із зазначенням партнера, семизначних дробів та шестизначного ліміту парсера, ймовірно, запобіг би цьому видаленню. Нечіткий запис про часові мітки партнера — ні. Якщо той, хто буде читати код далі, людина чи інструмент, не зможе зрозуміти, навіщо існує певний рядок, цей рядок може зникнути вже після однієї процедури очищення.
Порівняйте стару та нову версії з реальним трафіком
Пропустіть через стару та нову реалізації дані за один день з реального виробничого трафіку та порівняйте результати. Для простих операцій типу парсингу та нормалізації це дешево, і такий підхід виявляє саме ту категорію проблем, яку не помічають фікстури, адже виробничий трафік містить дані, про які ніхто не подумав записати. Якщо повторне використання трафіку неможливе, наступним кращим варіантом є вибірка реальних даних для фікстур регресійного тестування.
Основні висновки
Рефакторинг все одно вартий був того: модуль став кращим, робота впоралася за один день, а не тиждень, і одна відхилення серед чотирьох тисяч змінених рядків — це значно краще, ніж багато великих ручних рефакторингів. Називати цей результат „помилкою ШІ“ — оминати суть. ШІ дійшло розумного висновку на основі того, що могло побачити, у випадку без зазначеної причини, а людський рев’юер дійшов такого ж висновку на основі того самого неповного результату.
- Репозиторій — це лише частина системи; причини дивного коду знаходяться у тикетах, повідомленнях про коміт, аналізах після виникнення проблем та у спогадах людей.
- Інструмент, який бачить лише код, буде впевнено видаляти все те, для чого ніколи не було описано обґрунтування.
- Позитивні тести та повне покриття рядків не доводять, що поведінка збереглася для вхідних даних, які ніхто не моделював.