Разработка Java-проекта с ChatGPT-4: от идеи до реализации
Как Java-методист с 15-летним опытом я решил проверить, как нейросеть ChatGPT-4 придумывает use cases, создает доменную модель и пишет программный код. Давайте посмотрим, способна ли эта большая языковая модель в ее текущей итерации заменить разработчика или, по крайней мере, упростить процесс разработки.
Привет, Хабр! Меня зовут Игорь Судакевич, я преподаватель-практик Учебного центра IBS по программированию на языке Java. В начале 2024 года мой коллега из IBS уже делился собственным опытом создания базовой программы с помощью ChatGPT. В том эксперименте использовалась бесплатная модель 3.5, и мы получили множество комментариев из серии «А вы бы тоже самое провернули на последней версии модели — она все делает в разы лучше!» Мол, и разработчики скоро будут никому не нужны.
В целом такое утверждение не лишено оснований: ChatGPT развивается по экспоненте, и каждая новая версия нейросети работает на порядок лучше предшествующей. Однако на ближайшие полгода, думаю, мы все еще можем выдохнуть — до релиза 5-й, а то и 6-й итерации. GPT-4 хоть и способна упростить жизнь разработчику, пока точно его не заменяет. Создание Java-проекта с ChatGPT — это длительный итерационный процесс проб и ошибок, уточнения запросов, пристальных проверок и исправления «галлюцинаций» нейросети. Впрочем, судите сами.
Первая фаза эксперимента: анализ предметной области и генерация UML
В рамках эксперимента мы решили создать с нуля образовательную платформу по типу условных Udemy или Coursera. Плюс ChatGPT в том, что это универсальная нейросеть, а значит, с ее помощью мы можем создать не только код, но и описать всю структуру проекта. По этой причине использовался именно ChatGPT, а не специализированный Copilot или Codex. Также сразу поясню, что эксперимент проводился в начале весны — до выхода новой модели GPT-4o.
Итак, пишу запрос ChatGPT:
«Я хочу создать систему Образовательная платформа: создание системы для проведения онлайн-курсов, тестирования и отслеживания прогресса студентов.
Укажи use cases для данной системы».
И получаю такой ответ:
«Для создания образовательной платформы с онлайн-курсами, тестированием и отслеживанием прогресса студентов вы можете рассмотреть следующие use cases (примеры использования):
1. Регистрация и аутентификация пользователей:
Регистрация новых пользователей (студентов и преподавателей).
Вход в систему с использованием учетных данных (логин и пароль).
Восстановление пароля.
2. Управление профилями пользователей:
Редактирование личной информации.
Изменение настроек аккаунта.
Просмотр и редактирование списка курсов, на которые подписан студент.
3. Создание и редактирование курсов:
Создание нового курса с указанием названия, описания и прочих параметров.
Редактирование существующего курса.
Добавление и редактирование модулей и уроков в рамках курса.
Назначение преподавателей для курса.
4. Управление материалами курса:
Загрузка и организация учебных материалов (тексты, видео, аудио, изображения).
Создание и редактирование тестов и заданий для студентов.
Настройка условий прохождения курса и получения сертификатов.
5. Просмотр курсов и учебных материалов:
Поиск и выбор курсов.
Просмотр уроков и изучение материалов.
Прохождение тестов и выполнение заданий.
6. Отслеживание прогресса студентов:
Просмотр своего прогресса по курсам (для студентов).
Мониторинг успеваемости и активности студентов (для преподавателей).
Формирование отчетов и статистики по курсам.
7. Взаимодействие между пользователями:
Обмен сообщениями и комментариями между студентами и преподавателями.
Уведомления о новых материалах, заданиях, событиях и т.д.
8. Управление доступом и правами пользователей:
Назначение ролей и прав доступа для пользователей (администраторы, преподаватели, студенты)».
Звучит логично, переходим к визуализации требований.
Пишу следующий запрос:
«Разработай UML-диаграмму в формате PlantUML для этих use cases.
Уточняющий запрос: могу ли я не использовать псевдонимы UC1, UC2, и т. д.? И разместить эту диаграмму вертикально, чтобы teacher был под student?»
Получаю такой ответ:
@startuml
actor Student as student
actor Teacher as teacher
actor Administrator as admin
left to right direction
student --> (User Registration)
student --> (User Authentication)
student --> (Password Recovery)
student --> (Profile Management)
student --> (Search & Enroll in Courses)
student --> (View Lessons & Study Materials)
student --> (Take Tests & Complete Assignments)
student --> (Monitor Student Progress)
student --> (Messaging & Comments)
student --> (Notifications)
teacher --> (User Authentication)
teacher --> (Profile Management)
teacher --> (Create/Edit Course)
teacher --> (Manage Course Materials)
teacher --> (Create/Edit Tests & Assignments)
teacher --> (Assign Teachers)
teacher --> (Monitor Student Progress)
teacher --> (Generate Reports & Statistics)
teacher --> (Messaging & Comments)
teacher --> (Notifications)
admin --> (Profile Management)
admin --> (Create/Edit Course)
admin --> (Manage Course Materials)
admin --> (Assign Teachers)
admin --> (Assign Roles & Access Rights)
@enduml
Сгенерированный UML-скрипт был затем визуализирован с помощью опенсорсного инструмента PlantUML:
Следующий шаг — доменная модель. Задаем такой промпт:
«Разработай доменную модель для системы, описанной на use case диаграмме: <сюда подставляем UML-скрипт>».
Получаем доменную модель с ключевыми сущностями, связанными с образовательной платформой, такими как пользователи, курсы, уроки, материалы, тесты, задания и другие:
Визуализируем доменную модель с помощью того же инструмента:
Модель еще сыровата, поэтому дорабатываем запрос:
»Уточни поля для классов Teacher, Student, Administrator».
Вот обновленная доменная модель:
Теперь классы Teacher, Student и Administrator наследуются от базового класса User и имеют следующие дополнительные поля и связи, специфичные для каждой роли:
1. Student:
enrollments: Список записей на курсы (Enrollment) для данного студента.
testResults: Список результатов тестов (TestResult) для данного студента.
assignmentSubmissions: Список выполненных заданий (AssignmentSubmission) для данного студента.
2. Teacher:
3. Administrator:
В данном случае администратор наследует все поля от базового класса User и нет необходимости добавлять дополнительные поля. Если потребуются специфические свойства или функциональность для администратора, их можно добавить позже.
Продолжаем уточнять запрос:
«Переименуй Lesson в Module и добавь информацию по длительности для Material, Test и Assignment».
Получаем:
Теперь класс Lesson переименован в Module, а для классов Material, Test и Assignment добавлено новое поле «duration» для отражения длительности материала, теста или задания. Эта длительность может быть измерена в минутах, часах или днях в зависимости от контекста и требований системы.
Вторая фаза эксперимента: подключение и использование кодогенератора
ChatGPT — не прикладная нейросеть для разработки, она не может — пока — создавать собственно файлы и нужную нам структуру директорий, а способна лишь выдавать код в текстовом виде, малополезном без соответствующего окружения. Поэтому на данном этапе мы подключаем кодогенератор JAIG — отдельный инструмент, предназначенный для генерации кода с помощью ChatGPT API. Он поставляется в виде .jar файла и файла настроек. JAIG был создан специально для курса IBS по разработке с помощью машинного интеллекта, но вы можете использовать для тех же целей любой другой доступный «преобразователь» или даже написать собственный простой скрипт, который будет управлять отсылкой и приемом запросов в ChatGPT.
JAIG подключается как внешний инструмент. В настройках ExternalTools нужно только задать правильную конфигурацию:
Program: java
Arguments: -jar jCodeGen/jCodeGen.jar $FilePathRelativeToProjectRoot$
Working directory: $ProjectFileDir$
Для использования JAIG нужно создать папку, поместить в нее запрос в файле с расширением .txt — скажем, prompt.txt — и запустить генерацию. В запрос можно включать ссылки на уже имеющиеся файлы и папки. Обратите внимание: ссылка должна начинаться с »/» и содержать полный путь от корня проекта. Содержимое файлов автоматически подставится в полный запрос prompt-request.txt. На основе этого файла система сформирует окончательный запрос и затем получит ответ от ИИ, сохранив его в файле prompt-response.txt. Если ответ нейросети содержит код и в этом коде правильно указаны пакеты, то включается автоматический парсинг ответа: файлы .java распределяются по папкам согласно пакету. Результат парсинга записывается в папку prompt-response-parsed. Далее результат парсинга нужно скопировать в папку src, проанализировать выданный код и устранить ошибки. На основе внесенных корректив будет сформирован патч для автоматического исправления при очередной генерации кода.
То есть цикл разработки приложения с помощью ChatGPT и JAIG выглядит так:
Третья фаза эксперимента: реализация доменной модели на Java
Генерация доменной модели на Java на основе UML-диаграммы
В корне проекта создаем папку requests и в ней подпапку 01_PlantUML_to_domain_model. В подпапку кладем plain-ASCII-файл 01_PlantUML_to_domain_model.txt с таким запросом:
/task/requirements.txt
/task/plant-uml.txt
Cгенерируй классы доменной модели на Java. Укажи все необходимые поля.
Система сгенерировала доменную модель, но без указания пакетов. Исправляем запрос:
/task/requirements.txt
Ниже приведены все классы доменной модели:
/requests/01_PlantUML_to_domain_model-response.txt
Разбей все классы по пакетам, если базовый пакет ru.ibs.eduplatform.model.
Каждый пакет должен содержать не более 5 классов.
ИИ предлагает разбивку по пакетам с учетом функциональности:
Просим нейросеть разбить классы по пакетам и добавить Lombok-аннотации:
/requests/01_PlantUML_to_domain_model-response.txt
/requests/02_packages-response.txt
Добавь аннотации Lombok @Data, @NoArgsConstructor для всех классов доменной модели.
Добавь @AllArgsConstructor для всех классов доменной модели кроме унаследованных.
Используй ru.ibs.eduplatform.model в качестве базового пакета.
Убедись, что все импорты корректны и соответствуют созданным пакетам.
Выведи код всех классов, указав название пакета каждого класса и импорты.
Для каждого класса должен быть указан пакет.
Убедись, что в результатах есть все классы.
В результате у нас сгенерированы все классы доменной модели. Теперь необходимо распарсить ответ, то есть выполнить семантический анализ сгенерированного кода. Предварительный парсинг выполняется автоматически. Для всех классов были правильно указаны их пакеты, поэтому через 5 секунд кодогенератор самостоятельно разложил классы по папкам согласно их пакетам и записал результат в подпапку prompt-response-parsed.
Проверка доменной модели путем заполнения тестовыми данными
Пишем очередной запрос:
/src/main/java/ru/ibs/eduplatform/model/**
Сгенерируй класс, заполняющий доменную модель тестовыми данными.
Создай 2–3 студентов, добавь 2 курса и несколько материалов,
2 теста с 2–3 вопросами, запиши студентов на курс.
Выведи информацию о всех курсах.
Для каждого курса выводи список студентов (имя, фамилия, email), список материалов и список тестов.
Ответ должен содержать только код.
В качестве пакета используй ru.ibs.eduplatform.
Нейросеть забыла добавить конструкторы для унаследованных классов, пришлось доработать запрос:
/src/main/java/ru/ibs/eduplatform/model/**
Для классов доменной модели, унаследованных от User, добавь конструкторы с использованием super () и вызовом конструктора суперкласса.
Выведи только код классов, в которые добавлены конструкторы.
Генерация пользовательского интерфейса
С конструкторами разобрались, переходим к внешнему виду приложения:
/src/main/java/ru/ibs/eduplatform/model/**
Модель данных представлена выше.
Сгенерируй код Java-программы с пользовательским интерфейсом командной строки, которая покажет меню со следующими элементами:
Добавить слушателя (должен позволять вводить данные студента)
Найти слушателя (при выборе нужно запросить имя студента, найти его по имени и показать полную информацию)
Записать слушателя на курс (должен позволить найти курс по имени и фамилии студента, добавить этого студента на курс и создать Enrollment, добавив его в enrollments студента)
Добавить курс (должна позволять ввести данные курса)
Найти курс (должен находить курс по имени и выводить все данные курса и список записавшихся студентов)
Интерфейс должен быть на русском языке.
Ответ должен содержать только код.
На выходе получаем файл UI.java с первой версией кода.
Фрагмент полученного кода
Третья фаза эксперимента: обеспечение персистентности и работа над ошибками
С первого раза получить идеальный код практически невозможно. ChatGPT 4.0 плохо «переваривает» сложные запросы, состоящие из многих требований.
Как это исправить:
поправить код вручную; например, добавить недостающий импорт из определенного класса, конструктура или пакета;
изменить порядок требований в запросе, указав невыполненные требования ближе к концу, где у них будет более высокий приоритет;
добавить уточнение требования в запрос;
изменить температуру, чтобы получить другой ответ;
разбить запрос на несколько запросов, чтобы упростить задачу нейросети.
Генерация пользовательского интерфейса
Уточнение для ChatGPT:
ИИ генерирует новый пользовательский интерфейс в UI.java.
Поддержка базы данных
Пишем очередной запрос:
Добавь поддержку JPA, объявления пакетов и импортов для каждого сгенерированного класса.
Используй в импортах пакет jakarta.persistence вместо javax.persistence.
Сгенерируй конструкторы без идентификатора (id должен быть сгенерирован автоматически).
/src/main/java/ru/ibs/eduplatform/model/**
Добавь полностью квалифицированные импорты для всех импортов из всех классов.
Прекрасно, теперь все доменные объекты имеют поддержку JPA.
Чтобы сгенерировать репозитории Spring Data, пишем почти идентичный запрос:
Добавь поддержку JPA, объявления пакетов и импортов для каждого сгенерированного класса.
Используй в импортах пакет jakarta.persistence вместо javax.persistence.
/src/main/java/ru/ibs/eduplatform/model/**
Добавь полностью квалифицированные импорты для всех импортов из всех классов.
Запрос по поддержке базы данных для интерфейса:
/src/main/java/ru/ibs/eduplatform/model/**
/src/main/java/ru/ibs/eduplatform/repositories/**
Используй модель данных и репозитории для обновления класса UI.
Данные должны загружаться из базы данных при запросе от пользователя и помещаться в базу данных после любых изменений.
Модифицируй класс UI, чтобы он использовал репозитории.
/src/main/java/ru/ibs/eduplatform/UI.java
Результат: все данные при работе через интерфейс командной строки загружаются из базы данных и сохраняются в базу данных.
Дополнение: вывод всех слушателей и курсов
Запрос:
/src/main/java/ru/ibs/eduplatform/repositories/**
/src/main/java/ru/ibs/eduplatform/EduplatformUIApplication.java
Добавь в меню пункты показать всех студентов и все курсы
Система сгенерировала новый класс EduplatformUIApplication, включающий пункты о выводе студентов и курсов:
case 6:
showAllStudents();
break;
case 7:
showAllCourses();
break;
Работа над ошибками: LazyInitializationException
Без ошибок, конечно, не обошлось. После добавления курса при попытке поиска курса программа выдала исключение LazyInitializationException. Эта ошибка возникает, когда Hibernate пытается загрузить данные из связанных сущностей после закрытия сессии. В данном случае при вызове метода `findCourse ()` сессия уже закрыта, и Hibernate не может загрузить данные из связанных сущностей.
В ответ на жалобу GPT предложил поставить аннотацию `@Transactional` для метода `findCourse ()`:
@Transactional
public void findCourse(Scanner scanner) {
// ...
}
При этом машинный интеллект также пояснил, что такое решение должно позволить Hibernate открывать новую сессию при выполнения метода `findCourse ()`и загружать данные из связанных сущностей.
Но это в теории. А на практике программа снова выдала исключение. Выяснилось, что аннотация не работает, потому что мы вызываем этот метод внутри того же класса. В Spring аннотация `@Transactional` работает через прокси, и, когда мы пытаемся вызвать метод внутри того же класса, прокси не используется и транзакция не начинается.
Чтобы решить эту проблему, GhatGPT посоветовал вынести метод `findCourse` в отдельный сервисный класс и добавить к нему аннотацию `@Transactional`. Затем внедрить этот сервисный класс в `EduplatformUIApplication` и вызвать метод `findCourse` через этот сервисный класс:
@Service
public class CourseService {
@Transactional
public void findCourse(Scanner scanner) {
// ваш код метода findCourse
}
}
Проблема решена.
Рефакторинг: вынос бизнес-логики в сервисы
Запрос:
Обнови класс UI с использованием шаблона проектирования Command.
/src/main/java/ru/ibs/eduplatform/UI.java
При использовании паттерна проектирования Command добавление и работа с командами становится более простой в поддержке и модификации.
Персистентность базы данных
Еще одна проблема в сгенерированном ИИ приложении: данные не записывались на диск, то есть удалялись из памяти после перезапуска программы.
Чтобы сделать базу данных персистентной, ChatGPT предложил изменить настройки H2 в файле `application.properties`:
```
spring.datasource.url=jdbc: h2: file:./data/db; DB_CLOSE_DELAY=-1; DB_CLOSE_ON_EXIT=FALSE
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=update
```
Параметры `DB_CLOSE_DELAY=-1` и `DB_CLOSE_ON_EXIT=FALSE` указывают, что база данных должна оставаться открытой и не закрываться при завершении работы приложения.
Исправление: завершение работы сервера
Следующая ошибка — при выборе «Выйти» в меню сервер продолжал работу.
Чтобы завершить работу приложения и сервера, ChatGPT предложил добавить вызов `System.exit (0)` после `scanner.close ()` в методе `run`. Вот обновленный код:
@Override
public void run(String... args) throws Exception {
...
scanner.close();
System.exit(0); // Завершаем работу сервера
}
Исправление: не добавлять курс, если он уже существует
На этом патчи не закончились. Еще одна обнаруженная проблема — программа позволяла добавлять дублирующие курсы с одинаковыми названиями.
Для того чтобы не добавлять курс, если курс с таким же именем уже существует, ChatGPT посоветовал внести следующие изменения в метод `addCourse` в классе `CourseServiceImpl`:
@Override
public void addCourse(String name, String description) {
Course existingCourse = courseRepository.findByName(name);
if (existingCourse != null) {
System.out.println("Курс с таким названием уже существует.");
return;
}
Course course = new Course(name, description);
courseRepository.save(course);
System.out.println("Курс успешно добавлен.");
}
Четвертая фаза эксперимента: реализация REST API и разработка естественно-языкового интерфейса
Не будем в деталях останавливаться на этой фазе. Думаю, подробный разбор ошибок выше говорит сам за себя: брать готовый код от ChatGPT и сразу запускать его в работу — как для модели 3.5, так и на «четверке» — не представляется возможным.
Далее мы сгенерировали REST-запросы, контроллеры и сервисы, протестировали код с помощью автосгенерированных модульных тестов и, наконец, сформировали Swagger-документацию.
Результат получился следующим:
В рамках одной небольшой статьи невозможно рассказать обо всех наших попытках «прикрутить» машинный интеллект к разработке кода, поэтому давайте просто конспективно перечислим то, где действительно удалось добиться приемлемых результатов:
составление запросов со ссылками на исходники и другие документы;
отправка запросов через ChatGPT API (автоматически или вручную);
удобная интеграция со средой разработки (IDEA);
просмотр запросов в процессе генерации, подсветка кода;
парсинг результатов генерации, раскладывание исходников по файлам и пакетам;
запись сгенерированных файлов в требуемую папку (например, в канонические Maven-фолдеры src/main/java и src/test/java);
гибкие настройки генерации, в том числе выбор модели (gpt3/gpt4 и др.) и температуры, глобально или для каждого отдельного запроса;
создание и «автонакатывание» патчей для изменений в коде;
генерация серии запросов из шаблонов (на базе фреймворка Apache Velocity);
выполнение запросов единым пакетом (batch-файлы);
библиотечка промптов, например для рефакторинга выбранного фрагмента, генерации javadoc-комментариев, разъяснения того или иного сниппета, для выявления уязвимостей и антипаттернов, и пр.;
откат изменений в коде (rollbacks).
Эксперимент: выводы
Четвертая итерация ChatGPT работает заметно лучше версии 3.5. В нашем прошлом эксперименте на «пляски» с нейросетью у коллеги ушло в общей сложности 10 дней. При этом ручное написание кода заняло бы один рабочий день. ChatGPT 4.0, напротив, сокращает, а не затягивает процесс разработки. Благодаря переходу на «четверку» в наших экспериментах наблюдалась устойчивая тенденция к экономии времени на разработку не менее 20–25%. Однако сгенерированный код все еще требует серьезных доработок, а модель периодически допускает довольно нелепые ошибки. Посмотрим, какой будет ChatGPT 5.0.
Мы также надеемся, что появится API-доступ к большим языковым моделям других вендоров, показывающим интересные результаты именно для кодогенерации, — например, Llama или Vicuna.
P.S. JAIG, который-то и оказался ключом к общению с машинным интеллектом, представляет собой опенсорсный инструмент и доступен по ссылке.