
Привет, Хабр! Меня зовут Иван, я DevOps‑инженер в ИТ‑холдинге Т1. В современной разработке автоматизация — это давно не «роскошь», а «базовый минимум». Однако многие команды до сих пор ограничиваются лишь простыми возможностями IDE (Integrated Development Environment), упуская потенциал генерации кода. В прошлом году я выступал с этой темой на конференции Импульс Т1. В статье расскажу подробнее, как перейти к генерации кода в масштабах проекта и зачем разбираться в синтаксических деревьях.
Зачем создавать код автоматически
Главная цель генерации кода — не просто сэкономить время разработчика, а обеспечить соответствие кода заданной схеме (например, OpenAPI). Когда генерация настроена правильно, исчезает риск рассинхронизации схемы и реализации, и вносить изменения можно значительно быстрее.
Этот подход применим во многих задачах:
создание однотипного кода, часто встречающегося в проекте;
создание API‑клиентов из различных схем: OpenAPI, AsyncAPI, gRPC и др.;
создание конфигураций (Helm, Ansible) и инфраструктуры;
миграция баз данных;
стилизация, линтинг, форматирование кодовой базы;
автоматический рефакторинг.
Уровни автоматизации: от сниппетов до среды выполнения
Внедрять генерацию кода можно на разных этапах жизненного цикла приложения:
IDE и инструменты разработки: самый простой уровень — использование общих сниппетов для генерации повторяющихся методов, файлов, функций и классов. Их поддерживает большинство IDE.
Генерация кода при разработке: избавляет от рутинных задач, примеры которых решим в этой статье.
Генерация кода для общих модулей: API‑клиенты, схемы сообщений в брокере, схемы баз данных.
Генерация кода во время сборки: встраивание URL бэкенда во фронтенд, встраивание ключей для проверки лицензии, Whitelabel‑кастомизация.
Генерация кода во время развёртывания: встраивание URL бэкенда во фронтенд, патчинг, создание инфраструктуры.
Генерация в среде выполнения: продвинутый уровень, позволяющий менять логику программы на интерпретируемом языке во время выполнения. Также это используют в no‑code и low‑code средах для обфускации или расширения возможностей языка (как, например, в PyTest, который динамически трансформирует код для улучшения отладки).
Как сделать генератор
Первый шаг при создании генератора — выбор источника данных и потребителя кода. В качестве источника данных может быть внешний API, CLI‑аргументы генератора, содержимое базы данных или инспекция существующего кода.
Выходные данные генератора также гибки: результат можно отправить в файл, вывести в консоль, передать напрямую в интерпретатор или предложить разработчику как подсказку в IDE.
Представим, что у нас есть приложение, для которого необходимо перенести список текстов из json в константы. Нам нужно сгенерировать так, как показано справа:

Воспользуемся конкатенацией. Сначала получим список переводов (получение записано в константу для упрощения примера):
TRANSLATIONS = { «hello»: “Привет, {username}!”, «welcome»: “Добро пожаловать!”, «goodbye»: “Пока, {username}!”, }
Теперь генерируем код: получаем пары ключ‑значения из словаря, записанного выше. Далее к ключу добавляем префикс MESSAGE и переводим в UPPERCASE, обёртываем значение в кавычки и выводим результат в консоль.
for key, value in TRANSLATIONS.items(): # MESSAGE_{key.upper()} = "{value}" key = "MESSAGE_" + key.upper() print(key + ' = "' + value + '"')
Если у вас есть API‑схема с рекурсией, то вы можете расширить метод и добавить рекурсивную генерацию.
Генерация с помощью конкатенации позволяет создавать код на любом языке и любой сложности, обеспечивает полный контроль результата. Однако необходимо правильно экранировать строки и контролировать отступы: нет гарантий, что сгенерированный код синтаксически верен.
Масштабируемый подход (шаблонизация)
Если генератор должен поддерживать несколько языков программирования (например, Python и JavaScript) или пользовательские шаблоны, то прямая конкатенация строк становится неудобной. В этом случае правильнее использовать шаблонизаторы (например, Jinja2). Логика работы здесь разделена: генератор отвечает за получение данных из API или файла, а шаблон определяет структуру результирующего кода.
Например, у нас есть такой шаблон:
{%- for key, value in data.items() -%} MESSAGE_{{ key | upper }} = "{{ value }}" {% endfor -%}
Загрузим его из константы:
import jinja2 environment = jinja2.Environment() template = environment.from_string(""" {%- for key, value in data.items() -%} MESSAGE_{{ key | upper }} = "{{ value }}" {% endfor -%} """)
Передадим значения в поле data шаблона и выведем в терминал:
print(template.render(data=TRANSLATIONS))
После запуска генератора получим в терминале результат.
Вы можете хранить шаблоны в отдельных папках или подгружать их из API. Это позволяет использовать один и тот же движок генератора, просто подставляя нужный шаблон в зависимости от того, какой язык требуется пользователю. Такой подход минимизирует дублирование логики и упрощает расширение функциональности. При этом всё также необходимо следить за экранированием, отступами и корректным синтаксисом.
Изменяем уже существующий код
Конкатенация и шаблонизация позволяют генерировать новый код из схемы, однако этого может быть недостаточно, если мы хотим изменить уже существующий код. В таком случае нам на помощь приходят синтаксические деревья, которые хранят кодовую структуру в формате дерева.
Допустим, у нас уже есть некая кодовая база, которую мы хотим автоматически рефакторить, например, заменив все print на logger.info. Если просто найти и заменить поиском, то можно случайно испортить комментарии.

Можно воспользоваться абстрактным синтаксическим деревом (Abstract Syntax Tree, AST). Вот пример на Python:

Можно пройти по дереву и заменить вызовы функции print на logger.info, а затем вывести дерево обратно в терминал.
import ast from ast import * code = """ # print result of operation print("Result:", 1 + 2) """ tree = ast.parse(code) for node in ast.walk(tree): if not isinstance(node, Call): continue if not isinstance(node.func,Name): continue if node.func.id != "print": continue node.func = ast.Attribute( value=ast.Name( id="logger", ctx=ast.Load() ), attr="info", ctx=ast.Load(), ) print(ast.unparse(ast_obj=tree))
AST позволяет манипулировать кодом, генерировать его с нуля, применять несколько трансформаций подряд, анализировать логику кода и принимать решения. Однако у этих деревьев есть существенный недостаток: они не сохраняют комментарии и специфические особенности форматирования. Код после трансформации становится «чище», но теряет документацию.
Управляем стилем кода
В прошлом примере мы потеряли комментарий из кода. Чтобы этого избежать, можно использовать конкретные синтаксические деревья (Concrete Syntax Tree, CST). Они содержат не только логику, необходимую для запуска кода, но и всю его структуру — от запятых до комментариев. Именно CST позволяют проводить безопасный рефакторинг внутри репозитория без потери читаемости.
Преобразуем код в CST, найдём нужный узел:

и преобразуем дерево:

import libcst as cst tree = cst.parse_module( "# Log result of operation\n" "print('Result:', 1 + 2)\n" ) class PrintToLogger(cst.CSTTransformer): def leave_Name(self, _: cst.Name, node: cst.Name): if node.value == "print": # Заменяем переменную print на logger.info return cst.Attribute( value=cst.Name(value="logger"), attr=cst.Name(value="info"), dot=cst.Dot(), ) return node transformed_tree = tree.visit(PrintToLogger()) print(transformed_tree.code)
То есть CST обладает всеми возможностями AST, но при этом сохраняет стиль и комментарии.
Будущее: LLM и Model Context Protocol (MCP)
Использование LLM для написания кода выглядит заманчиво, но имеет свои риски: модели могут ошибаться и галлюцинировать, они не имеют прямого доступа к контексту вашего репозитория.
Здесь на помощь приходит Model Context Protocol (MCP). Это протокол, позволяющий моделям взаимодействовать с инструментами разработки: анализировать код проекта, запускать CLI‑команды, получать диагностику от IDE (LSP) и автоматически запускать тесты.
Для того, чтобы сократить количество галлюцинаций LLM в кодовой базе, мы можем предоставить инструменты для генерации кода напрямую языковой модели. Некоторые агенты (например, oh my pi) уже предоставляют инструменты для того, чтобы модель могла менять кодовую базу с помощью AST. Для своей же кодовой базы можно создать скилл агента, прописав логику использования вашего CLI‑инструмента. Альтернативный вариант — реализовать аналогичный функционал через MCP‑сервер, что поможет модели взаимодействовать с кодом и вносить изменения.
С чего начать
Я советую начинать внедрять генерацию кода постепенно, увеличивая количество сгенерированного из схем или другого кода. Внедрение технологии стоит начать с форматтера кода — он приведёт кодовую базу к единому стилю, и поможет привести к такому же стилю сгенерированный код.
Если у вас есть несколько сервисов, между которыми есть сетевое взаимодействие, то стоит внедрить контракты для этого взаимодействия. В этом поможет OpenAPI‑generator, который создаёт код на множестве языков, позволяет добавлять свои шаблоны и генерировать клиентский и серверный код.
Если у вас есть множество микросервисов с аналогичной базой, то поможет Cookiecutter, который создаёт из шаблонов (произвольных) репозитории и проекты (например, микросервисы).
Генерация кода — это мощный инструмент для обеспечения масштабируемости и чистоты кодовой базы. Создание собственных генераторов требует времени и погружения в тему, но вложения окупаются за счёт минимизации рутинных задач и уменьшения количества ошибок. Главное — правильно выбирать инструмент под конкретную задачу, будь то шаблонизаторы, работа с синтаксическими деревьями или современные протоколы для интеграции с ИИ.
Если вы хотите узнать больше о применении генерации кода в реальных проектах, приглашаем вас на конференцию Импульс Т1. 19 ноября в рамках трека «Архитектура и проектирование» мы обсудим, как меняется роль архитектора в эпоху ИИ, как доносить архитектурные решения до разработки и смежных ролей и какие практики лучше работают при построении ИТ‑систем.

