В последнее время я много экспериментировал с написанием кода при помощи LLM (Large Language Model, большая языковая модель). На мой взгляд, эти инструменты отлично справляются с генерацией небольших самодостаточных фрагментов. К сожалению, что-то большее уже требует человеческого участия для оценки результата LLM и предоставления дальнейших инструкций.
В большинстве случаев, когда кто-то утверждает, что «GPT написал X», человек выступает для LLM в роли своеобразного REPL (Read-Eval-Print Loop, цикл чтение-оценка-вывод), внимательно подводя модель к функциональному результату. Я нисколько не хочу принизить ценность этого процесса – очень здорово, что он работает. Но можем ли мы шагнуть дальше? Можем ли использовать LLM для генерации ВСЕГО кода сложной программы за раз без человеческого вмешательства?
Написание расширения VSCode
Чтобы проверить способность GPT-4 генерировать сложные программы, я попросил этот инструмент создать расширение VSCode, позволяющее пользователю корректировать уровень заголовка выбранного текста Markdown. Эта задача требует:
- Предметного понимания того, как выполнить скаффолдинг и внедрить расширение в VSCode.
- Комбинирования разных языков и платформ: расширения VSCode создаются на TypeScript, что требует написания конфигурации для самого Typescript, а также Node.js и VSCode.
- Генерации множества файлов.
- Создания схемы для отладки, сборки и выполнения кода.
▍ Конфигурация проекта
В этом эксперименте для всех задач генерации я использовал GPT-4. Эту модель я нахожу наиболее эффективной среди всех современных аналогов.
Вдобавок к этому — я использовал для генерации кода фреймворк smol-ai
.
Описание smol-ai
из его README:
Это прототип агента «джуниор-разработчика» (он же
smol dev
), который создаёт структуру всей базы кода за вас на основании предоставленной спецификации продукта, но не захватывает мир и не обещает фантастических возможностей AGI (Artificial General Intelligence, общий искусственный интеллект). Вместо создания специализированных, косных, одноразовых стартовых систем в стиле создать приложение react или создать приложение nextjs, этот инструмент, по сути, позволяет создать любое приложение, указания для скаффолдинга которого вы прописываете в тесном взаимодействии с вашимsmol dev
.
Я ценю smol-ai
за его простоту. Вся логика генерации кода находится в одном файле Python, состоящем из трёх основных функций:
- Генерации списка файлов, необходимых для передачи инструкций (например, package.json, index.js, ...).
- Генерации списка общих зависимостей, необходимых для передачи инструкций (например, axios, react, ...).
- Для каждого файла в списке также генерируется код, который будет внесён в этот файл с использованием общих зависимостей, если они применимы.
Имейте в виду, что smol-ai
также имеет и другие возможности вроде отладки ошибок и генерации инструкций на основе текущей базы кода. В этой статье мы их затрагивать не будем.
▍ Инструкции
Я начал с простой инструкции, описывающей мою задачу:
python main_no_modal.py "a vscode extension that lets the user adjust the heading level of the selected text. it should have three commands: increase heading level, decrease heading level, and set heading level"
Перевод:
python main_no_modal.py «расширение vscode, позволяющее пользователю корректировать уровень заголовка выбранного текста. Оно должно поддерживать три команды: повышение уровня заголовка, понижение уровня заголовка и установка уровня заголовка».
Обратите внимание, что я задал короткую и обобщённую инструкцию. Мне хотелось посмотреть, насколько хорошо GPT справится без получения дополнительных деталей или предметной информации, отсутствующих в его обучающем наборе.
▍ Результат
Спустя несколько минут генерации кода, smol-ai
создал следующие файлы:
.
├── extension
│ ├── package.json
│ ├── src
│ │ ├── commands
│ │ │ ├── decreaseHeadingLevel.ts
│ │ │ ├── increaseHeadingLevel.ts
│ │ │ └── setHeadingLevel.ts
│ │ └── extension.ts
└── shared_dependencies.md
Заметьте, что shared_dependencies.md создан в виде догадки о том, какие общие зависимости будут присутствовать. В нашем случае ИИ создал файл с таким содержимым:
the app is: a vscode extension that lets the user adjust the heading level of the selected text. it should have three commands: increase heading level, decrease heading level, and set heading level
the files we have decided to generate are: tsconfig.json, package.json, utils.ts, extension.ts
Shared dependencies:
1. Exported variables:
- adjustHeadingLevel (from utils.ts)
2. Data schemas:
- None
3. ID names of DOM elements:
- None
4. Message names:
- None
5. Function names:
- increaseHeadingLevel
- decreaseHeadingLevel
- setHeadingLevel
6. Dependencies:
- None
▍ Код
Файл extension.ts является точкой входа расширения VSCode. В нём мы регистрируем три команды, которые указали в инструкции.
import * as vscode from 'vscode';
import { increaseHeadingLevel } from './commands/increaseHeadingLevel';
import { decreaseHeadingLevel } from './commands/decreaseHeadingLevel';
import { setHeadingLevel } from './commands/setHeadingLevel';
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('extension.increaseHeadingLevel', increaseHeadingLevel),
vscode.commands.registerCommand('extension.decreaseHeadingLevel', decreaseHeadingLevel),
vscode.commands.registerCommand('extension.setHeadingLevel', setHeadingLevel)
);
}
export function deactivate() {}
Ниже я приведу логику, содержащуюся в decreaseHeadingLevel.ts:
import * as vscode from 'vscode';
export async function decreaseHeadingLevel() {
const editor = vscode.window.activeTextEditor;
if (!editor) {
return;
}
const document = editor.document;
const selection = editor.selection;
const selectedText = document.getText(selection);
const headingRegex = /^(#+)s(.*)$/;
const match = selectedText.match(headingRegex);
if (!match) {
vscode.window.showErrorMessage('Invalid selection. Please select a heading to decrease its level.');
return;
}
const currentLevel = match[1].length;
if (currentLevel === 1) {
vscode.window.showErrorMessage('Cannot decrease heading level. Already at the lowest level.');
return;
}
const newLevel = currentLevel - 1;
const newHeading = '#'.repeat(newLevel) + ' ' + match[2];
await editor.edit((editBuilder) => {
editBuilder.replace(selection, newHeading);
});
}
Код проверяет наличие активной программы-редактора, получает выбранный текст и ищет заголовок Markdown, используя регулярное выражение. Если заголовок найден, и его текущий уровень не является наименьшим, он этот уровень понижает.
На первый взгляд, никаких проблем в этой логике нет. Код выполняет команду и проверяет возможные пограничные случаи. Он даже предоставляет полезные сообщения об ошибках, чем уже опережает большинство программ, создаваемых людьми…
Тестирование расширения
Для проверки расширения нам нужно успешно проделать следующие шаги:
- Установить зависимости.
- Скомпилировать код.
- Запустить расширение.
▍ Шаг 1: установка
При попытке установить зависимости мы сталкиваемся с первой проблемой:
$ yarn
Couldn't find any versions for "vscode-test" that matches "^1.6.2"
? Please choose a version of "vscode-test" from this list: (Use arrow keys)
❯ 1.6.1
▍ Проблема 1: не удалось найти vscode-test
Инспектирование package.json выдаёт следующее:
{
"name": "adjust-heading-level",
...
"engines": {
"vscode": "^1.62.0"
},
"devDependencies": {
"@types/node": "^14.17.0",
"@types/vscode": "^1.62.0",
"typescript": "^4.4.2",
"vscode": "^1.1.37",
"vscode-test": "^1.6.2"
},
}
Движок VSCode определяет минимальную версию редактора. На сегодня (23.05.2023) это версия 1.78. Версия 1.62.0 была выпущена 21 октября 2021 года.
Это согласуется с крайним сроком имеющихся у GPT-4 знаний:
GPT-4, как правило, ничего не знает о событиях, произошедших после сентября 2021, так как этой датой ограничивается актуальность большей части известной ему информации.
Версия vscode-test 1.6.2
выглядит подозрительно похожей на 1.62, и это говорит о том, что GPT, скорее всего, эти числа выдумал.
Как бы то ни было, это легко исправить, указав верный номер версии и повторив установку:
- "vscode-test": "^1.6.2"
+ "vscode-test": "^1.6.1"
Повторная установка завершилась успешно.
$ yarn
...
[3/5] 🚚 Fetching packages...
[4/5] 🔗 Linking dependencies...
[5/5] 🔨 Building fresh packages...
✨ Done in 4.31s.
▍ Шаг 2: сборка
Поскольку TypeScript является компилируемым языком, нам потребуется пройти через этап сборки для компиляции кода в JS. Файл package.json содержит следующие скрипты:
"scripts": {
"vscode:prepublish": "npm run compile",
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./",
"postinstall": "node ./node_modules/vscode/bin/install",
"test": "npm run compile && node ./node_modules/vscode/bin/test"
},
Сборку кода можно выполнить скриптом compile
. И здесь мы сталкиваемся со второй проблемой:
$ yarn compile
warning package.json: No license field
warning adjust-heading-level@0.1.0: The engine "vscode" appears to be invalid.
$ tsc -p ./
error TS5057: Cannot find a tsconfig.json file at the specified directory: './'.
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
▍ Проблема 2: не удаётся найти tsconfig.json
TypeScript для компиляции в JS необходим файл конфигурации tsconfig.json. Если заглянуть в нашу изначальную структуру файлов, то среди них его нет.
.
├── extension
│ ├── package.json
│ ├── src
│ │ ├── commands
│ │ │ ├── decreaseHeadingLevel.ts
│ │ │ ├── increaseHeadingLevel.ts
│ │ │ └── setHeadingLevel.ts
│ │ └── extension.ts
└── shared_dependencies.md
Это можно исправить, добавив необходимую конфигурацию и повторив сборку. Но теперь у нас возникают другие проблемы:
$ tsc --init
$ yarn compile
src/commands/decreaseHeadingLevel.ts:1:25 - error TS2307: Cannot find module 'vscode' or its corresponding type declarations.
1 import * as vscode from 'vscode';
~~~~~~~~
src/commands/decreaseHeadingLevel.ts:30:24 - error TS7006: Parameter 'editBuilder' implicitly has an 'any' type.
30 await editor.edit((editBuilder) => {
~~~~~~~~~~~
src/commands/increaseHeadingLevel.ts:1:25 - error TS2307: Cannot find module 'vscode' or its corresponding type declarations.
...
Found 7 errors
▍ Проблема 3: не удаётся найти модули
Typescript не может найти модуль vscode
из-за синтаксиса, который мы используем для инструкций импорта:
// не сработает
import * as vscode from 'vscode';
// сработает
import vscode from 'vscode';
Разница в синтаксисе обуславливается отличиями между модулями CommonJs и ES, а также между тем, как они экспортируют зависимости и тем, как эти зависимости транспилирует TS. О подобных раздражающих причудах совместимости модулей можно написать целую отдельную статью. Что же касается нашей проблемы, то её можно исправить отключением esModuleInterop
в tsconfig.json:
@@ -71,7 +71,7 @@
- "esModuleInterop": true, /* Генерирует дополнительный JS-кода для лучшей поддержки импорта модулей CommonJS. Это активирует 'allowSyntheticDefaultImports' для совместимости типов. */
+ "esModuleInterop": false, /* Генерирует дополнительный JS-код для лучшей поддержки импорта модулей CommonJS. Это активирует 'allowSyntheticDefaultImports' для совместимости типов. */
Заметьте, что esModuleInterop начиная с версии TypeScript 4.4, стал по умолчанию true
. Эта версия вышла 15 марта 2022 года – то есть о ней GPT не знает.
Попробуем повторить сборку. На этот раз нас ждёт всего одна новая ошибка:
$ yarn compile
src/commands/setHeadingLevel.ts:2:10 - error TS2305: Module '"../extension"' has no exported member 'adjustHeadingLevel'.
2 import { adjustHeadingLevel } from '../extension';
Found 1 error.
▍ Проблема 4 – отсутствует экспортируемый член
Эта ошибка возникает в результате попытки импортировать несуществующую функцию. В частности, она вызвана следующей логикой в setHeadingLevel.ts:
import * as vscode from 'vscode';
import { adjustHeadingLevel } from '../extension';
export async function setHeadingLevel() {
...
}
При объявлении зависимостей GPT склонен ко лжи излишнему оптимизму. Иногда он вызывает или импортирует функции, которых не существует. Перед нами один из таких случаев.
Исправить это можно, удалив лишнюю зависимость и добавив вручную логику в setHeadingLevel
:
@@ -1,5 +1,4 @@
import * as vscode from 'vscode';
-import { adjustHeadingLevel } from '../extension';
export async function setHeadingLevel() {
const editor = vscode.window.activeTextEditor;
@@ -14,6 +13,12 @@ export async function setHeadingLevel() {
vscode.window.showErrorMessage('invalidSelection');
return;
}
+ const headingRegex = /^(#+)s(.*)$/;
+ const match = selectedText.match(headingRegex);
+ if (!match) {
+ vscode.window.showErrorMessage('Invalid selection.');
+ return;
+ }
const inputOptions: vscode.InputBoxOptions = {
prompt: 'setHeadingLevelPrompt',
@@ -31,6 +36,16 @@ export async function setHeadingLevel() {
if (headingLevel) {
const newHeadingLevel = parseInt(headingLevel);
- adjustHeadingLevel(editor, selection, selectedText, newHeadingLevel);
+
+ const newHeading = '#'.repeat(newHeadingLevel) + ' ' + match[2];
+
+ await editor.edit((editBuilder) => {
+ editBuilder.replace(selection, newHeading);
+ });
}
}
Заметьте, что бо́льшая часть кода была взята из decreaseHeadingLevel.ts.
Снова сборка. На этот раз успешная.
$ tsc -p ./
✨ Done in 0.80s.
Но запустится ли итоговая программа?
▍ Шаг 3: запуск
Обратите внимание, что GPT не предоставляет инструкций ни о запуске расширения, ни о его установке или сборке. Это довольно легко сделать, если вы уже собирали расширения VSCode ранее, но у новичков тут могут возникнуть сложности.
Для запуска расширения необходимо перейти в панель Run and Debug и выполнить задачу vscode extension
при открытом файле extension.ts в редакторе.
Так, вы вы запустите новое окно VSCode с установленным расширением. Здесь также возникла ошибка, как только я попробовал выполнить команду.
Command 'Increase Heading Level' resulted in an error command 'adjust-heading-level. 'increaseHeadingLevel' was not found
▍ Проблема 5: команды не найдены
VSCode знает о командах, которые объявлены внутри package.json.
В нашем package.json указаны следующие:
"activationEvents": [
"onCommand:adjust-heading-level.increaseHeadingLevel",
"onCommand:adjust-heading-level.decreaseHeadingLevel",
"onCommand:adjust-heading-level.setHeadingLevel"
],
...
"contributes": {
"commands": [
{
"command": "adjust-heading-level.increaseHeadingLevel",
"title": "Increase Heading Level"
},
{
"command": "adjust-heading-level.decreaseHeadingLevel",
"title": "Decrease Heading Level"
},
{
"command": "adjust-heading-level.setHeadingLevel",
"title": "Set Heading Level"
}
]
}
После объявления в package.json эти команды также необходимо зарегистрировать в расширении.
Вот наш extension.ts:
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('extension.increaseHeadingLevel', increaseHeadingLevel),
vscode.commands.registerCommand('extension.decreaseHeadingLevel', decreaseHeadingLevel),
vscode.commands.registerCommand('extension.setHeadingLevel', setHeadingLevel)
);
}
Видите, в чём проблема?
В этом файле TS команды объявлены как extension.{COMMAND}
, а в package.json – как adjust-heading-level.{COMMAND}
.
Исправить это можно, скорректировав package.json в соответствии с кодом. И хотя само это исправление довольно простое, для правильного определения проблемы необходимо знать, где конкретно смотреть.
@@ -1,5 +1,5 @@
{
"displayName": "Adjust Heading Level",
"description": "A VSCode extension that lets the user adjust the heading level of the selected text.",
"version": "0.1.0",
@@ -10,23 +10,20 @@
"Other"
],
"activationEvents": [
- "onCommand:adjust-heading-level.increaseHeadingLevel",
- "onCommand:adjust-heading-level.decreaseHeadingLevel",
- "onCommand:adjust-heading-level.setHeadingLevel"
],
"main": "./src/extension.js",
"contributes": {
"commands": [
{
- "command": "adjust-heading-level.increaseHeadingLevel",
+ "command": "extension.increaseHeadingLevel",
"title": "Increase Heading Level"
},
{
- "command": "adjust-heading-level.decreaseHeadingLevel",
+ "command": "extension.decreaseHeadingLevel",
"title": "Decrease Heading Level"
},
{
- "command": "adjust-heading-level.setHeadingLevel",
+ "command": "extension.setHeadingLevel",
"title": "Set Heading Level"
}
]
Примечание: я также воспользовался возникшей возможностью и удалил
activationEvents
– эти события определяют, когда активируется расширение VSCode. В случае активации командами VSCode теперь сможет обнаруживать соответствующие события автоматически, а значит, объявлять их вручную больше не нужно.
Давайте попробуем повторить запуск и повысить уровень заголовка.
Хмм, неожиданный результат.
▍ Проблема 6: понижающее повышение
Вместо повышения — уровень заголовка у нас понижается.
Давайте заглянем в increaseHeadingLevel.ts:
import * as vscode from 'vscode';
export async function increaseHeadingLevel() {
const editor = vscode.window.activeTextEditor;
if (!editor) {
return;
}
const document = editor.document;
const selection = editor.selection;
const selectedText = document.getText(selection);
const headingRegex = /^(#+)s(.*)$/;
const match = selectedText.match(headingRegex);
if (!match) {
vscode.window.showErrorMessage('Invalid selection. Please select a valid heading.');
return;
}
const currentLevel = match[1].length;
const newLevel = Math.max(1, currentLevel - 1);
const newText = '#'.repeat(newLevel) + ' ' + match[2];
await editor.edit((editBuilder) => {
editBuilder.replace(selection, newText);
});
}
Видите проблему?
Это баг, вызванный отличием в один символ.
@@ -19,7 +19,7 @@ export async function increaseHeadingLevel() {
}
const currentLevel = match[1].length;
- const newLevel = Math.max(1, currentLevel - 1);
+ const newLevel = Math.max(1, currentLevel + 1);
const newText = '#'.repeat(newLevel) + ' ' + match[2];
Скомпилируем расширение и заново его запустим.
Работает.
Размышления
И как мы в итоге справились? У нас получилось рабочее расширение, и мы добились от него поставленной изначально цели.
Путь к этому результату не был «автоматическим», и на нём мы столкнулись со множеством проблем. Не обладая достаточным уровнем знаний TS, Node.js и VSCode, мы бы решали их намного дольше.
И хотя в итоге мы смогли сгенерировать рабочий код, в нём есть ряд недоработок:
- отсутствуют инструкции о том, как разрабатывать, использовать или опубликовывать расширение;
- отсутствует
.gitignore
для артефактов typescript/javascript/vscode; - отсутствует файл
launch.json
, конфигурирующий запуск разрабатываемого расширения; - отсутствуют тесты;
- отсутствует повторное использование кода.
Немного статистики
GPT генерирует 9 файлов, включающих ~100 строк TS, ~180 строк json и 17 строк Markdown.
$ cloc --exclude-dir=node_modules,out --not-match-f=package-lock.json --not-match-f=prompt.md --include-ext=ts,json,md .
15 text files.
13 unique files.
7 files ignored.
github.com/AlDanial/cloc v 1.92 T=0.01 s (986.5 files/s, 36610.4 lines/s)
-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
JSON 4 8 0 181
TypeScript 4 22 0 98
Markdown 1 8 0 17
-------------------------------------------------------------------------------
SUM: 9 38 0 296
-------------------------------------------------------------------------------
Итоговая структура программы:
$ tree --gitfile extension/.gitignore
.
├── extension
│ ├── package.json
│ ├── src
│ │ ├── commands
│ │ │ ├── decreaseHeadingLevel.ts
│ │ │ ├── increaseHeadingLevel.ts
│ │ │ └── setHeadingLevel.ts
│ │ └── extension.ts
│ ├── tsconfig.json
│ └── yarn.lock
├── prompt.md
└── shared_dependencies.md
Из ~300 сгенерированных строк нам, чтобы всё заработало, пришлось изменить/добавить ~18.
Выводы
GPT смог сгенерировать бо́льшую часть кода, ориентируясь на простую инструкцию без какого-либо контекста предметной области.
Что хочется отметить:
- GPT-4 отлично справляется с генерацией кода в рамках своей информированности, но наверняка напортачит с логикой, если базовая спецификация изменилась с момента завершения обновления его базы знаний, то есть сентября 2021 года.
- GPT-4 может вносить трудноуловимые баги. В случае с increaseHeadingLevel.ts таким багом оказалось отличие в один символ, которое привело к тому, что расширение выполняло действие, в точности противоположное указанной команде.
- GPT-4 прекрасно подходит для скаффолдинга шаблонного кода, но при этом всё равно важно разбираться в предметной области (пока что). Особенно это актуально при создании программ на основе технологий, изменившихся с сентября 2021 года.
- GPT-4 вносит в процесс программирования ещё один слой абстракции. Теперь у нас есть 7 переходных слоёв для случаев написания TS (число которых может легко удвоиться при использовании контейнеров или VM).
Черепахи…
Дальнейшие шаги
В статье я описал первый эксперимент с простой обобщённой инструкцией без дополнительного контекста, так что здесь можно многое улучшить. Вот некоторые дальнейшие шаги:
- Добавить в базовую инструкцию все проблемы, с которыми мы столкнулись при попытке запустить расширение:
- обобщить эту информацию, составив документацию для расширения VSCode, чтобы сгладить проблему отсутствия последних данных в базе знаний GPT;
- проследить этот процесс, связав LLM, имеющую доступ к современным данным (например, Bard), с GPT;
- обобщить эту информацию, составив документацию для расширения VSCode, чтобы сгладить проблему отсутствия последних данных в базе знаний GPT;
- сгенерировать тесты для проверки логики и заставить GPT выполнять автоматическую корректировку в случае провала этих тестов;
- сгенерировать чек-лист, описывающий критерии качественного расширения VSCode, и заставить GPT выполнять по нему проверку, автоматически исправляя создаваемые артефакты.
Примечание: я уже проделал некоторые из этих шагов и смог свести число ошибок до нуля при первой же генерации. Нужно ещё посмотреть, будет ли мой подход обобщаться на другие примеры.
Автор: Дмитрий Брайт