Пока не долетело до вентилятора
Мне достался B2B SaaS проект в стадии, о которой мечтает любой техлид: “Почти готово. Релиз через две недели”. Два из трех API-вызовов возвращали HTTP 500.
В коде накопилось несколько поколений агентных исправлений. При обнаружении новой ошибки просили агента разобраться и пофиксить баг. Агент вбивал костыль в виде очередного условия. Состояние объекта определялось по косвенным признакам. В 21 месте обработчик узнавал свой вид через отрицание: не A и не B, значит мой. Агенты видели эти подходы перед собой и продолжали по образцу наматывать нейросопли на этот клубок.
Claude погружался в эту субстанцию с головой. А я снаружи подавал ключи. Когда Claude выныривал, он выглядел как кот после валерьянки. Никогда до этого я не встречал у него такого количества галлюцинаций на столь маленьком контексте. Он не мог внятно ответить на половину вопросов.
После двух дней такого шит-дайвинга я решил, что чинить симптомы бессмысленно. Мы переписали модуль целиком, восстановив намерение авторов по немногочисленным докам и имеющемуся коду. Среду, где агент пишет новый код и не видит старого, мы с Claude назвали «чистый контур». Три дня ушло на контракт переноса: алгоритмы и слепки входов и выходов. Еще два на новую реализацию. Алгоритмы прозой описывали, что надо сделать, а не как надо сделать.
Механизм переноса
Я разделил работу между двумя агентами. Аналитик изучал старый код, запускал систему, снимал слепки входов, выходов и изменений в базе. Результаты он записывал в карточки сценариев, по одной на операцию. Из карточек складывался контракт переноса. В каждой карточке:
вход
ожидаемый результат
артефакты и состояния
коды
что попадает в снимок
чем подтверждено
У каждой строки пометка: [замер], [код] или [дыра].
Исполнитель писал новый код по карточкам. Старого кода он не видел: запрет в задании, отдельная сессия, два запрета в линтере (новый код не вызывает старый, старый не вызывает новый) и сторож заимствований, который сравнивает деревья старого и нового кода и выводит только места совпадений.
Дефекты работающих функций переносили как есть и исправляли в чистом контуре. Неиспользуемые функции не переносили.
Брали целиком пользовательский сценарий: от входного запроса до результата. После успешной сверки переключали вызывающий код на новую реализацию. Старый путь удаляли после приемки.
Похожим способом в 1984 году Phoenix Technologies создала BIOS, совместимый с BIOS IBM PC. Одни инженеры изучали оригинал и составляли спецификацию, другие писали реализацию, не видя кода IBM. Этот метод называют clean room design.

Через несколько дней мне дали второй проект: Rust-бэкенд и пять Python-служб в отдельных репозиториях. Небольшая фича требовала согласованных изменений в нескольких местах, а контракты на двух языках расходились при зеленых тестах всех шести компонентов. Тем же способом за три дня я перенес сценарии в один Python-монолит и переключил на него маршрутизацию. Шесть репозиториев ушли в архив. Вызовы между сервисами из HTTP с JSON стали вызовами функций, и расхождение контрактов теперь ловит mypy.
Устав чистого контура
Даже в новом коде агент будет вбивать костыли. Поэтому у исполнителя есть устав из 15 правил. Часть из них опытный разработчик сочтет элементарными, часть - спорными.
№ |
Правило |
|---|---|
1 |
Состояние хранится в поле. Значения и разрешенные переходы объявлены один раз. Ограничение базы строится из того же перечня. Состояние не вычисляется по соседним полям и журналам. |
2 |
Состояние меняет одна функция, атомарно с данными перехода. Она сверяет переход с объявленной таблицей. |
3 |
У ожидания внешнего ответа или фоновой задачи есть состояние сбоя и выход по таймауту. |
4 |
Разрешенные и запрещенные переходы проверяются тестами. |
5 |
Код жизненного цикла сущности находится в одном пакете. Запись ее состояния снаружи запрещена. |
6 |
У отказа свой код из общего перечня. Код не принимает решений по тексту сообщения об ошибке. |
7 |
Неизвестное значение, сбой записи или потеря блокировки дают отказ. Молчаливый успех недопустим. |
8 |
Независимые части результата принимаются отдельно. Обязательность части задает контракт. |
9 |
Вид задания объявлен данными. Обработчик не вычисляет свой вид как «не A и не B». |
10 |
Между старым и новым кодом нет вызовов, кроме явно названных временных исключений. Они удаляются со старым путем. |
11 |
Новый тест или сценарий хотя бы раз должен упасть на соответствующем дефекте. Проверяется видимый результат. |
12 |
Вид объекта хранится явно. Его не угадывают по подписи, префиксу идентификатора или форме строки. |
13 |
Экран получает состояние сущности от сервера и отображает его. |
14 |
Для конкурирующих ответов и действий пользователя объявлено правило старшинства. Порядок прихода ответов не меняет итог. |
15 |
Допустимость действия проверяет сервер. Браузер получает доступные действия и причины отказа из его ответа. |
Antigoal rule
Если одну вещь не удалось починить за три попытки, остановись и пересмотри ее устройство. Четвертую заплатку не делай.
Название - ответ на антропиковский /goal, который жжет токены как мартеновская печь, пытаясь добиться цели.
Пересечение с ub-mode
У дяди Боба, недавно вышел skill ub-mode. Два правила совпадают с уставом чистого контура:
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 превратились в одно обращение к статусу.
Результат |
Без дополнительных правил |
С правилами |
С правилами и образцом |
|---|---|---|---|
Таблица переходов |
Нет |
Нет |
Есть |
Проверка завершения |
Повторяется в пяти местах |
Повторяется в пяти местах |
Собрана в одну функцию |
Отдельное поле состояния |
Нет |
Нет |
Есть |
Затравка кристалла
Образец в последнем опыте работал как затравка, из которой растят кристалл. DeepSeek получил одну хорошую реализацию автомата состояний и повторил ее устройство. Без образца та же модель повторила то, что видела в старом коде: пятую проверку по cancel_reason.

Комментарии (2)

dgrees
29.10.2026 17:09Интересно про antigoal. я не натыкался на такую проблему. сейчас посмотрел, у меня есть такие записи:
Ground rules
- When you run into a problem you didn't expect, write yourself some exploratory code piece to see what's going on when appropriate. You can do it whenever you like in the repository, just don't mix it up with production code so you or I can delete it easily.
- Push back when something seems wrong - don't just agree with mistakes
- Prove the simplest design first. Before building elaborate machinery, work concrete cases to show a simpler approach is insufficient. Do this before implementing, not just in the post-write simplification pass — it's cheap to reason through and expensive to tear out.
- Explain the current architecture and what you plan to change, in a nutshell. You may use ascii or other diagrams, examples, to explain data flow.Возможно это влияет - такой подход к исследованию должен исключать костыли. То есть дойти до корня проблемы, проверить его, найти и доказать наиболее подходящий дизайн, и потом писать.
Но может у меня сеттинг другой - я не работал ещё с вайбкоженными репо, обычно были хреново-руками-коженые.
alfredlao
Чистота контекста это недостижимый идеал человечества со времен Вавилона. А агенты, слепленные по образу и подобию нашему, не исключение.