本篇文章聊聊如何使用 GPT 快速完成一个开源小项目,解决实际的问题,顺手点亮 GitHub 上 Nginx 开源社区的贡献者图标。

“Talk is Cheap,Show you the Code。”

写在前面

整理了一篇本该上个月就发出的内容。

前段时间,有个投资人朋友,问了我好几会到底如何使用 GPT 或相关工具来写代码的,希望能有个“step by step”的教程,正巧前几天有这么一个例子,就写一篇文章吧。

其实,我之前已经写过很多篇关于 Nginx 的实践内容了,我很难说我不喜欢这款实践性极强的开源软件。上个月在折腾内部服务的时候,再次用到了 Nginx 这个老伙计,以及我曾经分享过很多次的 NJS。

为了更快的验证功能(偷懒不想写代码),我打开了 GitHub 上 Nginx 官方社区的 nginx/njs-examples 寻找示例配置。

当我在代码编辑器里打开官方项目的配置时,映入眼帘的是方佛是从 90 年代到现在的内容:

  • 最直观的问题是缩进符号混着用,“Tab”和空格“交相辉映”,而且数量“随心所欲”;
  • 其次不同的内容中,哪怕是相同含义的 return 表达,配置中的写法也是“千变万化”;
  • 最难以让我忍受的是,编辑器中的格式化插件会破坏掉正确的配置语法结构,当我翻看插件代码仓库和其核心依赖组件(nginxbeautifier)的时候,我发现了前者已经因为这个问题将项目归档(弃坑),而后者因为缺少社区贡献者对于解决这类问题也进展缓慢,虽然坚持了 7 年的项目十分值得敬佩。

不光是因为有“强迫症”(代码洁癖),更是因为我希望 Nginx 的配置文件都是简洁、美观,以及可靠的,如果没有靠谱好用的 Nginx 格式化工具,那么就做一个呗。

毕竟,Talk is Cheap。

开源的 Nginx 配置格式化工具,Nginx Formatter

完整项目,我已经上传到了 soulteary/nginx-formatter,希望它能帮到你。

当然,也十分欢迎一键三连

方案设计

动手之前,我们最好先做一个简单的规划,以及针对这个规划做一些适当的可行性调研。

社区已有项目调研

我简单翻阅了社区中有关于 Nginx 配置格式化相关的项目,包括其中一个已经坚持了 7 年的格式化开源软件 nginxbeautifier 的代码和历史演进过程。

我发现在 GitHub 社区中,Nginx 代码格式化相关的工具不多,但却分为了三种语言阵营,两种玩法。按照语言来分类:

  • Python 实现 (nginxfmt.py 项目)
  • JavaScript 实现 (借鉴自 nginxfmt.py
  • Golang 实现(借鉴自 nginxfmt.py

按照处理方式来看,则是下面两类玩法:

  • 基于字符串特征进行格式化处理
  • 基于 AST 语法树进行格式化处理

第一种方法,相对比较“治标”,解决问题会更快一些,但可能会因为 Nginx 配置的演进越来越复杂,解析、格式化能力跟不上迭代,以及判断逻辑不够周全,导致格式化出错。

比如,raynigon/vscode-nginx-formatter 这个在 VSCode 插件市场里被下载了二十万次的插件,就是采用这种方案(基于 JS 版本的 nginxbeautifier),以至于有用户确实反馈,会“损坏”配置。

第二种方法,相对比较“治本”,解决问题更靠谱,但是需要完整的了解 Nginx 配置文件的定义,实现起来需要额外的一些时间。

况且,我也不太相信创建项目有一段时间的语法解析方案,对于现在的 Nginx 配置的支持能力,目前的 Nginx 配置丰富程度早已经不是早些年可比的了。

所以,这里我们先来实现一个能解决问题,但是不那么完美的方案吧。

使用 AutoGPT 做方案交叉验证

当然,在实现之前,我们可以使用 AutoGPT 等方法,来对我们想要做的事情,或者想法进行任务拆解或分析,来为我们“查缺补漏”。

使用 AutoGPT 做方案的交叉验证

类似的工具很多,社区里随便找一个用 Docker 跑起来就好。因为模型的结果有一定的随机性,所以我们可以反复尝试,以及适当调整 “Prompt”,让模型的回答更全面一些。 因为很多项目里使用的“提示咒语”默认都是英文,所以在执行之后,得到的结果也都是英文的结果。

这里我们可以使用 ChatGPT 来进行偷懒,只需要把内容复制粘贴到 ChatGPT 里,然后在上面添加一句要求:“将下面的内容翻译为中文”。

使用 ChatGPT 进行内容翻译

然后,我们稍等片刻,这些内容就变成了阅读更简单的母语内容啦。

使用 ChatGPT 翻译完毕的内容

最终方案设计

结合上文提到的各种内容,结合实现时间成本,我们考虑使用“基于字符串特征进行格式化处理”的方案来解决问题。

我期望工具能够开箱即用,没有任何依赖问题,所以我的基础技术栈选择的是 Golang。

然而,Golang 生态下,并没有类似 Python 或者 JavaScript 生态的格式化工具库,所以我们需要手动实现一个格式化工具库,或者让社区的 Python 或者 JavaScript 代码能够在我们的 Golang 程序中运行,内化为我们程序的一部分。

相比较前者,后者的代码实现更少一些,实现速度更快一些,所以我们就用这个方式来玩吧。

实战:询问 GPT 如何实现基础功能

在前文中,我们提到了开源社区现在的各种实现,以及我们计划使用的方案。在实际 Coding 的时候,我们可以借助 ChatGPT 来完成逻辑。

为了演示最低成本的实现,这里我们虽然能够使用 GPT-4,但是考虑到多数人还是有使用限制,我们用 GPT 3.5 来实现我们所需要的东西。

调整 JavaScript 版的格式化程序实现

虽然 JavaScript 版的格式化程序有用户吐槽,但其实,只要我们修正其中的“corner cases”,程序还是能够使用的。完整代码在项目中的 soulteary/nginx-formatter/internal/formatter/beautifier.js,两百行出头。整体结构如下:

/**
 * - Soulteary Modify the JavaScript version for golang execution, under [Apache-2.0 license], 18/04/2023:
 *   - simplify the program, fix bugs, improve running speed, and allow running in golang
 *   - https://github.com/soulteary/nginx-formatter
 *
 * History:
 * - Yosef Ported the JavaScript beautifier under [Apache-2.0 license], 24/08/2016
 *   - https://github.com/vasilevich/nginxbeautifier
 * - Slomkowski Created a beautifier for nginx config files with Python under [Apache-2.0 license], 24/06/2016
 *   - https://github.com/1connect/nginx-config-formatter (https://github.com/slomkowski/nginx-config-formatter)
 */

/**
 * Grabs text in between two seperators seperator1 thetextIwant seperator2
 * @param {string} input String to seperate
 * @param {string} seperator1 The first seperator to use
 * @param {string} seperator2 The second seperator to use
 * @return {string}
 */
function extractTextBySeperator(input, seperator1, seperator2) {
...
}

/**
 * Grabs text in between two seperators seperator1 thetextIwant seperator2
 * @param {string} input String to seperate
 * @param {string} seperator1 The first seperator to use
 * @param {string} seperator2 The second seperator to use
 * @return {object}
 */
function extractAllPossibleText(input, seperator1, seperator2) {
...
}

/**
 * @param {string} single_line the whole nginx config
 * @return {string} stripped out string without multi spaces
 */
function strip_line(single_line) {
...
}


/**
 * @param {string} configContents the whole nginx config
 */
function clean_lines(configContents) {
...
}


function join_opening_bracket(lines) {
...
}

function fold_empty_brackets(lines) {
...
}


function add_empty_line_after_nginx_directives(lines) {
...
}


function fixDollarVar(lines) {
...
}


var options = { INDENTATION: "\t" };

function perform_indentation(lines) {
...
}


function FormatNginxConf(text, indentSize = 2, indentChar = " ") {
...
}

在实现的过程中,你有任何懒得动手的地方,都可以交给 ChatGPT,比如张贴之前的老代码,询问它这段代码的含义:

使用 ChatGPT 解读代码

尤其是对于陈旧的老代码(特别是别人写的),我们可以通过 ChatGPT 来进行含义解读,并且要求它来一些代码的单元测试。这样可以极大的缩短我们在阅读代码上花费的时间。

使用 ChatGPT 解读后的代码

当然,很多时候,它生成的内容是有问题的,需要我们进行仔细甄别或进行额外的测试验证。但即使如此,也会比我们从零到一自己搞来的快。

让 JavaScript 能够在 Golang 中运行

前文提到,因为 Golang 中没有类似 ngxfmt 或者 nginxbeautifier 类似的工具库,所以最快完成我们需求的方式,除了切换技术栈之外,就是将这些不同语言的程序,在 Golang 中直接运行。

这里我们询问下 ChatGPT:“如何在 Golang 中运行 JavaScript 代码”。

询问 ChatGPT 如何在 Golang 中运行 JavaScript 代码

能够看到,在 ChatGPT 的回答中,推荐我们使用 goja,并给出了最简单的实现。这个项目确实是一个有趣的项目,使用纯 Go 实现的 ECMA 5.1 解析引擎,能让我们在 Golang 中直接运行 JavaScript 代码。

当然,除了 goja 之外,参考我之前一个的开源项目soulteary/rss-can,我们也可以使用更强悍的 v8go 来实现这个功能,实际执行速度更快一些,但会让构建文件的体积稍大一些。

让程序能够一个文件解决战斗

前文提到,我们希望程序能够“一个文件走天下”,不需要带着一堆依赖、配置文件等乱七八糟的东西。

我们都知道 Golang 能够编译成一个文件,但是一般情况下只能处理 Go 文件的编译构建。那么如何将 JavaScript 变成 Golang 的一部分呢?如果是你我的老读者,你一定会想起我曾经提到的 go embed 嵌入方案。

如果你没有了解过这个技术方案,我推荐你看一下 Golang 资源嵌入方案,了解它的来龙去脉、几种方案的性能几何。

不过,这里我们想实现具体功能,并且越快越好,我们不妨直接问问 ChatGPT:“如何在 Golang 里使用 Embed ,嵌入一个 JS 文件。”

如何使用 Go Embed 将 JS 和 Golang 合二为一

使用追问,完成我们要的代码

比如,在上面的章节中,我们询问如何在 Golang 中运行 JavaScript 代码。

结合实际需要,我们应该需要构建一个 Go 的格式化函数,接受一些必要的参数,比如:原始配置内容、缩进数量、缩进符。

那么我们可以在具体的会话中,追加问题:

如何使用 Go Embed 将 JS 和 Golang 合二为一

一般情况下,ChatGPT 的表现是可以的:

ChatGPT 针对追问的回答

类似上面提到的具体代码实现,我们在写工具的过程中会有许多许多。

但是并非每次生成的代码,都能够派上用场,以及并非每次的代码都是正确的,这时,我们可以基于已经生成好的代码,进行多轮对话,让 ChatGPT 的答案,能够接近我们的需求,如果答的不对,我们可以让他重新生成。如果多次重新生成依旧不能让你满意,那么大概率是问题提的不够贴近,我们需要适当调整问题。

优化程序生成的代码

就上面的代码而言,虽然能够满足需求,但是写的未免太过于啰嗦。而默认生成的代码一般都是直白的逻辑呈现,并且因为我们的提问都比较简单,所以都有一些啰嗦。

所以,我们需要针对 GPT 生成的内容做一些优化,比如上面提到的比较关键的 Formatter 函数(位于项目位置 soulteary/nginx-formatter/internal/formatter/formatter.go):

package formatter

import (
	"fmt"

	"github.com/dop251/goja"
)

func Formatter(s string, indent int, char string) (string, error) {
	if s == "" {
		return "", nil
	}
	vm := goja.New()
	v, err := vm.RunString(fmt.Sprintf("%s;FormatNginxConf(`%s`, %d, `%s`)", JS_FORMATTER, s, indent, char))
	if err != nil {
		return "", err
	}
	return v.String(), nil
}

当然,在过程中你也可以咨询 ChatGPT ,具体的细节优化,函数使用。

实战:完善程序阶段

当我们把程序的核心功能实现完毕之后,剩下的就都是比较通用的边边角角的功能或者“质量保证”相关的测试啦。

编写一般功能,都比较简单,使用下面的句式即可完成任务:

  • 使用 Golang 完成 xxx 功能
  • 使用 Golang 语言中的 xx 框架/工具包,来完成 xxx 功能

这里就不多做展开,浪费篇幅了,我们聊聊比较典型的单元测试。

使用 GPT 完成单元测试

应该不会有太多工程师对于写测试感兴趣,尤其是程序频繁变动的前提下,我们写的测试越多,可能随着项目变化变成废代码的可能性也就越高。

但是,如果不需要我们动手,这个事情就能完成呢?

直接问 ChatGPT 要单元测试代码

比如,我们将上面的代码直接粘贴到 ChatGPT 中,要求他完成单元测试。

ChatGPT 完成的单元测试

如果是上下文不够明确、缺少 Heredoc 注释的函数,一般会生成比较泛的代码,如果你愿意完善注释或者多提供一些上下文,那么你将得到覆盖率 80~90%,甚至完全覆盖的测试代码。

实战:编写效率提升

除了和 ChatGPT 聊天,笨笨的复制粘贴代码之外,是否还有更偷懒的方式写代码呢?

答案是有的,借助离线和在线的语言模型即可。

本地代码补全模型的使用

关于离线模型做代码补全,是一个老话题了,如果你追求更快的实时性,以及和代码工具的贴合程度。就个人体验来说,我暂时还推荐 TabNine。

至于其他的工具,建议感兴趣的同学自己试试看,包括性能、生成结果、代码 IDE 的兼容性等等,感觉差距还是挺明显的。

我使用了三年左右,本地模型尺寸拢共 1.2G,如果一周写代码的时间比较多,至少能够帮助我节约 13~30% 的输出时间。

# pwd
/Users/soulteary/Library/Application Support/TabNine/models

# ls
29b87067.tabninemodel b8373e4b.tabninemodel ce94127b.tabninemodel
# du -hs *
241M	29b87067.tabninemodel
685M	b8373e4b.tabninemodel
256M	ce94127b.tabninemodel

# du -hs .
1.2G	.

不过,TabNine 的上限取决于你让它见过的代码有多少,以及有多好,培养好的模型,和喂电子宠物差不多,需要时间。

在线代码补全模型的使用

如果你想开箱即用,并且代码没有那么敏感,那么在线代码补全,会更适合你。

Copilot,好用的代码补全工具

这里唯一推荐的是:github/copilot,如果你的网络通畅,一般情况下你的代码补全都能够在 1s~2s 内完成。

默认情况下,你可能需要花一些小钱,来订阅这个功能。很幸运,我的账号有资格直接使用它。

免费不限量的 Copilot

网上应该有很多对于 Copilot 的介绍了,我这里介绍两个实际使用时的小技巧。

基于上下文(打开的文件)生成

在实际的程序编写中,我们会打开很多不同的文件,但是如果我们要生成的代码只和某个或者某几个文件相关,可以考虑关闭其他的文件。

基于上下文(包括剪贴板)生成

如果我们想针对某段具体的内容进行代码生成,在生成之前,可以顺手复制粘贴一下我们想作为上下文进行代码生成的内容。在生成代码的时候,能省一些事情。

实战:收尾工作

编码工作完成之后,我们还需要做一些内容的收尾。

比如,编写中英双语的项目文档,以及设计项目的 Logo。

使用 GPT 完成开源项目的文档

这里和前文中使用 AutoGPT 一样,我们可以多次提交内容,让 ChatGPT 帮助我们写出项目的框架。

让 ChatGPT 生成项目文档模版

然后我们根据实际情况,把文档中的内容进行替换即可。

至于英文文档,只需要和上文中将 “Auto GPT 内容翻译中文”一样,反过来,让 ChatGPT 将内容翻译成英文即可。

是不是简单省事。

使用 MidJourney 完成项目图标

编写项目最难的部分之一,就是为项目设计一个 Logo。不过现在有了 SD、Midjoruney ,这件事变的太简单了。

我们只需要对它下命令:“帮助我设计一个 Logo,Logo 的内容是…”

使用 Midjoruney 生成项目 Logo

当然,在实际的使用中,如果我们将 prompt 改写为英文,对于模型而言,生成的效果会更好一些。

提前将 Prompt 内容翻译为英文

如果你经常使用 Midjoruney 等图文模型,可以试试使用之前在 GitHub 热榜上待了很久的,我另外一个开源项目:《八十行代码实现开源的 Midjourney、Stable Diffusion “咒语”作图工具》。

其他

好了,文章的基本内容,到这里就聊完了。

我们来聊聊开源社区里的趣事。

开源社区里的趣事

其实去年的时候,在 Nginx 社区里,有一个老外曾留下一个 issue,包含了几个去掉配置中多余空格的修改。

社区里的 PR

我当时看到了这个提交,觉得因为没有提供一致性的标准或可复现的工具,这个属于水 PR。于是,留了一条评论 “这个变更似乎没有必要,或许提供一个通用的格式化工具,对于开发者而言更有价值。”

但是,不论是这个变更提交者,还是项目相关维护者都没有继续进行回复。于是,这个 issue 就挂了一年之久。正巧借着这个机会,就用 ChatGPT 来解决这个事情吧。

彻底解决 Nginx 社区项目的提交

目前,我已经用这个小工具完成了 Nginx 官方配置仓库中的“内容翻修”,以及点亮了 Nginx 开源社区的贡献者图标记录。

点亮了的 Nginx 项目贡献者图标

最后

终于将这篇待在草稿箱里一个月的文章整理了出来,希望接下来,随着业务的正常发展,我能够有更多的时间来分享如何“为了不折腾而折腾”的事情。

–EOF