图片

最近,我一直在尝试使用语言模型(LLM)来编写代码。我发现它们非常擅长生成小而完整的代码片段。不幸的是,如果需要生成更多的代码,就需要人类评估 LLM 的输出,并提供适当的后续提示,进行优化,之后才可以使用。

目前,大部分“GPT 写了 X”的案例都是如此——人类充当 LLM 的 REPL(Read-Eval-Print Loop,读取-求值-输出循环),仔细引导大模型产生功能性的结果。这并不是贬低这个过程的意思——它的可行性实则令人惊叹。但我们能否进一步呢?我们能否使用 LLM 一次性生成复杂程序的所有代码,而无需任何人类介入呢?

 

编写一个 VSCode 扩展

为了测试 GPT-4 生成复杂程序的能力,我让它创建一个 VSCode 扩展,允许用户能够调整选定的 Markdown 文本的标题级别。这个任务需要:

  • 针对如何搭建和将程序公开给 VSCode 的领域特定知识
  • 混合使用多种语言和平台:VSCode 扩展是使用 TypeScript 编写的,需要编写TypeScript、Node.js 和 VSCode 的配置
  • 生成多个文件
  • 生成用于调试、构建和运行代码的脚手架

设置

在这个实验中,我使用 GPT-4 进行所有的生成任务。我发现 GPT-4 在当今的模型中效果最好。

此外,我使用了开源的 smol-ai 框架来生成代码。

smol-ai 在 README 中的描述如下:

这是一个“初级开发人员”代理(也称为 smol dev)的原型,一旦你给它一个产品规范,它就能为你搭建整个代码库,但它并不会过度承诺通用人工智能(AGI)。与创建特定、固定、一次性的启动器(如 create-react-app 或 create-nextjs-app)不同,这基本上是一个创建任何应用的框架,你可以与 smol dev 紧密循环开发。

我喜欢 smol-ai 的简洁性。整个代码生成逻辑都在一个包含三个主要函数的单个 Python 文件中:

  1. 生成执行提示所需的文件列表(例如 package.json、index.js 等)。
  2. 生成执行提示所需的共享依赖列表(例如 axios、react 等)。
  3. 对于生成的文件列表中的每个文件,生成应该放入文件中的代码,如果适用的话,使用共享依赖。

需要注意的是,smol-ai 还提供其他功能,比如能够调试错误和根据当前代码库生成提示。这些在本文中没有展开来讲。

Prompt

我先基于给出一段提示语,来描述我的问题:

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"

请注意,这个提示是简短且概述问题有些笼统。我想看看 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. 安装依赖项

2. 编译代码

3. 运行扩展

第一步:安装

我们在尝试安装依赖项时遇到了第一个问题:

$ 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 引擎决定了 vscode 的最低版本。现在的(截至 2023-05-23)引擎版本是 1.78。1.62.0 版本是在 2021 年 10 月 21 日发布的。

这与 GPT-4 的知识截止日期相吻合:

GPT-4 通常缺乏对其绝大部分数据截止日期(2021年9月)之后发生的事件的知识。

1.6.2 的 vscode-test 版本与 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.

第二步:编译构建

由于 Typescript 是一种编译语言,我们需要执行一个构建步骤,将代码编译成 Javascript。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"
  },

我们可以通过运行编译脚本来构建代码。这就是我们遇到下一个问题的地方:

$ 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 需要一个 tsconfig.json 文件来编译成 Javascript。如果你记得我们最初的文件布局,没有 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 模块的原因是我们使用的导入语句的语法:

// this is failing
import * as vscode from 'vscode';

// this would work
import vscode from 'vscode';

语法不同的原因来自于 CommonJs 和 ES 模块之间的差异,以及它们如何导出依赖关系,以及 Typescript 如何转译这些导出。模块兼容性中令人抓狂的怪异现象本身就可以再写一篇博文来详述了。只是现在,我们可以通过在 tsconfig.json 中禁用 esModuleInterop 来解决这个问题。

@@ -71,7 +71,7 @@
-    "esModuleInterop": true,                             /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
+    "esModuleInterop": false,                             /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */

请注意,从 Typescript 4.4 开始,esModuleInterop 默认改为 true。这是在2022 年 3 月 15 日发布的–在 GPT-4 所获得的最新知识截止日期之后。

让我们再次尝试构建。这次只有一个错误了:

$ 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.

接下来就是运行啦~~

第三步——运行

请注意,GPT 没有提供关于如何运行该扩展的说明。也没有说明如何安装或构建该扩展。如果你以前构建过 vscode 扩展,这很简单,但这对新人来说可能会有些难度。

运行一个 VSCode 扩展需要你去 “运行和调试 “面板,当 extension.ts 文件在编辑器中处于打开状态时,启动 vscode 扩展任务。

图片

这启动了一个新的 vscode 窗口,安装了我们的扩展。当我试图调用一个命令时,这也会出错。

Command 'Increase Heading Level' resulted in an error command 'adjust-heading-level. 'increaseHeadingLevel' was not found

问题 5——找不到命令

当命令在 package.json 中声明时,VSCode 就会知道它们的存在。

我们的 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)
    );
}

你看到这个问题了吗?

Typescript 文件将命令声明为 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 现在能够自动检测它们,因此不再需要手动声明了。

让我们再试着运行并增加 header 的级别。

图片

嗯,这是不应该发生的

问题6–递减的增加

我们没有增加 header,而是让 header 水平下降。

让我们看一下 increateHeadingLevel.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];

让我们再次编译和运行它。

图片

好了,没问题了!

 

思考

那么,我们做得怎么样?我们得到了一个有效的扩展程序,也完成我们提示中设定的目标。

达到这一点的过程并不是 “自动”的。一路上我们遇到了很多问题。如果事先没有对 Typescript、Node.js 和 VSCode 有一定的了解,这些问题将需要花费一段时间来调试。

而且,尽管我们能够生成工作代码,但仍有许多地方需要改进,如:

  • 没有关于如何开发、使用或发布该扩展的说明
  • 没有针对 Typecript/javascript/vscode 工件的 .gitignore 文件
  • 没有配置开发中运行扩展的 launch.json 文件
  • 没有测试
  • 没有代码重用

 

一些统计数字

GPT 生成了 9 个文件,涵盖了约 100 行的 Typescript,约 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年9月)后发生了变化,则可能会产生错误的逻辑。
  • GPT-4 会产生细微的 Bug。在 increateHeadingLevel.ts 的案例中,就是一个字符的差异导致扩展做了与该命令应该做的完全相反的事情。
  • GPT-4 在搭建模板方面非常出色,但领域的专业知识仍然很重要(目前)。当建立在 GPT-4 的截止日期后发生变化的技术上时,这一点尤其真实。
  • GPT-4 为编程引入了另一个抽象层。我们现在有 7 个翻译层,用于编写 typescript(当涉及到容器或虚拟机时,可以很容易地增加一倍)。

图片

未来的方向

我在最初的实验中使用了一个非常简单的提示,并没有提供额外的背景信息,这意味着有很多改进的余地。这是接下来的一些步骤和想法:

  • 在尝试运行扩展时遇到的每一个问题,将其作为一个细节包含在 GPT 的提示中,以引起注意

    通过对 VSCode 扩展文档进行索引和总结,以减轻 GPT 当前索引中没有的新信息,从而推广这一做法。

    探讨通过将能够访问当今的上下文(如Bard)的 LLM 与 GPT 链接来实现这一目的。

  • 生成测试来验证逻辑,并在测试失败时让 GPT 自动纠正。
  • 为高质量的 VSCode 扩展生成一个检查表,让 GPT 验证并自动纠正它生成的工件。

注意:我已经运行了这些步骤的一部分,并且能够在第一代中使错误数量为零。你可以看看它是否可以推广到其他案例中。

也欢迎分享你使用 GPT-4 做开发的经验。

Loading

作者 amtbbsportal