
AI прямо сейчас наступает на пятки разработчикам. У кого-то это вызывает иронию, кому-то помогает писать код. Но как ни крути, LLM создали прецедент, который громко заявил о себе и продолжает широко шагать по миру, сотрясая заголовки новостей и видео.
Меня зовут Рустам Курамшин, я работаю в IT более 10 лет, и мне как бэкенд-разработчику феномен LLM сначала казался больше игрой, чем реальным инструментом разработки. Все изменилось, когда я вырвался из проектов, предоставляющих опосредованный доступ к сервисам известных языковых моделей, и начал пользоваться официальными сервисами. Последние пару лет я активно использую ChatGPT для обучения, разработки и просто чтобы пообщаться о жизни.
А еще LLM помогает мне и моей хакатонной команде Java Boys уверенно побеждать на хакатонах. Опытом нужно делиться, так что ловите историю одной из наших побед. Расскажу, как мы с моими тиммейтами разработали AI-агента на Spring AI и API ChatGPT и выиграли полмиллиона на хакатоне МТС True Tech Hack 2025.
Хакатоны сегодня

Сидишь за кодом, расставляешь аннотации Spring Boot в проекте, и тут раздается звонок в дверь. На пороге стоит девушка с татуировкой кролика и предлагает тебе проследовать с ее компанией в загадочный и неизвестный мир хакатонов.
Сейчас хакатоны — это матрица. Не так много разработчиков, как хотелось бы, участвуют в таких соревнованиях. По опыту из близкого и не очень далекого окружения моих многочисленных коллег о хакатонах вообще хоть что-то слышали только единицы. А жаль, ведь это отличный способ для изучения новых технологий и развития навыков командной работы. Команда — это главное, но об этом позже.
О первом своем хакатоне зимой 2023 года я случайно узнал из поста в Telegram. До этого такие мероприятия казались мне чем-то непонятным или связанным с хакингом. Тогда я еще не знал, что CTF-соревнования — это отдельная дисциплина и предметная область. На скорую руку сколотил команду из своих коллег, с которыми на тот момент работал в ConTech-стартапе, и мы ушли на две ночи кодить. С тех пор для меня открылся этот дивный новый мир хакатонов! Как он устроен сейчас?
Провайдеры хакатонов

Сделать хорошее и яркое мероприятие — та еще задача. Для начала вам нужно найти участников. Где вы будете искать людей с мотивацией разрабатывать проекты в нерабочее время без четких гарантий что-то выиграть? Вузы? Окей, студентам это интересно. Но одна из целей, которую хотят достичь крупные компании через хакатоны, — это возможность узнать о новых технологических решениях в современной разработке. Это полезно для расширения технологического кругозора внутри, и тут уже нужны действующие разработчики.
На помощь приходят комьюнити, сформированные компаниями — организаторами хакатонов. Если на ваш хакатон придет 10 человек, это провал. Нужно, чтобы их были сотни, а лучше тысячи. Тогда будет настоящий ажиотаж и накал страстей.
Самые известные компании, которые помогают организовывать такие соревнования, — хакатоны.рус и Сodenrock. Мне нравится, что сейчас это область развивается и есть проекты, которые могут конкурировать между собой.
Технические затраты на проведение хорошего хакатона достаточно серьезные:
Нужен маркетинг мероприятия, чтобы привлекать лучшие умы.
Нужен красивый лендинг для уверенного присутствия в сети.
Нужна студия для ведущих и организаторов, чтобы вести трансляцию. Если хакатон проходит офлайн, нужно большое помещение для участников.
Нужна платформа для регистрации команд, размещения заданий и загрузки решений.
Нужны эксперты и волонтеры, которые будут вести участников на всех этапах от старта до финала.
Вывод: хороший хакатон — это дорого.
Где отслеживать хакатоны
Для меня есть один работающий источник — Telegram. Поделюсь любимыми каналами:
Именно здесь идет основной поток новостей. Следить за ними, чтобы отлавливать интересные соревнования, очень просто. Мы пользуемся сами и вам советуем.
Хакатон МТС True Tech Hack 2025

МТС в последнее время устраивает добротные мероприятия для комьюнити. Этой весной в апреле они провели True Tech Hack 2025 — соревнование, о котором лучше всего говорят его цифры:
призовой фонд — 1 500 000 руб.;
более 2000 регистраций;
более 100 загруженных решений;
8 команд-победителей.
И это только по внешнему треку. Всего их было два: для внутренних сотрудников и для внешних специалистов, подробно об этом писали тут.
Как человек, повидавший множество таких мероприятий, могу сказать, что компании удалось хорошо организовать хакатон. Тут были маркетинг, лендинг, платформа для участников, стандартная папка с чатами в Telegram для поддержки, офлайн-финал в Москве.
Самое интересное, конечно, это задачи: они вращались вокруг разработки AI-агентов на базе LLM и анализа данных. Компания принесла не только задачи, но и ИТ-платформу The Platform — на хакатоне можно было вплотную поработать с ее инструментами.
Задачи хакатона
Задачи, или, как иногда говорят, «треки», были привязаны к инструментам и стримам платформы:
MWS Data. Разработка решения для автоматизации процессов сбора данных из открытых источников, их обработки и создания аналитических отчетов на основе набора инструментов MWS Data.
MWS Octapi. Разработка решения на базе ИИ, автоматизирующего процесс создания JSON-схем для описания бизнес-логики и интеграций.
MWS Tables. Автоматизация процессов работы с данными. Создание единой экосистемы на базе MWS Tables для минимизации дублирования задач и повышения прозрачности операций.
Product Factory. Разработка AI-ассистента, который анализирует интерфейсы мобильных приложений, реагирует на касания и жесты пользователя и преобразовывает визуальные элементы в голосовые подсказки.
MWS GPT. Разработка мультиагентной системы на базе языковых моделей, которая помогает оператору контакт-центра в реальном времени.**
Когда я открыл лендинг хакатона, у меня возник синдром самозванца. Для меня и моей команды тема разработки проектов на базе AI и ML всегда была чем-то из другого мира, ведь мы занимаемся разработкой классических web-приложений. Но интуиция не подвела. Оказалось, что при наличии удачи и погружения в тему можно взять задачу AI Schema Builder из трека MWS Octapi и выполнить ее с помощью интеграции с API LLM и Spring AI, о котором я тогда знал только в общих чертах. Да и в описании задачи было знакомое до боли слово JSON. Ну и кто мы, java backend-разработчики, если не CRUDовщики — создатели джейсономешалок?
Как работает команда Java Boys
На таких мероприятиях мы именно работаем. За первый проигранный хакатон было очень обидно. Мы разработали классную бэкендовую архитектуру, сделали работающий проект, но фронтендеры, которых мы нашли по объявлению, подкачали — и мы не прошли в финал, хотя жюри отметило наше решение. С тех пор я сделал выводы.
Самое важное — это команда. Она должна состоять из людей, которых я лично знаю и с которыми я работал в реальных коммерческих проектах. Организация работы команды — первый приоритет.
В будущем именно такой подход помог нам занимать призовые места и писать интересные проекты.
Все ребята из команды — Java-разработчики, простое и понятное название на это намекает. У нас разный опыт и бэкграунд в разработке, и это дает нам широкий кругозор внутри команды.
По дефолту мы все пишем на Java. В нашей команде нет потребности в аналитиках, продактах, UI/UX дизайнерах, тестировщиках и прочих ролях. Все достаточно самостоятельные люди в разработке со своим видением работы над проектом.
А теперь раскрою несколько профессиональных секретов на примере проекта Vibe JSON, который мы написали на хакатоне МТС. Покажу, как была организована наша работа и какие инструменты мы использовали. Это хорошее подспорье, чтобы понять, как эффективно распределять усилия и побеждать.
Дисциплина и командная работа

Без поддержания порядка не получится написать работающий проект в сжатые сроки. Нужно принять внутреннее соглашение, что хакатон — это работа, и обычно достаточно напряженная. А поскольку мы ей занимаемся в нерабочее время, то будут и соответствующие неудобства в виде усталости и недостатка сна.
Как я уже писал выше, команда — это главное. Побеждают не хорошие программисты или крутые технологии — побеждает именно она. Если у вас нет сплоченного коллектива, вы можете рассчитывать только на себя. Но если пишешь проект с людьми, в которых ты уверен, если знаешь, что они не сольются на второй день хакатона, тогда у тебя все хорошо.
Считайте, что это как компьютерная игра Overcooked. Вы работаете совместно на кухне, а вокруг происходит This is fine.
Брейншторм задачи

Мы используем Miro, но это не единственное решение. Смысл в том, что команда собирается и разбирает условия задачи, зачитывает их несколько раз. Цель — осознать, что именно требуется разработать.
Первый этап — это брейншторм. Можно добавлять на доску стикеры с произвольными идеями и ключевыми смыслами. Эти вещи должна видеть вся команда, чтобы настроить свое мышление и начать генерировать идеи. Пишите на доске все, что в этот момент создает ваш мозг. Главное, чтобы через определенное время вы увидели свет в конце тоннеля воображения и поняли, как писать свой проект.
Когда очертания решения сформированы, нужно перейти к архитектурному проектированию. Придумать архитектуру вашего проекта и понять, как он будет работать.

У Vibe JSON была простая архитектура для реализации требований задачи, но так бывает не всегда
Перед началом разработки нужно определиться с моделью данных и понять, какие таблицы, entity-классы будут в проекте. Иногда этот этап может занимать существенное время.
Таск-трекер для распределения задач
Дальше нужно сделать декомпозицию задач и напилить таски. Все как в обычном проекте разработки. Можно использовать Miro:

Но есть классный инструмент — GitHub Projects. Вы все равно будете хостить код проекта на GitHub, почему бы там же не управлять задачами?

Когда проект разделен на задачи, можно начинать писать код.
Как бэкендерам писать проект на Java и не беспокоиться о фронтенде?

Кажется, выбор стека разработки для Java-проекта вполне очевиден. Нам достаточно взять Java/Kotlin, Spring Framework, PostgreSQL (прочие хранилки, кэши, очереди, если нужно). Но что делать с фронтендом, если не все в команде знают JavaScript/TypeScript и React? Конечно, можно позвать в команду надежных фронтов, но если у вас таких нет?
Тут я хочу рассказать о Jmix — это фреймворк для Spring Framework, который основан на Vaadin и позволяет разрабатывать full stack приложения на Java. Я уже много чего успел поразрабатывать на Jmix для разных задач. Могу сказать, что это спасение, когда нужно сделать фронтенд без фронтендеров. Ты разрабатываешь в привычной парадигме Spring, но у тебя появляется UI!
Подписка на лучшие LLM-сервисы и их API

Я не продаю подписки на LLM, но без качественных моделей не получится написать хороший проект, который будет работать с API LLM. Да и не только с API. На хакатоне можно столкнуться со сложными нетривиальными задачами, связанными с разработкой. И у меня такое было — например, когда нужно было читать MongoDB change stream и писать в WebSockets, работать с блокчейном и API его нод доступа, запускать docker-контейнеры из java-кода, работать с локальными git-репозиториями из java-кода и прочими необычными вещами, которые ты придумываешь в рамках разработки на хакатоне.
Великий Google и Stackoverflow уже не помогут так быстро. Поможет навык быстро решать вопросы с помощью LLM. Если ты умеешь писать код с их помощью, ты банально получаешь преимущество в скорости разработки, а на хакатонах скорость критична. К тому же хорошая LLM и навык писать промпты помогут быстро сделать сильную презентацию для питч-сессии.
Кроме прочего, можно использовать API больших языковых моделей, чтобы создавать на их основе бизнес-функциональность в своем проекте. Что мы успешно и начали применять в разработке.
Кроме репо на GitHub нужен еще и CI
Выше я упоминал, что у нас свои серверы. Если вы разрабатываете пет-проекты или участвуете в хакатонах, для вас это тоже отличный вариант.

На фото выше — мой кластер одноплатных компьютеров. Когда-нибудь я напишу о нем отдельный текст. Это удобная домашняя сетевая лаборатория, при этом очень мощная по своим характеристикам. Так мне не нужно думать о цене за облако. Статический IP у провайдера стоит недорого. Дальше я регистрирую свой домен — и дело в шляпе. Остается накатить на все хосты Linux, настроить окружение и сетевую конфигурацию на маршрутизаторе — трансляцию сетевых адресов через NAT. На моем кластере задеплоены все проекты с хакатонов.
Дальше нужно написать CI-пайплайн, чтобы инкременты разработки проекта автоматически деплоились на сервер. И здесь очень удобен GitHub Actions. Необязательно писать суперавтоматизацию. Достаточно базовой возможности выкатывать проект на сервер автоматически без ручных манипуляций, и чтобы это мог делать любой участник команды. Вот небольшой пример простого CI-пайплайна, который деплоит проект:
name: CI/CD Pipeline for Vibe-JSON Project
on:
pull_request:
branches:
- main
- 'feature/**'
workflow_dispatch:
inputs:
branch:
description: 'Branch name to deploy'
required: true
default: 'main'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'adopt'
- name: Build with Gradle
run: |
./gradlew clean build -x test
./gradlew -Pvaadin.productionMode=true bootJar -x test
deploy:
needs: build
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch'
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Deploy to Server
uses: appleboy/ssh-action@master
with:
host: kuramshin-dev.ru
username: ubuntu
port: 50151
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /home/ubuntu/vibe-json
./deploy.sh ${{ github.event.inputs.branch }}
Это деплой с запуском контейнеров на linux-сервере. Если вы хотите более продвинутых решений, например, на основе kubernetes, это тоже не проблема. Можно написать пайплайн, где kubectl или helm будут деплоить ваш проект в k8s.
Что самое главное в деплое проекта на сервер и публичном доступе к нему? Жюри сможет посмотреть проект, просто кликнув URL. Им не придется разбираться с инструкциями по локальному развертыванию на своей системе проекта участника. Здесь можно возразить, что, мол, есть docker-compose. Но когда ваш проект использует внешние интеграции с LLM, встает вопрос, как передавать токены и так далее.
Презентация и выступление на питч-сессии всё решают

В хакатоне побеждает презентация и немного — ваш проект и его код. Это может звучать абсурдно, но это так. На питч-сессии в финале хакатона вы приходите продавать ваш проект, и от умения его презентовать с помощью речи и презентации зависит почти все. Тут уже не имеет значения, насколько вы хорошие или плохие разработчики. Мы всегда покупаем обложку и идею, а только потом разбираемся, что там внутри.

Вывод такой: если хотите побеждать, вам придется вплотную познакомиться с PowerPoint и научиться делать интересные слайды. Потом — еще интереснее, вам точно нужно будет репетировать выступление на питч-сессии, и чем больше раз вы это сделаете, тем лучше. Советую заранее подготовить свою речь, расписать план, что именно и когда вы будете говорить. Это поможет структурировать информацию, справиться с волнением на сцене и не упустить самого важного. Записывайте себя на видео, чтобы понимать, как вы звучите и выглядите со стороны.
AI Schema Builder: генерация схем
Всего на хакатоне было пять задач, мы выбрали трек от MWS Octapi — платформы для надежной и безопасной интеграции высоконагруженных систем в разнородных ИТ-ландшафтах. Кратко задача звучала так:
AI Schema Builder: генерация схем. Разработай решение на базе ИИ, автоматизирующее процесс создания JSON-схем для описания бизнес-логики и интеграций
В MWS Octapi есть такая необычная штука, как Low-Code-система для реализации интеграций на бэкенде. Она представляет из себя некий workflow, DAG, граф, BPMN-диаграмму. Можете взять любой знакомый вам термин. Часто это называют DAG (Directed Acyclic Graph). Его узлы в контексте задачи называются Activity, а сам граф называется workflow.

Workflow описывает выполнение шагов интеграции на бэкенде. Например, получи выписку с банковского счета на почте, отправь ее по REST в сервис A, потом отправь ее данные в топик Kafka. Каждый шаг — это отдельный Activity. Если вы слышали про Camunda и движки бизнес-процессов, вам не нужно объяснять, что это такое.
В MWS придумали классную штуку. Чтобы реализовать интеграции на бэкенде, не нужно писать код — нужно описать JSON для специальной Low-Code-системы. Она получает на вход ваш JSON, описывающий процесс интеграции, и начинает как бы его воспроизводить, проигрывать, как магнитофон проигрывает кассету (да-да, были такие времена с кассетами).
Но в компании пошли еще дальше и стали генерировать этот JSON с помощью диалога с LLM. То есть условный системный аналитик пишет LLM в чате, какую интеграцию он хочет реализовать. LLM задает уточняющие вопросы и в конце генерирует валидный JSON, который можно скормить Low-Code-системе.
К задаче прилагалась достаточно внушительная спецификация этого JSON — 150 страниц текста и таблиц. Такой объем инфы LLM не сможет переварить в качестве контекста вашего запроса, и делать тут RAG, скорее всего, не хватит времени.
Итак, в задаче на хакатоне требовалось разработать такое решение на базе LLM, которое позволит генерировать JSON для Low-Code-системы на основе диалога с пользователем.
Как мы явили миру Vibe JSON
Ранее я показывал архитектуру Vibe JSON. Проект устроен просто: Java 17, Spring Boot, Jmix, PostgreSQL, Spring AI, Docker. Но все дело в Spring AI. Без него разработать годный прототип в такие короткие сроки не получилось бы.
У Vibe JSON достаточно простой UI.

Пользователь описывает, какую интеграцию он хочет реализовать. ChatGPT задает ему уточняющие вопросы. После ряда шагов формируется JSON для Low-Code-системы.
Чтобы вам было легче разобраться в реализации проекта, я напомню некоторые аспекты разработки AI-агентов, которые сейчас делают LLM не просто приятным собеседником, а инструментом создания автоматизации. Поскольку я использую API OpenAI Platform, буду опираться на их терминологию.
Немного терминов
AI-агент — это программа или система, которая может самостоятельно принимать решения и выполнять действия, направленные на достижение определенной цели, используя методы искусственного интеллекта.
JSON — один из самых широко используемых форматов для обмена данными между приложениями во всем мире.
Функция Structured Outputs (структурированный вывод) гарантирует, что модель будет всегда генерировать ответы, соответствующие заданной JSON-схеме. Это избавляет от необходимости проверять наличие всех обязательных ключей или следить за тем, чтобы значения соответствовали допустимым перечислениям.
Function calling, или вызов функций, позволяет моделям получать данные и выполнять действия. Вызов функций — это способ интеграции моделей с вашим кодом или внешними сервисами. Вы можете предоставить модели доступ к своему коду с помощью механизма вызова функций. На основе системного промпта и предыдущих сообщений модель может принять решение о вызове определенной функции — вместо (или помимо) генерации текста.
Здесь нужно понять одну ключевую особенность: модель должна быть хорошо обучена работать с JSON-схемами, ведь фактически Structured Outputs и Function calling связаны с передачей модели JSON-схемы и парсинга JSON-а из ответа модели. И Spring AI отлично справляется с этими задачами.
Начать работать со Spring AI несложно, есть документация.
Я не буду показывать очевидные разделы проекта Vibe JSON, в конце публикации будет ссылка на репозиторий и вы при желании сможете посмотреть код самостоятельно. Я покажу интересные, по моему мнению, места в коде, чтобы подчеркнуть возможности Spring AI.
Решение этой задачи именно на Java дает определенное преимущество: если прилагаемую к задаче 150-страничную документацию к JSON не перевести в Java-классы, ничего не получится. Это стало отправной точкой в разработке проекта.
Экскурсия по исходному коду проекта
Все начинается с описания интеграционного workflow:
@Data
@EntityDescription("Определение рабочего процесса")
@JsonIgnoreProperties(value = {"flowEditorConfig"})
public class WorkflowDefinitionDto {
@NotBlank
@Size(max = 255)
private String type = "complex";
@NotBlank
@Size(max = 255)
private String name;
@Size(max = 4000)
private String description;
private Integer version = 1;
private String tenantId = "default";
@NotNull
@Valid
private DetailsDto details;
@Valid
private CompiledDto compiled;
}
Дальше в проекте идет 65 (!) java-классов, описывающих вложенные структуры рабочего процесса. Два человека из нашей команды занимались переводом спецификации в java-классы. Пришлось немного попотеть, но и здесь ChatGPT тоже помогла немного ускориться, хоть и не всегда с первого раза.
Стоит отметить, что некоторые поля классов workflow размечены аннотациями jakarta.validation.constraints. Потом это будет иметь значение при реализации function calling — механизма, который позволяет вызывать ChatGPT методы в нашем коде.
К задаче прилагались несколько json-файлов с примерами workflow. Понадобилось написать тесты десериализации, чтобы гарантировать, что наш проект будет работать с корректной структурой workflow, потому что участники жюри будут проверять работу сервиса на реальной Low-Code-системе у себя в компании. Тесты дают гарантии, что мы не разойдемся в ожиданиях.
@Slf4j
public class WorkflowDeserializationTest {
private final ObjectMapper objectMapper = new ObjectMapper()
.registerModule(new JavaTimeModule())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true)
.configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, true)
.configure(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, true);
private <T> void assertJsonMatchesDto(File jsonFile, Class<T> dtoClass) {
try {
objectMapper.readValue(jsonFile, dtoClass);
} catch (UnrecognizedPropertyException e) {
String shortMessage = String.format(
"В файле [%s] обнаружено неизвестное поле: \"%s\" в классе [%s]",
jsonFile.getName(),
e.getPropertyName(),
e.getReferringClass().getSimpleName()
);
fail(shortMessage);
} catch (IOException e) {
fail("Ошибка при десериализации " + jsonFile.getName() + ": " + e.getMessage());
}
}
@Test
@DisplayName("wf-1.json должен корректно десериализоваться в WorkflowDefinitionDto")
public void testWf1Json() {
File file = new File("src/test/resources/workflows/wf-1.json");
assertJsonMatchesDto(file, WorkflowDefinitionDto.class);
}
//... прочие тесты на другие json-файлы
}
Когда у тебя столько java-классов моделей в проекте и идет достаточно интенсивная разработка, хочется быстро проверять, что все 65 классов связаны между собой. Да, можно каждый раз открывать пакет и руками проходить по списку классов в IDEA. Но мы, разработчики, ленивые люди. Всегда хочется какой-то автоматизации.
Достаточно давно для автоматизации работы со своими проектами я пишу скрипты на Bash или Groovy. И да, Groovy жив как никогда. Я очень люблю и ценю этот JVM-язык, пишу на нем скрипты, и меня особо не беспокоят расхожие выражения, что Groovy устарел или что он мертв. Это не так.
Чтобы быстро чекать связность классов workflow, я сделал скрипт на Groovy:
#!/usr/bin/env groovy
if (args.length != 1) {
println "Usage: ./analyzePackage.groovy path/to/package"
System.exit(1)
}
def packagePath = args[0]
def baseDir = new File(packagePath)
if (!baseDir.exists() || !baseDir.isDirectory()) {
println "Invalid package path: $packagePath"
System.exit(1)
}
// Рекурсивно собираем все .java файлы
def getJavaFilesRecursively = { File dir ->
def javaFiles = []
dir.eachFileRecurse { file ->
if (file.name.endsWith(".java")) {
javaFiles << file
}
}
return javaFiles
}
def javaFiles = getJavaFilesRecursively(baseDir)
if (javaFiles.isEmpty()) {
println "No Java files found in: $packagePath"
System.exit(0)
}
Set<String> allClassNames = []
Map<String, File> classFileMap = [:]
Map<String, Boolean> hasFields = [:]
// Собираем имена классов и проверяем на наличие полей
javaFiles.each { file ->
def text = file.text
def matcher = text =~ /class\s+([A-Za-z0-9_]+)/
if (matcher.find()) {
def className = matcher.group(1)
allClassNames << className
classFileMap[className] = file
def fieldMatcher = text =~ /(?:private|protected|public)?\s+(?!class|interface)[\w<>]+\s+\w+\s*(=|;)/
hasFields[className] = fieldMatcher.find()
}
}
Set<String> usedClasses = [] as Set
// Ищем упоминания классов в других файлах
javaFiles.each { file ->
def text = file.text
allClassNames.each { className ->
if (text.contains(className) && !file.name.contains(className + ".java")) {
usedClasses << className
}
}
}
println "\n? Классы без полей (пустые):"hasFields.each { className, hasField ->
if (!hasField) {
println " - $className (${classFileMap[className].path})"
}
}
println "\n? Неиспользуемые классы (в рамках пакета):"(allClassNames - usedClasses).each { unusedClass ->
println " - $unusedClass (${classFileMap[unusedClass].path})"
}
Да, он не идеальный и может иногда давать ложные срабатывания, но это редкие кейсы. Скрипт запускается командой:
groovy analyzePackage.groovy src/main/java/ru/javaboys/vibejson/wfdefenition
Теперь, у нас есть классы, описывающие модель данных выходного workflow. Осталось дело за малым — реализовать AI-агента.
В этом проекте у агента минималистичная функциональность. Но поскольку используется structured output и function calling — это именно агент, а не просто чат с AI-ассистентом.
Spring AI можно достаточно быстро затянуть в проект на Gradle (с Groovy DSL):
// ...
repositories {
mavenCentral()
maven {
url 'https://global.repo.jmix.io/repository/public'
}
maven { url 'https://repo.spring.io/milestone' }
maven { url 'https://repo.spring.io/snapshot' }
maven {
name = 'Central Portal Snapshots'
url = 'https://central.sonatype.com/repository/maven-snapshots/'
}
}
// ...
ext {
set('springAiVersion', "1.0.0-M7")
}
// ...
implementation platform("org.springframework.ai:spring-ai-bom:1.0.0-SNAPSHOT")
implementation 'org.springframework.ai:spring-ai-starter-model-openai'
implementation 'org.springframework.ai:spring-ai-starter-model-chat-memory-jdbc'
// ...
dependencyManagement {
imports {
mavenBom "org.springframework.ai:spring-ai-bom:${springAiVersion}"
}
}
Использование Gradle и Groovy DSL обусловлено Jmix.
Алгоритм работы AI-агента в этом проекте можно представить такой схемой:

Теперь давайте детальнее рассмотрим элементы этого процесса.
Первое, что нам нужно, — это системный промпт, который будет управлять поведением AI-агента. Как известно, промпты рождаются в муках — и этот не был исключением:
Ты – AI помощник для построения интеграционных бизнес-процессов.
Ты можешь обсуждать только вопросы и задачи связанные с формированием workflow.
Твоя основная задача - преобразовывать интеграционный бизнес-процесс, описанный пользователем с помощью текста, в структуру workflow.
Допустимые типы Starter (стартеры): {allowedStarterTypes}.
Допустимые типы Activity (активити): {allowedActivityTypes}.
Активити - это те кубики, из которых строится основная логика работы интеграционных бизнес-процессов.
Стартеры - это способ запуска интеграционного бизнес-процесса.
Твой алгоритм по формированию workflow из текстового описания пользователя:
- Построй список активити в соответствии с доступными активити.
Сам выбирай активити в соответствии с описанием интеграционного бизнес-процесса, который указывает пользователь и названием активити.
В соответствии со структурой требуемых активити, описанных в workflow, уточни параметры недостающие для заполнения полей активити.
- Задай вопрос какие стартеры использовать для запуска интеграционного бизнес-процесса, над которым ты работаешь.
В соответствии со структурой требуемого стартеров, описанного в workflow, уточни параметры недостающие для заполнения полей стартера.
- Если не хватает данных (например, отсутствует обязательный параметр), задавай уточняющие вопросы.
- Некоторые поля workflow можешь заполнять подходящими по контексту значения на своё усмотрение.
- Для валидации workflow используй инструмент validateWorkflow, который возвращает признак валидности и список ошибок, если workflow не валидный.
Если были обнаружены ошибки валидации, то на основе описания этих ошибок сформируй дополнительные вопросы для пользователя,
чтобы на их основе пользователь уточнил или исправил параметры workflow, что позволило бы пройти валидацию.
- На каждом шаге возвращай сообщение для пользователя в поле chatMessageForUser.
- Возвращай workflow только, если он был полностью сформирован. В противном случае, не заполняй это поле.
Здесь можно заметить интерполяцию переменных в текст промпта через {allowedStarterTypes}
и {allowedActivityTypes}
. У этой, казалось бы, простой операции серьезное название — prompt templates, термин получивший широкое применение в библиотеках для работы с LLM. Он нужен, чтобы сделать ваши промпты не прибитыми гвоздями к конкретным значениям, а дать им возможность подстраиваться под ваши данные.
В Spring AI есть специальные классы для работы с шаблонами:
Map<String, Object> templateParams = new HashMap<>();
templateParams.put("allowedStarterTypes", String.join(",", WorkflowUtils.getAllowedStarterTypes()));
templateParams.put("allowedActivityTypes", String.join(",", WorkflowUtils.getAllowedActivityTypes()));
// ...
SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(systemText);
return systemPromptTemplate.createMessage(templateParams);
Теперь мы можем объединить системный промпт с сообщением пользователя из чата и отправить запрос в API OpenAI Platform:
public LLMResponseDto processUserMessage(String sessionId, String userMessage, String currentWorkflow) {
Message systemMsg = createSystemInstruction(currentWorkflow);
Message userMsg = new UserMessage(userMessage);
List<Message> promptMessages = new ArrayList<>();
promptMessages.add(systemMsg);
promptMessages.add(userMsg);
return chatClient
.prompt(new Prompt(promptMessages))
.advisors(advisor -> advisor.param("chat_memory_conversation_id", sessionId)
.param("chat_memory_response_size", 100))
.tools(workflowTools)
.call()
.entity(LLMResponseDto.class);
}
Пока не смотрим на advisor. Самое интересное здесь — это entity(LLMResponseDto.class)
. Это тот самый Structured Output, который в этом проекте позволяет получать от ChatGPT одновременно workflow и следующее сообщение для пользователя с помощью класса LLMResponseDto:
@Setter
@Getter
@Data
public class LLMResponseDto {
private String chatMessageForUser;
private WorkflowDefinitionDto workflow;
}
Использование .entity()
спасает от написания вагона бойлерплейт-кода, который нужен, чтобы заставить ChatGPT работать с JSON-схемой и отдавать ответ в виде JSON. Я знаком с этим не понаслышке, потому что в первых версиях этого проекта делал structured output «голыми руками» с помощью расширенного промпта, библиотеки для работы с json-схемами и парсинга ответа через regex. Хорошо, что Spring AI может взять на себя эту работу и вдобавок еще обрабатывать ошибки десериализации.
Следующая проблема, которую предстояло решить, — получение валидного JSON workflow. Конечно, .entity()
гарантирует получение ответа в виде инстанса класса LLMResponseDto. Но проблема в том, что там внутри 65 классов и ChatGPT попросту может забыть заполнить часть полей или даже блоков workflow.
Как заставить ChatGPT заполнять все требуемые поля? Как мы добиваемся проверки полей, когда разрабатываем REST-микросервисы? Конечно же, с помощью валидации через Bean Validation! Провернуть это с ChatGPT можно с помощью Function Calling — он позволяет LLM вызывать методы в вашем коде.
Тут нет никакой магии. Библиотека-посредник сообщает LLM, что есть определенные Tools, инструменты. Дает ей описание переменных и возвращаемого значения с помощью JSON-схемы. LLM передает библиотеке JSON, на вашей стороне происходит вызов метода в коде, ответ сериализуется и отдается LLM в виде JSON. Главное, это поддержка Function Calling самой моделью. Не все модели умеют делать это из коробки.
В итоге нам нужен tool, который позволит ChatGPT проверять, корректно ли она, по нашему мнению, сформировала workflow. Для этого мы опишем следующий инструмент:
@Data
public class ValidationResult {
private Boolean isValid;
private List<String> errors;
}
@Slf4j
@Component
public class WorkflowTools {
@Tool(description = "Метод позволяет выполнить валидацию workflow")
public ValidationResult validateWorkflow(
@ToolParam(description = "Текущий сформированный интеграционный бизнес-процесс") WorkflowDefinitionDto workflow
) {
ValidationResult result = new ValidationResult();
List<String> errors = new ArrayList<>();
if (workflow == null) {
result.setIsValid(false);
errors.add("workflow не может быть null");
result.setErrors(errors);
logValidationErrors(errors);
return result;
}
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Set<ConstraintViolation<WorkflowDefinitionDto>> violations = validator.validate(workflow);
if (!violations.isEmpty()) {
errors = violations.stream()
.map(v -> formatViolation(v))
.collect(Collectors.toList());
result.setIsValid(false);
result.setErrors(errors);
logValidationErrors(errors);
} else {
result.setIsValid(true);
result.setErrors(List.of());
}
return result;
}
private String formatViolation(ConstraintViolation<?> v) {
String path = v.getPropertyPath() == null ? "" : v.getPropertyPath().toString();
Object invalid = v.getInvalidValue();
String invalidStr;
try {
invalidStr = invalid == null ? "null" : String.valueOf(invalid);
if (invalidStr.length() > 500) {
invalidStr = invalidStr.substring(0, 500) + "...";
}
} catch (Exception e) {
invalidStr = "<unprintable>";
}
String message = v.getMessage();
return String.format("%s: %s (значение: %s)", path, message, invalidStr);
}
private void logValidationErrors(List<String> errors) {
if (errors == null || errors.isEmpty()) return;
String block = "\n================= Ошибки валидации Workflow =================\n"
+ errors.stream().map(e -> " - " + e).collect(Collectors.joining("\n"))
+ "\n=============================================================";
log.error(block);
}
}
Теперь на основе системного промпта ChatGPT вызывает этот tool. Если возникает ошибка валидации, ChatGPT задает уточняющие вопросы для пользователя, чтобы дополнить или обновить данные.
Function calling и structured output — это два кита, на которых стоит разработка AI-агентов. Третий и четвертый киты — это паттерны организации цепочки запросов в LLM и качество самой модели.
Эти приемы работы со Spring AI позволяют получить AI-агента, который возвращает увесистый JSON для Low-Code-системы в MWS Octapi.
Выше мы видели такую интересную штуку, как advisor. Адвисоры в Spring AI — это мощнейшие инструменты для управления циклами взаимодействия фреймворка и LLM. В этом проекте используется два базовых адвисора.
Первый вопрос, который у вас возникнет, когда вы будете писать проекты, основанные на LLM: как заставить API LLM запоминать сообщения, которые я ей раньше присылал? Как сделать память агента? Из коробки такой функциональности нет. Для этого вам нужно на своей стороне хранить историю переписки и передавать ее в специальном поле в запросе к LLM. Иногда это поле называется assistant.
В Spring AI для реализации памяти есть MessageChatMemoryAdvisor
. Вот как на коленке сделать память, которая будет храниться в отдельной таблице в PostgreSQL и доставаться по ключу.
Нужно создать таблицу с помощью миграции. В доках Spring AI есть раздел про память, а на GitHub есть репо от разработчиков с примерами проектов. Вот пример для Liquibase:
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd" objectQuotingStrategy="QUOTE_ONLY_RESERVED_WORDS">
<changeSet id="spring-ai-add-chat-memory" author="vibe-json">
<!-- Создаём таблицу ai_chat_memory -->
<createTable tableName="ai_chat_memory">
<column name="conversation_id" type="VARCHAR(36)">
<constraints nullable="false"/>
</column> <column name="content" type="TEXT">
<constraints nullable="false"/>
</column> <column name="type" type="VARCHAR(10)">
<constraints nullable="false"
checkConstraint="type IN ('USER','ASSISTANT','SYSTEM','TOOL')"/>
</column> <column name="timestamp" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP">
<constraints nullable="false"/>
</column> </createTable>
<!-- Создаём индекс по conversation_id и timestamp (по убыванию) -->
<createIndex indexName="ai_chat_memory_conversation_id_timestamp_idx"
tableName="ai_chat_memory">
<column name="conversation_id"/>
<column name="timestamp" descending="true"/>
</createIndex> </changeSet>
</databaseChangeLog>
Дальше нужно определить адвисор и по ключу хранить сообщения:
public AiAgentService(ChatClient.Builder chatClientBuilder, JdbcTemplate jdbcTemplate, WorkflowTools workflowTools) {
this.workflowTools = workflowTools;
var chatMemory = JdbcChatMemory.create(JdbcChatMemoryConfig.builder().jdbcTemplate(jdbcTemplate).build());
this.chatClient = chatClientBuilder
.defaultAdvisors(
new MessageChatMemoryAdvisor(chatMemory),
new SimpleLoggerAdvisor()
)
.build();
}
// ...
return chatClient
.prompt(new Prompt(promptMessages))
.advisors(advisor -> advisor.param("chat_memory_conversation_id", sessionId)
.param("chat_memory_response_size", 100))
.tools(workflowTools)
.call()
.entity(LLMResponseDto.class);
Здесь же мы видим SimpleLoggerAdvisor
— это отличный адвисор для базового логирования взаимодействия фреймворка и LLM. Он помог мне раздебажить не одну проблему. Его нужно дополнительно настроить через проперти:
logging.level.org.springframework.ai.chat.client.advisor=DEBUG
На этом с обзором проекта можно остановиться и рассказать еще немного про хакатон. Исходный код проекта лежит на GitHub.
Финал MTC True Tech Hack и питч-сессия

Финал проходил 25 апреля в Москве в Императорском яхт-клубе. Мы прибыли в назначенное время, заняли места в зале, ждали своей очереди на питч и смотрели проекты других команд.
Тут стоит сказать, что хакатоны вроде МТС True Tech Hack — самые сложные. По сути, для участников есть два важных условия: тебе должно быть 18 лет и ты должен проживать в РФ. Поэтому команды тут самые разные, много и новичков, и опытных разработчиков. Казалось бы, где сеньоры и где начинающие? Но на True Tech Hack начинающие разработчики показывали такие результаты, что в какой-то момент я уже потерял веру в нашу победу. Одна из таких команд прям на сцене получила приглашение на стажировку в МТС от CTO Александра Бардаша. Я считаю, это очень правильный и важный поступок — помогать молодым специалистам вливаться в промышленную разработку и занимать свое место в профессии.
И вот настал наш черед делать питч. Мы пытались показать лучшие стороны нашего проекта и, конечно же, мемы — какая без них презентация. Потом была пауза для нетворкинга: можно было пообщаться с другими участниками, перекусить и поучаствовать в конкурсах.

Когда началось объявление финалистов, лично я уже не верил, что мы победим. Но потом мы все-таки услышали от ведущего наше Java Boys, дружно опешили и под аплодисменты стали продвигаться в сторону сцены. Как-то так и родилась фактура для этого поста!
В заключение хочу подвести немного итогов и выразить благодарность: МТС — за хакатон, а всем участникам — за их проекты и количество вложенных в них усилий. Такие вещи не даются легко.
Итак, чтобы сделать хороший проект, нужно:
задизайнить,
спроектировать,
написать код,
ошибиться,
найти решение,
задеплоить,
отладить.
Если вы дошли в этом деле до конца, вы и ваша команда уже победили время, лень и проблемы. Это заслуживает как минимум уважения, даже если вам не удалось попасть в призеры.
Читателям я хочу посоветовать найти силы и время, чтобы собрать команду и попасть на хороший хакатон. Это подстегивает развитие в разработке и создании проектов, прокачивает ваши навыки и дает зачатки какого-то стартаперского духа. Кто знает, может, после очередного хакатона у вас родится идея своего собственного невероятного проекта и вы представите его миру. Я вам этого искренне желаю!