Разработчик описал DDD-манифест для ИИ-агентов в legacy-коде
Автор блога coldtake.dev много лет использует LLM в разработке ПО и постоянно видит один и тот же контраст: на гринфилд-проектах и в небольших репозиториях ИИ-агенты дают заметный прирост продуктивности, но стоит ввести агентов в legacy-кодовую базу, с тяжёлыми деревьями зависимостей, сильной связанностью и накопленным техдолгом, и качество их работы резко падает. У сбоя, по его наблюдению, конкретная форма: попроси добавить поле «статус предложения о работе» в гринфилд-репозитории, получишь его без проблем. Попроси то же самое в системе, которая условно работает уже четыре года (пример иллюстративный, не разбор конкретной системы), и модель изобретает четвёртое написание понятия, которое в кодовой базе и так существует в трёх видах, потому что сама кодовая база никогда не решала, какой из вариантов настоящий. Модель пишет адаптер там, где хватило бы прямого вызова, или, наоборот, обращается напрямую там, где весь смысл был именно в адаптере. Каждый такой случай, вопрос о системе, на который сама система нигде не даёт ответа, и модель просто угадывает, часто неверно. Вывод автора: чинить нужно не модель, не готов сам код, а готовность к работе с агентами можно наращивать, причём постепенно, часть за частью.
Дальше автор выстраивает экономику вокруг техдолга, естественного следствия того, что разработка всегда меняет будущее качество кода на скорость поставки прямо сейчас. Обычная практика, закладывать на расчистку техдолга 10, 20% технологического бюджета, но на деле это остаётся теорией и постоянно откладывается «на следующий квартал». По мысли автора, пятая часть бюджета (верхняя граница той же вилки 10, 20%) уходит на две принципиально разные по цене вещи: решить, что менять, и напечатать само изменение. Цена решения осталась прежней, а цена печати обрушилась, LLM теперь берёт на себя механическую половину чистки (выделенный модуль, рефакторинг между двумя пакетами, дополнительное тестовое покрытие) по цене, уже не похожей на цены 2020 года. Расчистка техдолга всё ещё требует времени, просто заметно меньше, и у автора на руках остаётся именно решающая часть.
Для этого разделения автор занимает пару терминов у Джона Оустерхаута из книги «A Philosophy of Software Design», тактическое и стратегическое программирование, сразу оговариваясь, что сознательно их «гнёт» под свою задачу. Стратегическая работа, это решение: прочитать систему, понять, что и почему должно измениться и действительно ли изменение служит нужной функции. Тактическая работа, перенести это решение в файлы. Первую половину автор оставляет полностью за собой, во второй он скорее ревьюер, чем исполнитель: анализирует кодовую базу и заводит по нужным изменениям issue в GitHub. Дальше issue разбирает его собственная ИИ-система на скиллах и сабагентах: скилл, это оформленная процедура, markdown-файл с инструкциями, который модель подгружает, когда задача ему соответствует, чтобы «разобрать issue» или «пересобрать карту контекстов» каждый раз выполнялось одинаково, а не так, как автор случайно сформулировал его этим утром; сабагент, отдельная модельная сессия со своим чистым контекстом и узкой задачей (реализовать, проверить безопасность, сверить со спецификацией), которая возвращает только результат, а не весь свой транскрипт целиком. Когда issue реализованы, автор проходит по PR: принимает изменения или просит доработать, инкрементально, следя за тестовым покрытием и за тем, кто может «сломаться», какие ещё части системы потребляют то, что он трогает, и переживут ли они изменение.
Стратегическую половину автор считает настолько ценной, насколько точен язык, которым она записана, и здесь в дело идёт DDD. Domain-Driven Design он называет одним из своих постоянных выборов для кода, который через год ещё можно менять: подход, предложенный Эриком Эвансом, сократил разрыв в понимании между бизнесом и разработкой через общий словарь терминов и деление системы на ограниченные контексты, обе стороны говорят на одном языке. С агентами в контуре эта связь становится ещё важнее: именно через неё формулируются задачи для модели и читается её объяснение своих решений.
Механика конкретная. Каждый репозиторий несёт в корне файл «.workflow.json», личный манифест, единственная регистрация, которая вообще нужна репозиторию, без второй записи, с которой можно рассинхронизироваться. Доменный блок манифеста называет проект, его ограниченные контексты, путь к глоссарию каждого, тип поддомена и каждое ребро (edge) к соседнему контексту. У ребра четыре поля: «to», адрес контекста на другом конце; «direction», кто кого вызывает, объявляется отдельно с каждой стороны («outbound», если репозиторий обращается наружу, «inbound», если обращаются к нему); «owner», чья модель данных побеждает при разногласии; «pattern», тип связи из фиксированного словаря, например «conformist» (принять чужую форму как есть) или «anticorruption-layer» (прослойка, переводящая чужую форму в свою); если репозиторий ведёт себя непоследовательно, на запись принимает чужую форму как есть, а на чтение переводит в свою, паттерн помечается как «unclassified» с отдельной запиской, потому что один ярлык описал бы верно только один из двух случаев. Рядом с манифестом на каждый ограниченный контекст лежит «CONTEXT.md», живой глоссарий с точным значением каждого термина и списком сознательно отвергнутых синонимов; по словам автора, на контекст приходится два файла, и оба принадлежат тому репозиторию, который владеет кодом. Портфельную карту контекстов, «CONTEXT-MAP.md», руками никто не пишет, её собирает генератор: скрипт обходит все репозитории на диске и объединяет их доменные блоки; карта одноразовая и пересобирается по требованию.
Пример у автора свой: job-offer-box, трекер заявок на работу из двух репозиториев, Rust-бэкенд job-offer-backend (числится в более широком личном проекте hyperion) и веб-фронтенд job-box-web. Термином владеет тот контекст, где хранится постоянное состояние: бэкенд владеет продуктовым языком, Job Offer, Profile, Profile Variant, Resume, Cover Letter (предложение о работе, профиль, вариант профиля, резюме, сопроводительное письмо), а фронтенд владеет только собственным экранным словарём, View Model, Filter State, Facet Stats (модель представления, состояние фильтра, статистика по фасетам), и помечает всё остальное как «[published]»: оно приходит дословно, как TypeScript, сгенерированный из OpenAPI-документа бэкенда. Указав агенту на веб-репозиторий, автор даёт ему знание границ: переименование Job Offer там, решение бэкенда, а не его; адаптеры на пути чтения стоят не случайно; и видно, какие слова агент вообще вправе придумывать сам.
Поскольку каждое ребро объявлено дважды, с обеих сторон, генератор может свести пары и найти нестыковку: поставщик называет свою позицию («published-language»), потребитель, свою («conformist», «anticorruption-layer»), и проверка по сути сверяет одну табличную пару с другой. Автор запускает эту сверку как отдельный скилл в трёх случаях: когда он только что менял манифест, когда онбордит новый репозиторий и перед тем, как менять что-то, от чего зависят соседние контексты. Каждое найденное расхождение становится DDD-issue на репозитории неправой стороны, с отпечатком (тип находки плюс два адреса), поэтому повторный прогон после половинчатого исправления обновляет тот же issue, а не плодит второй, а находка, которая перестала повторяться, закрывает issue сама. Автор называет это стратегическим слоем и говорит, что он уже готов: слой определяет, где кончается контекст и как он говорит с соседями, то есть форму карты. Внутри же любого отдельного контекста код остаётся обычным, всё ещё позволяющим собрать бессмысленный объект и сохранить его: перенос кода на настоящие DDD-примитивы (объекты-значения, агрегаты, доменные сервисы) автор называет следующим шагом, который пока не сделан и не показан, и обещает раскрыть всю систему со скиллами «в ближайшее время».
Ключевые факты
- ИИ-агенты хорошо работают в гринфилд-проектах и на небольших репозиториях, но в legacy-коде с тяжёлыми зависимостями и техдолгом качество их работы резко падает: модель встречает понятие с тремя разными написаниями в коде и добавляет четвёртое, ставит адаптер там, где хватило бы прямого вызова, или наоборот, потому что сама кодовая база нигде не фиксирует, какой вариант верный.
- Автор делит работу на «стратегическую» (решить, что и почему менять; термины он сознательно «гнёт» у Джона Оустерхаута и эту часть оставляет за собой) и «тактическую» (перенести решение в код; подешевела благодаря LLM и теперь уходит скиллам и сабагентам по цепочке issue на GitHub, затем PR, затем его ревью).
- Ядро системы, файл «.workflow.json» в корне каждого репозитория: описывает домен (ограниченные контексты, путь к глоссарию каждого, тип поддомена) и все связи с соседними контекстами через поля «to», «direction», «owner», «pattern»; рядом лежит «CONTEXT.md», живой глоссарий терминов и отвергнутых синонимов, а портфельная карта «CONTEXT-MAP.md» не пишется вручную, а собирается генератором из всех репозиториев на диске.
- Каждая связь между контекстами объявлена дважды, с обеих сторон, поэтому генератор сверяет пары автоматически; расхождение он фиксирует как DDD-issue с отпечатком (тип находки плюс два адреса) на репозитории неправой стороны и запускает такую сверку как скилл в трёх случаях: после правки манифеста, при онбординге репозитория и перед изменением того, от чего зависят соседи.
- На собственном примере job-offer-box (Rust-бэкенд job-offer-backend в личном проекте hyperion плюс веб-фронтенд job-box-web): бэкенд владеет пятью терминами продукта (Job Offer, Profile, Profile Variant, Resume, Cover Letter), фронтенд, только тремя терминами экрана (View Model, Filter State, Facet Stats), а остальное получает как TypeScript, сгенерированный из OpenAPI-документа бэкенда; сам автор подчёркивает, что готова пока только карта границ, а перенос кода внутри контекстов на DDD-примитивы, следующий, ещё не показанный шаг.
Почему это важно
Автор формулирует наблюдение резко: у сбоя ИИ-агентов в legacy-коде конкретная форма, а не общая «модель слабовата». Попроси добавить поле в гринфилд-репозитории, получишь его без проблем. Попроси то же самое в системе, которая условно работает уже четыре года (пример иллюстративный), и модель изобретает четвёртое написание понятия, которое в коде и так существует в трёх видах, потому что сама кодовая база никогда не решала, какой вариант настоящий: она пишет адаптер там, где хватило бы прямого вызова, или обращается напрямую там, где весь смысл был именно в адаптере. Каждый такой случай, вопрос о системе, на который сама система нигде не отвечает, и модель угадывает, часто неверно. Отсюда и вывод, на котором держится вся статья: чинить нужно не модель, не готов сам код, а готовность к работе с агентами можно наращивать постепенно, часть за частью. Экономика подкрепляет тезис: обычно на техдолг закладывают 10, 20% технологического бюджета, но это остаётся теорией и сдвигается «на следующий квартал»; из двух половин расчистки, решить, что менять, и напечатать изменение, вторая благодаря LLM подешевела почти до цен, непохожих на 2020 год, а первая осталась такой же дорогой. Именно эта, теперь единственно дорогая половина и держится на том, есть ли у кода общий, точный язык, а это ровно то, что даёт DDD.
Кому это важно
Технологическим лидерам и архитекторам, которые уже завели кодовых агентов в старые, сильно связанные кодовые базы и упираются в то же самое угадывание терминов, которое описывает автор. Командам, которые сами используют скиллы и сабагентов для перевода GitHub issue в PR, но пока держат знания о границах домена только в головах разработчиков или в разрозненной документации для людей, а не в артефакте, который читает сам агент. Практикам DDD, которые хотят перенести идею ограниченных контекстов и общего языка из документации в машиночитаемую инфраструктуру. И тем, кто работает с системами из нескольких репозиториев (отдельно бэкенд, отдельно фронтенд, отдельные сервисы), именно рассинхрон терминов между репозиториями и есть, по описанию автора, главный источник ошибок агента.
Как это применить
Рецепт автора воспроизводим по шагам. В корень каждого репозитория добавляется файл «.workflow.json» с доменным блоком: имя проекта, список его ограниченных контекстов, путь к глоссарию каждого, тип поддомена и рёбра к соседним контекстам; каждое ребро описывают четыре поля, «to» (адрес контекста на другом конце), «direction» (кто кого вызывает, объявляется отдельно с обеих сторон), «owner» (чья модель данных побеждает при разногласии) и «pattern» (тип связи из фиксированного словаря вроде «conformist», когда сторона принимает чужую форму как есть, или «anticorruption-layer», когда переводит её в свою; несогласованное поведение сразу в обе стороны помечается как «unclassified» с поясняющей запиской). На каждый контекст заводится «CONTEXT.md», глоссарий точных значений терминов и сознательно отвергнутых синонимов; правило владения термином простое: если два контекста используют одно слово, им владеет тот, где хранится постоянное состояние. Портфельную карту «CONTEXT-MAP.md» руками не пишут, её собирает генератор, который обходит все репозитории на диске; карта одноразовая, её можно пересобрать в любой момент. Сверку рёбер (каждое объявлено дважды, с обеих сторон) запускают как отдельный скилл в трёх случаях: после правки манифеста, при онбординге нового репозитория и перед изменением того, от чего зависят соседние контексты; каждое найденное расхождение автоматически уходит issue на репозиторий неправой стороны, с отпечатком из типа находки и двух адресов, чтобы повторный прогон обновлял тот же issue, а не плодил дубликаты. Дальше, та же цепочка, что и для остального кода: issue, агент, PR, ревью автора, с обязательной проверкой, какие ещё части системы потребляют то, что меняется.
Можно ли доверять
Это личный практический отчёт, а не исследование: автор блога coldtake.dev (пост «Domain-Driven Agents», подхвачен на Hacker News) описывает собственную систему на собственных репозиториях, ни компания, ни работодатель, ни клиент нигде не названы. Сам JSON-листинг манифеста до текста статьи не дошёл, это пробел извлечения, не источника: автор вводит пример как урезанный до одного ребра и сразу разбирает его построчно, называя поля to, direction, owner, pattern; CONTEXT.md и CONTEXT-MAP.md остаются только словесным описанием структуры, без примера, поэтому проверить их точный синтаксис по самой статье нельзя. Автор честно называет источники своих терминов и не выдаёт заимствование за собственное изобретение: прямо говорит, что берёт и сознательно «гнёт» термины тактическое и стратегическое программирование Джона Оустерхаута, и называет Эрика Эванса автором самого подхода DDD. Прирост продуктивности от переноса тактической работы на агентов не подкреплён ни одной цифрой, сказано только качественно, «время экономится» и «значительно меньше», без конкретных часов или процентов. Наконец, автор сам обозначает границы готового: работающая часть, это стратегический слой (карта контекстов и правила сверки), перенос кода внутри контекстов на настоящие DDD-примитивы он называет следующим шагом и обещает показать «весь набор» скоро, то есть на момент публикации это анонс продолжения, а не готовый, полностью показанный инструмент.
Риски и подводные камни
Главный риск методический: вся конструкция держится на дисциплине заполнения манифеста и глоссария вручную (или скиллом) при каждом изменении, если хотя бы один репозиторий отстаёт с обновлением своей половины ребра, сверка либо не найдёт расхождение, либо начнёт заваливать issue-трекер ложными находками. Проверено пока только на одном личном проекте из двух небольших репозиториев (job-offer-box), а не на портфеле с десятками сервисов, где карта контекстов и должна приносить больше всего пользы, как схема ведёт себя при таком масштабе, из статьи не видно. Сам автор отдельно предупреждает: готова только «карта», где кончается контекст и как он говорит с соседями, а внутри любого отдельного контекста код остаётся обычным, всё ещё позволяющим собрать бессмысленный объект и сохранить его; следующий шаг, перенос кода на настоящие DDD-примитивы, ещё не сделан и не показан. И поскольку числовой оценки экономии времени или денег в статье нет, посчитать, окупает ли содержание манифестов и глоссариев тот выигрыш, который они дают, по одному этому тексту невозможно.
«Не модель нуждается в апгрейде. Не готов сам код, а готовность, это то, что можно построить. Постепенно, часть за частью.»
— автор блога coldtake.dev