AI уже неплохо пишет отдельные функции, исправляет ошибки и собирает небольшие приложения. Но чем больше проект, тем заметнее странный парадокс: прежде чем изменить несколько строк, агенту приходится прочитать сотни файлов и попытаться восстановить устройство всей системы.
Мы привыкли считать исходный код наиболее точным описанием программы. Для машины это действительно исполняемая истина. Но код не всегда хорошо отвечает на другой вопрос: почему система устроена именно так?
Где в репозитории заканчивается намеренная архитектура и начинается исторически сложившаяся реализация? Какая ветка выполняется параллельно? Какой таймаут является частью контракта, а какой случайно оказался в конфиге? Какие зависимости допустимы, а какие появились как временный обходной путь три года назад?
Разработчик постепенно узнаёт всё это из документации, обсуждений и опыта работы с системой. AI-агент видит в основном код и каждый раз проводит небольшое архитектурное расследование.
Мне стало интересно: что, если проблема не только в размере контекстного окна или качестве модели? Возможно, сам репозиторий является слишком низкоуровневым интерфейсом для совместной работы человека и AI.
Ниже — инженерная гипотеза, которую я пытаюсь проверить на практике: для совместной работы разработчика и AI системе нужен отдельный архитектурный слой между намерением и исходным кодом.
Один промпт — несколько архитектур
Представим обычную задачу: сделать сервис обработки заказов. Он принимает заказ, разбивает его на позиции, параллельно проверяет остатки через gRPC, учитывает таймаут, объединяет результаты и возвращает ответ.
Если несколько раз попросить AI реализовать такой сервис, можно получить несколько вполне правдоподобных вариантов:
-
HTTP и горутины с собственными ретраями;
-
gRPC, каналы и circuit breaker;
-
REST и worker pool с другой моделью обработки ошибок.
Каждое решение может выглядеть разумно само по себе. Проблема в том, что вместе с бизнес-логикой модель каждый раз заново выбирает архитектуру, модель конкурентности, границы компонентов и инфраструктурные паттерны.
Можно сделать промпт подробнее. Описать параллельные ветки, таймауты, ошибочные потоки, ретраи, метрики и трейсинг. Но в какой-то момент промпт начинает напоминать архитектурную спецификацию — только неформальную и допускающую разные трактовки.
С существующим проектом происходит обратный процесс. Спецификации уже нет, и агент пытается восстановить её из кода. Получается замкнутый цикл:
-
человек выражает намерение текстом;
-
AI превращает его в множество файлов;
-
при следующем изменении другой агент читает эти файлы;
-
агент пытается восстановить исходное намерение;
-
после этого снова меняет код.
Мы используем наиболее подробное представление системы как основной способ передачи её замысла.
Почему гора спецификаций не решает проблему
Сегодня много говорят о Spec-Driven Development, или SDD: сначала человек вместе с AI формулирует спецификацию, а уже затем агент пишет по ней код. Для отдельной функции или небольшого сервиса это действительно делает результат предсказуемее. Но мне кажется, что в чистом виде такой подход плохо масштабируется на большие системы.
Если каждому модулю, сценарию и архитектурному решению соответствует отдельный текстовый документ, рядом с горой кода постепенно вырастает гора спецификаций. Их тоже нужно связывать друг с другом, версионировать, поддерживать в актуальном состоянии и сопоставлять с реализацией. Перед изменением агенту придётся анализировать уже два больших массива информации и выяснять, какая спецификация ещё действует, как она связана с соседними и не расходится ли с кодом.
Поэтому более реалистичным направлением мне кажется переход к высокоуровневым DSL и формальным архитектурным моделям. Такая модель становится не ещё одним описанием поверх кода, а источником, из которого можно получать реализацию, проверки и локальную спецификацию конкретной задачи для агента.
Это не отменяет написанные человеком требования, бизнес-правила и объяснение намерения. Но техническую часть контекста — типы, допустимые зависимости, границы изменения, успешные и ошибочные пути — не приходится каждый раз пересказывать вручную. Агент получает не всю гору документации, а компактную проекцию модели, относящуюся к выбранному компоненту или подграфу.
Код точен, но находится не на том уровне
Здесь легко сделать слишком сильный вывод: «код больше не нужен, всё будем рисовать мышкой». Я так не думаю.
Код остаётся лучшим инструментом для алгоритмов, нестандартной логики, профилирования, низкоуровневой оптимизации и отладки. IDE тоже никуда не исчезнет. Вопрос не в замене кода, а в том, должен ли он оставаться единственным источником понимания системы.
У компиляторов давно существует промежуточное представление — IR. Исходная программа переводится в структуру, с которой удобно выполнять анализ, оптимизации и генерацию машинного кода. Пользователю обычно не приходится работать с IR напрямую, но именно оно разделяет смысл программы и особенности конкретной платформы.
Похожий слой может оказаться полезным и на уровне архитектуры:
человек / AI ↓архитектурное представление ↓валидация и политики ↓генерация реализации ↓runtime и observability
Такое представление должно быть достаточно наглядным для человека и достаточно формальным для машины. Например, типизированный граф, в котором узлы описывают операции, рёбра — движение данных и семантику вызовов, а отдельные выходы — успешные и ошибочные сценарии.
Здесь возникает справедливый вопрос: разве языки описания архитектуры уже не существуют? UML, C4, ArchiMate и различные model-driven подходы давно позволяют фиксировать устройство системы. Некоторые инструменты умеют и генерировать код из моделей, поэтому сама идея генерации из диаграммы не нова.
Но на практике такие модели часто становятся отражением уже существующей реализации: их обновляют после изменения кода, используют для документации и постепенно перестают считать источником истины. Принципиальное отличие здесь не столько в нотации, сколько в жизненном цикле модели. Она должна участвовать в валидации и сборке, порождать инфраструктурный код и локальные задания для AI-агента, а затем связываться с runtime-наблюдаемостью. Расхождение между моделью и реализацией в таком случае должно обнаруживаться инструментами, а не читателем устаревшей диаграммы.
Граф в этом случае — не диаграмма, нарисованная после реализации. Это исходное архитектурное описание, из которого при фиксированных правилах генерации можно получать повторяемую реализацию.
Предсказуемость появляется не из-за запрета AI
Обычно предсказуемость AI пытаются повысить более подробными инструкциями: добавить системный промпт, правила проекта, примеры кода, список запрещённых действий. Это помогает, но не меняет основной процесс. Агент всё ещё должен понять репозиторий и самостоятельно решить, какие части затронуть.
В большой системе особенно важно не просто дать агенту больше контекста, а быстро задать границы, внутри которых он должен искать решение. Например: изменить только один компонент, сохранить входной и выходной контракты, не добавлять новые зависимости, использовать существующий порт для внешнего вызова и добавить явный путь обработки новой ошибки.
Ограничение здесь — не запрет на полезную инициативу AI. Это способ уменьшить пространство возможных решений. Чем точнее задана граница задачи, тем меньше агенту приходится угадывать и тем проще человеку проверить результат.
Такие ограничения не заменяют права доступа, sandbox, тесты и code review. Они не обеспечивают безопасность сами по себе, но существенно сокращают пространство архитектурных решений, которое приходится исследовать агенту.
Но такие границы должны задаваться быстро. Если для подготовки задания человеку сначала нужно самому изучить десятки пакетов и вручную перечислить все архитектурные инварианты, значительная часть пользы AI теряется. В формальном представлении системы границу можно задать выбором подграфа или доменного компонента, а типы, допустимые связи и зависимости уже становятся частью контракта.
Архитектурное представление тем самым меняет не только объём контекста, но и сам масштаб задачи.
Вместо «создай сервис обработки заказов» агент может получить более узкую задачу:
Task: ProcessOrderInput: OrderOutput: OrderStateFile: internal/functions/processorder.go- реализовать тело функции;- не создавать новый context;- завершить result context после ответа;- запустить тесты.
Архитектура, типы, связи и инфраструктурные зависимости уже определены. Агенту остаётся реализовать локальную бизнес-функцию.
Важно, что это не просто ограничивает модель. Это ещё и экономит её работу. Агенту не нужно повторно читать транспортный слой, искать место регистрации обработчика, выяснять схему трассировки и анализировать десятки соседних пакетов. Необходимый контекст уже находится в графе и сгенерированном контракте задачи.
Архитектурный diff вместо надежды на code review
Есть ещё одно следствие, которое кажется мне важнее самой генерации.
Сегодня человек обычно проверяет предложение AI постфактум — в виде diff по множеству файлов. Чтобы понять изменение, ревьюеру приходится проделать ту же работу: восстановить архитектурный смысл из технических деталей реализации.
При наличии формальной модели агент сначала может предложить изменение на уровне архитектуры:
+ добавлена ветка fallback~ timeout изменён с 25 до 50 ms+ появился отдельный поток PartialResult~ InventorySink заменён на ReserveInventory
Такое изменение можно проверить до генерации кода. После подтверждения генератор создаст техническую реализацию по известным правилам.
Появляется разделение ответственности:
-
человек принимает архитектурное решение;
-
валидатор проверяет структурную корректность;
-
генератор создаёт повторяемую инфраструктурную реализацию;
-
AI пишет ограниченную бизнес-логику;
-
обычные компилятор и тесты проверяют результат.
Изменение по-прежнему может быть предложено AI. Но правила корректности определяет не AI.
Почему именно граф и при чём здесь стриминг
Для исполняемого графа нужна единая модель выполнения. В своём эксперименте Open Service Architect я использую типизированные потоки сообщений.
Узел выполняет небольшое преобразование, а ребро задаёт передачу результата. Split создаёт несколько веток, FlatMap разворачивает одно значение в набор элементов, Sink вызывает внешнюю систему, Merge объединяет результаты. Параллелизм и fan-out/fan-in выражаются структурой, а не ручной комбинацией горутин, каналов и WaitGroup.
При этом стриминговая модель не означает, что любой сервис обязан выглядеть как сложный data pipeline. Обычный CRUD тоже укладывается в неё: на каждый API-метод может приходиться одна бизнес-нода. Граф в таком случае почти тривиален, но транспорт, конфигурация, метрики и трейсинг всё равно создаются по единым правилам.
Стриминг здесь не самоцель. Это механизм, который позволяет сделать архитектуру исполнимой.
Для долгоживущих процессов — саг, распределённых транзакций и компенсаций — нужен другой исполнитель, например Temporal. Архитектурный слой не обязан конкурировать с ним: он может запускать workflow через выходной порт и получать результат через входной. Один инструмент описывает интеграционный контур, другой гарантирует долговечное выполнение процесса.
Одна модель для проектирования и наблюдаемости
У архитектурных диаграмм есть известная проблема: они начинают устаревать сразу после совещания. Код меняется, а картинка остаётся в Confluence и постепенно превращается в исторический документ.
Исполнимое представление меняет ситуацию. Если runtime действительно строится из графа, тот же граф может стать картой трассировки. Путь сообщения в production отображается на структуре, по которой сервис реально выполняется.
Это даёт любопытное совпадение нескольких представлений:
архитектура = runtime-топология = trace map
Конечно, равенство не буквальное. Трейс содержит временные данные, конкретные вызовы и ошибки, а архитектура — допустимые пути. Но общая топология перестаёт быть приблизительной иллюстрацией.
До этого речь шла о возможностях, которые я уже проверяю в Open Service Architect. Следующие две идеи — доменные компоненты и несколько языков исполнения — пока являются направлениями развития, а не готовыми возможностями инструмента.
Следующий уровень — не операторы, а бизнес-смысл
У графового подхода есть очевидная опасность: заменить текстовый код визуальным. Пока узлов шесть, схема читается мгновенно. Когда их двести, получается «визуальная лапша», которая ничем не лучше большого файла.
Поэтому одних операторов недостаточно. Нужны композиция, иерархия и переиспользуемые доменные компоненты.
Например, операция ReserveInventory внутри может состоять из проверки входа, запроса остатков, резервирования и преобразования ответа. Но снаружи она должна выглядеть как один компонент с явными портами:
-
команда резервирования;
-
успешный результат
Reserved; -
бизнес-ошибка
Unavailable; -
техническая ошибка
InventoryFailure.
На следующем уровне из таких операций собирается PlaceOrder: проверить заказ, зарезервировать остатки, рассчитать цену, авторизовать оплату и сохранить результат.
В основании этой иерархии по-прежнему находится сгенерированный код и runtime. Над ним располагаются операторы как исполняемые примитивы, затем потоковый граф, доменные компоненты и, наконец, топология приложения. Каждый следующий слой скрывает технические детали, но не отрывается от реальной реализации.
Здесь особенно хорошо видна польза для AI. Названия Map и FlatMap сообщают механизм, но почти ничего не говорят о намерении. ReserveInventory и AuthorizePayment дают агенту доменный контекст ещё до чтения реализации.
Стрим становится механизмом, а доменный компонент — единицей архитектуры.
Архитектура не должна зависеть от языка исполнения
Если граф действительно описывает архитектуру, возникает следующий вопрос: почему он должен быть привязан к одному языку?
Одна и та же операция может исполняться:
-
на Go в обычном backend-сервисе;
-
на Python рядом с ML-моделью или исследовательским pipeline;
-
на C++, если важны микросекунды, кодеки или нативные библиотеки.
Это не означает, что реализацию можно механически перенести между языками и получить одинаковые эксплуатационные свойства. У каждого runtime свои библиотеки, модель конкурентности, управление памятью и способы диагностики.
Но архитектурные контракты, топология, правила валидации и модель observability могут оставаться общими. Язык тогда выбирается под задачу, а не диктуется способом описания системы.
Где подход может не сработать
Было бы нечестно закончить на красивой схеме. У архитектурного IR есть сложные вопросы, и некоторые из них пока не имеют окончательного ответа.
Как избежать визуальной сложности? Нужны подграфы, доменные компоненты, сворачивание деталей и хорошие правила границ. Без этого граф быстро перестаёт помогать.
Где проходит граница между сгенерированным и ручным кодом? Если повторная генерация перезаписывает изменения разработчика, инструменту перестают доверять. Нужны явно разделённые зоны: инфраструктурный код принадлежит генератору, бизнес-функции и расширения — разработчику.
Как работать с существующими системами? Полная миграция почти никогда не реалистична. Практичнее описывать графом отдельный интеграционный контур и постепенно оборачивать существующие фрагменты в типизированные компоненты.
Не станет ли DSL новой формой vendor lock-in? Чем более стандартным остаётся результат — обычный код, обычный бинарник, OpenTelemetry, стандартные протоколы, — тем ниже этот риск. Но полностью он не исчезает: архитектурная модель сама становится зависимостью.
Можно ли выразить всё? Вероятно, нет — и это нормально. У системы должен оставаться escape hatch в обычный код. Ценность ограниченного языка как раз в том, что он не пытается описать абсолютно любую конструкцию.
Вместо заключения
Мне кажется, разработка действительно движется от ручного редактирования каждого файла к работе на более высоком уровне. AI ускоряет этот переход, но одновременно показывает ограничение нынешней модели: агент умеет писать код быстрее, чем понимать большую систему.
Увеличение контекстного окна поможет, но не устранит саму необходимость каждый раз восстанавливать архитектурный замысел из реализации.
Поэтому мне интересна идея общего архитектурного представления — слоя между намерением и кодом, с которым могут работать и человек, и AI. Человек видит структуру и принимает решения. Агент получает компактный контекст и ясные границы. Валидатор проверяет инварианты. Генератор создаёт повторяемую реализацию. Runtime связывает ту же модель с наблюдаемостью.
Open Service Architect — моя попытка проверить эту идею на практике, начиная с Go-сервисов и типизированных потоков.
MVP, на котором я проверяю эти идеи, опубликован в репозитории gorundebug/servicelib. Он реализует пока только часть описанного подхода, но типизированный граф уже служит в нём исполняемым архитектурным описанием: из графа генерируется сервис, а вместе с ним — небольшие локальные SDD-задачи для AI-агента с готовым контрактом и границами конкретного изменения.
Но сам вопрос шире конкретного инструмента:
если AI становится полноценным участником разработки, должен ли исходный код оставаться единственным языком, на котором мы объясняем ему устройство системы?
ссылка на оригинал статьи https://habr.com/ru/articles/1061672/