- BrainTools - https://www.braintools.ru -
Пока не долетело до вентилятора
Мне достался B2B SaaS проект в стадии, о которой мечтает любой техлид: “Почти готово. Релиз через две недели”. Два из трех API-вызовов возвращали HTTP 500.
В коде накопилось несколько поколений агентных исправлений. При обнаружении новой ошибки [1] просили агента разобраться и пофиксить баг. Агент вбивал костыль в виде очередного условия. Состояние объекта определялось по косвенным признакам. В 21 месте обработчик узнавал свой вид через отрицание: не A и не B, значит мой. Агенты видели эти подходы перед собой и продолжали по образцу наматывать нейросопли на этот клубок.
Claude погружался в эту субстанцию с головой. А я снаружи подавал ключи. Когда Claude выныривал, он выглядел как кот после валерьянки. Никогда до этого я не встречал у него такого количества галлюцинаций на столь маленьком контексте. Он не мог внятно ответить на половину вопросов.
После двух дней такого шит-дайвинга я решил, что чинить симптомы бессмысленно. Мы переписали модуль целиком, восстановив намерение авторов по немногочисленным докам и имеющемуся коду. Среду, где агент пишет новый код и не видит старого, мы с Claude назвали «чистый контур». Три дня ушло на контракт переноса: алгоритмы и слепки входов и выходов. Еще два на новую реализацию. Алгоритмы прозой описывали, что надо сделать, а не как надо сделать.
Я разделил работу между двумя агентами. Аналитик изучал старый код, запускал систему, снимал слепки входов, выходов и изменений в базе. Результаты он записывал в карточки сценариев, по одной на операцию. Из карточек складывался контракт переноса. В каждой карточке:
вход
ожидаемый результат
артефакты и состояния
коды
что попадает в снимок
чем подтверждено
У каждой строки пометка: [замер], [код] или [дыра].
Исполнитель писал новый код по карточкам. Старого кода он не видел: запрет в задании, отдельная сессия, два запрета в линтере (новый код не вызывает старый, старый не вызывает новый) и сторож заимствований, который сравнивает деревья старого и нового кода и выводит только места совпадений.
Дефекты работающих функций переносили как есть и исправляли в чистом контуре. Неиспользуемые функции не переносили.
Брали целиком пользовательский сценарий: от входного запроса до результата. После успешной сверки переключали вызывающий код на новую реализацию. Старый путь удаляли после приемки.
Похожим способом в 1984 году Phoenix Technologies создала BIOS, совместимый с BIOS IBM PC. Одни инженеры изучали оригинал и составляли спецификацию, другие писали реализацию, не видя кода IBM. Этот метод называют clean room design [2].
Через несколько дней мне дали второй проект: Rust-бэкенд и пять Python-служб в отдельных репозиториях. Небольшая фича требовала согласованных изменений в нескольких местах, а контракты на двух языках расходились при зеленых тестах всех шести компонентов. Тем же способом за три дня я перенес сценарии в один Python-монолит и переключил на него маршрутизацию. Шесть репозиториев ушли в архив. Вызовы между сервисами из HTTP с JSON стали вызовами функций, и расхождение контрактов теперь ловит mypy.
Даже в новом коде агент будет вбивать костыли. Поэтому у исполнителя есть устав из 15 правил. Часть из них опытный разработчик сочтет элементарными, часть – спорными.
|
№ |
Правило |
|---|---|
|
1 |
Состояние хранится в поле. Значения и разрешенные переходы объявлены один раз. Ограничение базы строится из того же перечня. Состояние не вычисляется по соседним полям и журналам. |
|
2 |
Состояние меняет одна функция, атомарно с данными перехода. Она сверяет переход с объявленной таблицей. |
|
3 |
У ожидания внешнего ответа или фоновой задачи есть состояние сбоя и выход по таймауту. |
|
4 |
Разрешенные и запрещенные переходы проверяются тестами. |
|
5 |
Код жизненного цикла сущности находится в одном пакете. Запись ее состояния снаружи запрещена. |
|
6 |
У отказа свой код из общего перечня. Код не принимает решений по тексту сообщения об ошибке. |
|
7 |
Неизвестное значение, сбой записи или потеря блокировки дают отказ. Молчаливый успех недопустим. |
|
8 |
Независимые части результата принимаются отдельно. Обязательность части задает контракт. |
|
9 |
Вид задания объявлен данными. Обработчик не вычисляет свой вид как «не A и не B». |
|
10 |
Между старым и новым кодом нет вызовов, кроме явно названных временных исключений. Они удаляются со старым путем. |
|
11 |
Новый тест или сценарий хотя бы раз должен упасть на соответствующем дефекте. Проверяется видимый результат. |
|
12 |
Вид объекта хранится явно. Его не угадывают по подписи, префиксу идентификатора или форме строки. |
|
13 |
Экран получает состояние сущности от сервера и отображает его. |
|
14 |
Для конкурирующих ответов и действий пользователя объявлено правило старшинства. Порядок прихода ответов не меняет итог. |
|
15 |
Допустимость действия проверяет сервер. Браузер получает доступные действия и причины отказа из его ответа. |
Если одну вещь не удалось починить за три попытки, остановись и пересмотри ее устройство. Четвертую заплатку не делай.
Название – ответ на антропиковский /goal, который жжет токены как мартеновская печь, пытаясь добиться цели.
У дяди Боба, недавно вышел skill ub-mode [3]. Два правила совпадают с уставом чистого контура:
Attack the Premise: если два исправления с общей предпосылкой провалили одну проверку, разбери предпосылку, прежде чем делать следующее. Это antigoal rule.
TDD: увидь, как тест падает по ожидаемой причине. Это правило 11.
ub-mode это skill, и его специально вызывают для выполнения конкретной задачи. Устав чистого контура лежит в репозитории постоянно и читается в каждой сессии.
Правила можно положить в AGENTS.md, и модель прочитает их сама. Слабой модели я вставляю правила в промпт вместе с задачей.
Я сравнил на DeepSeek V4 Flash, 36 запусков, нарушения считал Claude Sonnet вслепую.
|
Что получила модель |
Замечаний |
|---|---|
|
Ничего |
36 |
|
Образец |
31 |
|
Образец и правила в |
25 |
|
Образец, |
18 |
Первый пример. Когда правило «одно значение объявляется один раз» лежало в AGENTS.md, DeepSeek записывал тип события строкой в трех запусках из трех. Когда в задании прямо назвали эту ловушку, ноль из трех.
Второй пример. В старом коде у заказа не было поля статуса. Было строковое поле cancel_reason, причина отмены. Отменен заказ или нет, код решал по тому, заполнено ли это поле. Это проверялось в четырех местах.
Я дал DeepSeek задачу: по отмененному заказу нельзя выдавать новые задания на сборку. Без дополнительных правил модель решила ее той же догадкой, пятой по счету:
- if order is None:
+ if order is None or order.cancel_reason is not None:
return None
С четырьмя правилами в промпте, но без образца, результат тот же.
Затем я дал ту же задачу с правилами из AGENTS.md в промпте и образцом автомата состояний. В задании прямо написал: «Повтори устройство, а не имена». DeepSeek объявил OrderStatus с четырьмя значениями (new, paid, shipped, cancelled), таблицу переходов и одну функцию проверки. Пять разбросанных проверок cancel_reason превратились в одно обращение к статусу.
|
Результат |
Без дополнительных правил |
С правилами |
С правилами и образцом |
|---|---|---|---|
|
Таблица переходов |
Нет |
Нет |
Есть |
|
Проверка завершения |
Повторяется в пяти местах |
Повторяется в пяти местах |
Собрана в одну функцию |
|
Отдельное поле состояния |
Нет |
Нет |
Есть |
Образец в последнем опыте [4] работал как затравка, из которой растят кристалл. DeepSeek получил одну хорошую реализацию автомата состояний и повторил ее устройство. Без образца та же модель повторила то, что видела в старом коде: пятую проверку по cancel_reason.
Автор: blurman
Источник [5]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/37752
URLs in this post:
[1] ошибки: http://www.braintools.ru/article/4192
[2] clean room design: https://en.wikipedia.org/wiki/Clean_room_design
[3] ub-mode: https://github.com/unclebob/ub-mode/blob/master/SKILL.md
[4] опыте: http://www.braintools.ru/article/6952
[5] Источник: https://habr.com/ru/articles/1101314/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1101314
Нажмите здесь для печати.