Кажется, LLM программирует лучше меня

Может ли искусственный интеллект (ИИ) превзойти программиста-человека при написании кода? Спустя ряд экспериментов становится понятно, что ответ на этот вопрос — уверенное да!

Эволюция программистов

Эволюция программистов

В этой статье не будет ни строчки теоретических рассуждений о преимуществах и недостатках использования LLM в программировании. Будет только практика и в конце некоторые обоснованные выводы с ироничным взглядом в недалекое будущее для всех программистов. Содержание всех шаги будут полностью представлено как есть, поэтому статья может показаться несколько длинноватой, но ото необходимо для понимания сути самого процесса.

Вначале для поднятия настроения вот вам «вредный совет» программистам от LLM (оказывается она не только пишет код, но и обладает своеобразным чувством юмора):

Никогда не изучайте новые языки программирования; в конце концов, вы уже и так знаете достаточно, чтобы хоть как-то писать свой обычный код — так что твердо придерживайтесь привычного и не тратьте время на изучение чего-то нового!

Какой будет эксперимент?

Цель эксперимента — написать довольно сложную с точки зрения логики игру, максимально используя модель с минимальными ручными доработками.

Если это получится, можно будет сравнить новую парадигму программирования со старой по критериям затраты ресурсов (времени, интеллектуальных усилий), удобства использования технологии, качества полученных результатов и удовлетворенностью в целом процессом решения задачи.

Почему создавать будем игру, а не какую-нибудь, например, полезную утилиту для компьютера?

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

Как будем проводить эксперимент?

Сначала попытаемся сразу создать начальный рабочий вариант с базовой функциональностью.

Далее код я буду создавать методом последовательных уточнений и дополнений. Шаг за шагом код будет изменяться, логика игры будет улучшаться, становиться более разнообразной. Графические функции тоже будут последовательно улучшаться, совершенствуя игровой процесс. Я постараюсь сделать так, чтобы весь основной код написала модель и не буду сильно вмешиваться в код, который она сама будет предлагать на каждом шаге. Однако мелкие ошибки и незначительные исправления я все же буду время от времени производить, если это совершенно необходимо, но это будит действительно минимальное вмешательство.

Сам эксперимент я разбил на два этапа.

Первый этап — это получение полнофункционального кода и пользовательского интерфейса.
Второй этап — это доведение кода до кондиции и получении рабочей игры.

Какие исходные условия эксперимента?

Я намеренно буду усложнять условия эксперимента, хотя многое можно было-бы изначально сделать более правильно. Но в данном случае здоровая доля мазохизма будет уместна. Поэтому пора определить наши правила.

Инструкции я буду давать на русском языке.

Игра будет создана с нуля, так чтобы модель по возможности ничего не знала и ней.
В качестве языка будет выбран не самый подходящий для этого C#.
В качестве модели я буду использовать специально обученную для кодинга на 80 языках квантованную Codestral-22B-v0.1-Q4_K_M (Про оригинальную модель можно узнать на сайте разработчиков https://mistral.ai/news/codestral/).

Работать с кодом буду в VS, перенося в проект создаваемый моделью код просто копипастом, соблюдая ее инструкции, которые она будет давать в комментариях к каждой порции создаваемого ею кода.

Я буду создавать довольно известную в народе игру «Шарики», Lenes (Color Lines), которая является непростой в реализации как в логическом, так и в графическом смысле. Предварительно я убедился, что модель о ней ничего не знает.

Начинаем первый этап

Перед началом работы я настрою модель определенным образом, задав значения нескольким ее параметрам настройки, что важно для ее лучшей эффективности.
Далее я подробно шаг за шагом приведу весь протокол взаимодействия с моделью, исключив только сам сгенерированный код, вместо которого будет текст «Далее код и комментарии». Иначе статья превратится в простыню с нагромождением плохо читаемого текста. В конце для тех, кому будут интересны подробности, я дам ссылку на полный протокол с кодом для скачивания.

Итак, можно начинать сам эксперимент.

Первый промпт, который должен объяснить модели смысл игры, которую она должна создать.

Напишите код Windows forms на c# с учетом игровой логики и специфических функциональных возможностей для игры в lines. Суть игры в следующем.
На игровом поле 10 на 10 клеток в случайной клетке появляется шарик одного из 5 случайных цветов. Игрок мышкой выбирает любой шарик в поле и указывает свободную клетку, куда этот шарик нужно передвинуть и шарик плавно передвигается в эту клетку. Затем в случайной свободной клетке опять появляется другой шарик одного из случайных 5 цветов и таким образом игра продолжается. Задача игрока в том, чтобы из 5 или более шариков одного цвета собрать непрерывную линию по вертикали или по горизонтали или по диагонали. В этом случае все эти 5 шариков исчезают. Игра заканчивается, когда все клеточки заполнены шариками. Новый шарик может появиться только на свободной клеточке.

Ответ Модели.
Here is a simple implementation of the game’s logic in C# Windows Forms. This version doesn’t include any animations; the movement of balls is instant.

Далее код и комментарии.
Note: To run this code, you need to have a Windows Forms project in your C# environment and replace the contents of the Form1.cs file with this code. This is just a simple text-based representation of the game and does not include any graphics or animations. For a more complete implementation, you may want to consider using a graphics library like Unity or XNA for the visual representation of the game.

Мы сразу получаем рабочий код без ошибок и можно попытаться играть в эту игру, чтобы найти первый недостаток и исправить его.
Он заключается в том, что шарик мы выбираем левой кнопкой мыши, а цель указываем правой, поэтому пишем следующий промпт.

Измени функцию Grid_MouseClick так, чтобы оба действия и выбор шарика и указание клеточки для его перемещения производились только нажатием MouseButtons.Left

Ответ Модели.
Here’s how you can modify the `Grid_MouseClick` function to handle both selecting a ball and specifying a destination for it solely through left clicks:

Далее код и комментарии.
In this version, clicking on a ball will first select it if no ball is currently selected. Clicking on another ball will change the selection to that ball. If we then click on an empty cell, the previously selected ball will move to the new cell. This only works if there are two distinct clicks — one on a ball and one on an empty cell — in between.

Обращаем внимание на то, что выбранный шарик никак не отличается от других и хотим, чтобы он чем-то отличался, поэтому пишем следующий промпт.

Измени код так, чтобы выбранный шарик сохранял свой цвет, но отличался от не выбранных шариков.

Ответ Модели.
You can modify the `Grid_MouseClick` function to change the color of the selected ball when it’s clicked, and revert its color back to normal when it’s deselected:
First, you need to introduce a new variable to keep track of the original color of the selected ball:

Далее код и комментарии.
Then, adjust your `Grid_MouseClick` method like so:

Далее код и комментарии.
In this version, clicking on a ball will change its color to the contrast color (which is calculated by the `GetContrastColor` function), and if you click on another ball, the first one will revert back to its original color. Then, you can move the selected ball to an empty cell.
Note: You should also add a `GetContrastColor` function that takes a `Color` as input and returns either black or white based on the perceived brightness of the input color.

Модель забыла сама определить функцию GetContrastColor, поэтому просим ее написать.

Напиши функцию GetContrastColor

Ответ Модели.
Here’s a simple implementation of the `GetContrastColor` function in C#:

Далее код и комментарии.
This function calculates the perceived brightness of a color using the RGB values and the formula for luminance (also known as relative luminance). If the brightness is greater than 128 (out of 255), it returns black; otherwise, it returns white. This provides a simple way to choose a contrasting color for any given color.

Теперь выбранный шарик как-то коряво выглядит при выборе, но действительно, он изменяется, поэтому хотим усовершенствовать эту функцию и сделать выбор шарика красиво.

Измени функцию GetContrastColor так, чтобы цвет шарика при его выборе не менялся, а яркость плавно циклически изменялась по синусоиде с помощью таймера

Ответ Модели.
To create this effect, you could use a `Timer` object to change the brightness of the selected ball at regular intervals. Here’s an example of how to do this:
First, add a `Timer` field to your form:

Далее код и комментарии.
Then, in your constructor, initialize the timer and subscribe to its `Tick` event:

Далее еще код и комментарии.
Next, modify your `Grid_MouseClick` method to start the timer when a ball is selected and stop it when it’s deselected:

Далее еще код и комментарии.
Finally, implement your `Timer_Tick` event handler. This should change the color of the selected ball based on a sinusoidal brightness value:

Далее еще код и комментарии.
In this version, the `time` variable accumulates over time, and the brightness is calculated as a function of this variable using a sinusoidal waveform that cycles between 0 and 1. The resulting brightness value is then used to calculate a new grayscale color for the selected ball.

Вот теперь при выборе шарика его яркость красиво изменяется, то больше, то меньше, пока не будет указана цель.
Настало время исправить другое очевидное недоразумение: наши шарики вовсе не круглые шарики, а цветные квадратики.

Преобразуй внешний вид шариков из квадратной формы в круглую.

Ответ Модели.
To make the balls circular instead of square, you can set their `Region` property to an instance of the `System.Drawing.Drawing2D.GraphicsPath` class with an ellipse shape. Here’s how to modify your code to achieve this:
First, add the following `using` directive at the top of your file:

Далее код и комментарии.
Then, in your constructor, create a new `GraphicsPath` object with an ellipse shape and set it as the `Region` of each `PictureBox`:

Далее код и комментарии.
In this version, a new `GraphicsPath` object is created for each `PictureBox`, and an ellipse is added to its path that fits within the bounds of the `PictureBox`. This ellipse is then used to define the region of the `PictureBox`, which determines which pixels are considered part of the control and which are transparent. This makes the balls circular instead of square.

Переносим созданный код по инструкции в наш проект и получаем то, что хотели. Круглые красивые шарики можно выбирать и указывать цель, куда они перемещаются и при формировании непрерывной линии длиной 5 и более шариков одного цвета подряд эта линия удаляется. В принципе, игра готова, но нужно довести ее до кондиции, поэтому пишем новый промпт.

Добавь кнопку, нажав на которую начинается новая игра, а также Label, на котором считается число удаленных линий из шариков. Сделай панель, кнопку и Label более красивыми, с четкими линиями и приятными цветами.

Ответ Модели.
To add a new game button, a label to display the number of removed lines, and improve the appearance of the panel, button, and label, you can use the following code:
First, add the following fields to your form:

Далее код и комментарии.
Then, in your constructor, create and initialize these controls, and set their properties as desired:

Далее код и комментарии.
In this version, a new `Button` and `Label` are created, and their properties are set to display the desired text and appearance. The button’s `Click` event is also handled by the `NewGameButton_Click` method, which you can implement to start a new game.

Далее код и комментарии.
To update the number of removed lines, you can modify your `RemoveLine` method as follows:
In this version, the `linesRemoved` field is incremented each time a line is removed, and the label’s text is updated to reflect the new value.

Модель забыла определить функцию NewGameButton_Click, напоминаем ей об этом.

Напиши функцию NewGameButton_Click.

Ответ Модели.
Here’s an example implementation of the `NewGameButton_Click` function:

Далее код и комментарии.
In this version, the `NewGameButton_Click` function clears the grid by setting the background color of each `PictureBox` to white, resets the `linesRemoved` field and the label’s text, and then fills the grid with new balls using the `AddNewBall` method.

Итак, первый этап создания игры завершен. Все основные функции есть, логика работает, внешний вид вполне удовлетворительный. На первый этап было потрачено меньше одного дня с перерывами.

Что можно сказать о качестве кода? Он чистый, откомментированный, хорошо структурированный и главное безошибочный.

Я в ручную исправил только одну незначительную ошибку, не связанную с логикой работы. Модель точно выполняет условия промпта и подробно комментирует свои решения. Результат выше всяких смелых ожиданий, так как я был внутренне готов и к логическим ошибкам и ко всем прочим ошибкам тоже. Но их удалось избежать, также как и рутинного кодирования, очередной раз изобретая велосипеды или ища их в интернете, потом скачивая и адаптируя для своего проекта. Я занимался действительно творческой работой, придумывал и руководил созданием программы, а не погружался в дебри утомительного кодинга и его отладки. Кстати об отладке. Это удивительно, но отладка мне не потребовалась от слова совсем. Код получился настолько чистым, что после очередной модификации каждый раз работал безупречно.

Переходим ко второму этапу

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

Ниже представлен код Windows forms на c# с учетом игровой логики и специфических функциональных возможностей для игры в lines. Суть игры в следующем.

На игровом поле 10 на 10 клеток в случайной клетке появляется шарик одного из 5 случайных цветов. Игрок мышкой выбирает любой шарик в поле и указывает свободную клетку, куда этот шарик нужно передвинуть и шарик плавно передвигается в эту клетку. Затем в случайной свободной клетке опять появляется другой шарик одного из случайных 5 цветов и таким образом игра продолжается. Задача игрока в том, чтобы из 5 или более шариков одного цвета собрать непрерывную линию по вертикали или по горизонтали или по диагонали. В этом случае все эти 5 шариков исчезают. Игра заканчивается, когда все клеточки заполнены шариками. Новый шарик может появиться только на свободной клеточке.

Далее весь рабочий код из нашего проекта в VS.
И новая инструкция.

Измени код так, чтобы у выбранного шарика изменялся не только цвет, а еще и размер.

Ответ Модели.
Sure, I can help you modify the code to change both the color and size of the selected ball. Here’s how you could do it:
First, we need to add a new variable to keep track of the initial size of the ball. We will use this value to calculate the new size during the animation. Let’s add it in the `Form1` class:

Далее код и комментарии.
Then, when selecting a ball, store its initial size:

Далее код и комментарии.
Now let’s modify the `Timer_Tick` function to also change the size of the ball:

Далее код и комментарии.
This should make the selected ball expand and shrink in size while changing its color. You may need to adjust the calculation of the new size to get the exact effect you want.

Получилось, что теперь изменяется размер самой клеточки выбранного шарика, а не он сам, поэтому уточняем задачу.

Замени изменение размера выбранного шарика Size на изменение размера AddEllipse.

Ответ Модели.
Sure, I can help you with that. Instead of changing the `Size` property of the `PictureBox`, we will change the size of the ellipse used to draw the ball. This requires us to create a new `GraphicsPath` with each tick of the timer. Here’s how you can modify the code:
Instead of storing the initial size of the ball, let’s store the initial radius of the ellipse:

Далее код и комментарии.
Then, when selecting a ball, store its initial radius:

Далее код и комментарии.
Now let’s modify the `Timer_Tick` function to also change the size of the ellipse:

Далее код и комментарии.
This should make the selected ball expand and shrink in size while changing its color. Note that we’re assuming that the width and height of the `PictureBox` are always equal to maintain a circular shape. If they can be different, you would need to store and calculate with both initial width and height instead of just the radius.

Вот теперь мы достигли желаемого эффекта: выбранный шарик не только меняет циклически яркость, но и свой размер, что выглядит так как надо.

Шарики работают

Шарики работают

Финальная и самая сложная доработка

И вот теперь наконец мы достигли финального, самого сложного рубежа, который должен показать всю мощь модели как хорошего программиста, или она сломается на этом уровне сложности. Я честно ожидал от модели провала с множеством ошибок и структурных нарушений, но собственно ради этого то всё и затевалось.

Пока что наш выбранный шарик просто перескакивает в указанную клеточку без каких-либо ограничений и это надо исправить.

Пишем самый сложный промпт.

Измени код так, чтобы выбранный шарик передвигался в указанную клеточку плавно, последовательно перемещаясь в одну из соседних примыкающих слева, справа, сверху или снизу клеточку пока не достигнет указанной клеточки. Траектория передвижения должна каждый раз рассчитываться так, чтобы не пересекаться ни с одним другим шариком в игровом поле.

Ответ Модели.
Sure, I can help you modify the code to move the selected ball to the target cell smoothly and sequentially, moving one step at a time towards the target cell without intersecting any other balls on the field. Here’s how you could do it:
Firstly, we need a way to calculate the shortest path from the current position of the selected ball to the target cell, taking into account the positions of all other balls on the field. This is a complex problem known as pathfinding, and there are many algorithms to solve it. For simplicity, let’s use A* (A-Star) algorithm for this task.
We need to add a new method `FindShortestPath` to the `Form1` class that takes the starting point and the ending point of the path as parameters and returns an array of points representing the shortest path:

Далее код и комментарии.
Then, when clicking on a target cell, if the clicked cell is not empty, we don’t select or move any ball. Otherwise, we calculate the shortest path from the current position of the selected ball to the target cell. If the shortest path exists, we start moving the selected ball along this path, one step at a time.
Here’s how you may modify the `Grid_MouseClick` method to handle this:

Далее код и комментарии.
Now let’s add a new method `MoveAlongPath` that takes an array of points representing the path as a parameter and moves the selected ball along this path, one step at a time:

Далее код и комментарии.
This should move the selected ball along the shortest path to the target cell smoothly and sequentially, one step at a time without intersecting any other balls on the field. You may need to adjust the speed and number of steps of the movement to get the exact effect you want.

Модель определила основную функцию FindShortestPath без содержания, но с комментариями.
// Calculate the shortest path using A* algorithm.
// This function should return a list of points representing the shortest path.
// The implementation of this function is beyond the scope of this response.
// You can find many implementations of A* algorithm online.

Поэтому заставим ее написать эту функцию.

Напиши функцию FindShortestPath.

Ответ Модели.
Sure, I can help you write a function to find the shortest path between two points in the game grid using A* algorithm. Here’s a simple implementation of the `FindShortestPath` function:

Далее код и комментарии.
This function uses a `Node` class that represents a position in the game grid with its G cost, H cost, and parent node:

Далее код и комментарии.
The function also uses a `PriorityQueue` class to store nodes in the open set with their priority based on their F cost:

Далее код и комментарии.
The `GetNeighbors` function returns a list of valid neighboring positions of the given position:

Далее код и комментарии.
The `Heuristic` function calculates the H cost (estimated distance to the destination) using Manhattan distance:

Далее код и комментарии.
Finally, the `ReconstructPath` function reconstructs the path from the destination node to the starting node:

Далее еще порция кода.

Перенеся всю эту массу кода в проект, замечаем, что модель забыла определить три функции, которые вызываются из ее кода, напоминаем ей об этом.

Напиши функцию Heuristic.

Ответ Модели.
Sure, I can help you write a function for the heuristic calculation in the A* algorithm. The heuristic function estimates the cost to reach the destination from a given position on the game grid. A common choice for this is the Manhattan distance, which calculates the sum of the absolute differences between the x and y coordinates of two points. Here’s a simple implementation of the `Heuristic` function:

Далее код и комментарии.
This function takes two `Point` objects as input, representing the starting point and the ending point, and returns an integer value that represents the estimated cost to reach the destination from the starting point using Manhattan distance

Напиши функцию ReconstructPath.

Ответ Модели.
Sure, I can help you write a function to reconstruct the path from the destination node to the starting node in the A* algorithm. This function backtracks from the destination node to the starting node by following the parent nodes of each node in the path. Here’s a simple implementation of the `ReconstructPath` function:

Далее код и комментарии.
This function takes a `Node` object as input, representing the destination node, and returns a list of `Point` objects that represent the positions of the nodes in the shortest path from the starting node to the destination node. The function starts at the destination node and adds its position to the path. Then it moves to the parent node of the current node and repeats this process until it reaches the starting node. Finally, the function reverses the path so that it goes from the starting node to the destination node.

Напиши функцию GetNeighbors.

Ответ Модели.
Sure, I can help you write a function to get the valid neighboring positions of a given position on the game grid. This function checks if a neighboring position is within the bounds of the game grid and if it’s not occupied by another ball. Here’s a simple implementation of the `GetNeighbors` function:

Далее код и комментарии.
This function takes a `Point` object as input, representing the current position, and returns a list of `Point` objects that represent the valid neighboring positions of the current position. The function loops through four possible directions (up, down, left, right) and checks if each neighboring position is within the bounds of the game grid and if it’s not occupied by another ball. If a neighboring position is valid, it’s added to the list of neighbors. Finally, the function returns the list of neighbors.

После того как весь этот созданный моделью код был просто перенесен в проект без каких либо изменений и запущен, произошло невероятное: без единой ошибки и каких-либо недочетов все заработало так, как было сформулировано в задании.
Выбранный шарик находит кратчайший путь к цели и красиво перемещается по клеточкам по этому пути.

Модель справилась на отлично и в конце второго этапа получилась вполне добротная игра.

Выводы из всего этого чудесного безобразия

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

Вывод достаточно очевиден, новая парадигма программирования с помощью модели явно более эффективна с любой точки зрения.

Рутинная работа сводится к минимуму, время сокращается, качество кода повышается, процесс программирования просто становится комфортнее и больше похож на интеллектуальное творчество.

Не могу не сделать скромный взгляд в ближайшее будущее программирования.

Итак, поскольку мы, как выясняется, уже стоим на пороге новой эры технологий, давайте примем это будущее сознательно и с распростертыми объятиями, а не пугливо прячась в таком привычном, закостенелом консерватизме уютных старых парадигм. Используйте имеющиеся уже сейчас LLM и все другие инструменты искусственного интеллекта, которые обязательно последуют в будущем по их стопам. Давайте овладевать ими, чтобы улучшить свои творческие и конструкторские навыки, быстрее и лучше решать задачи и проблемы уже на новом уровне. В конце концов, разве не в этом суть новых технологий?

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

© Habrahabr.ru