别抱太高期望
我和 Kevlin Henney 最近讨论了一个问题:将来的自动代码生成工具,比如升级版的 GitHub Copilot,是否有可能取代现有的高级编程语言。我们具体想知道的是,ChatGPT N(N 很大)是否能跳过用高级语言编写代码的步骤,直接生成像今天的编译器那样的可执行机器代码?
这个问题并不仅仅是理论上的。随着编程助手越来越精准,它们很可能不再仅仅是助手,而是开始承担起编写代码的重任。这对程序员来说将是一场革命,尽管编程只是他们工作的一小部分。现在,类似的情况已经开始出现了:比如 ChatGPT 4 能够在 Python 中自动生成代码,还能在一个沙盒环境中运行代码,收集错误信息,并尝试调试。谷歌的 Bard 也有类似功能。虽然 Python 是一种解释型语言,不产生机器代码,但这种自动生成和测试代码的过程完全可以应用到 C 或 C++ 这样的编译型语言上。
这种变化其实并不新鲜:在计算机的早期,程序员是通过插拔电线和输入二进制数字来“编写”程序的,后来才使用汇编语言和(在 20 世纪 50 年代末)如 COBOL(1959 年)和 FORTRAN(1957 年)这样的早期编程语言。对于那些习惯用电路图和开关编程的人来说,这些早期的编程语言简直是革命性的。COBOL 甚至被设计成像写英语那样简单。
Kevlin 强调,高级语言是一种我们暂时还无法替代的“决定性知识库”。虽然这个词听起来有点怪异,但它的重要性不容小觑。在编程的历史上,总有一些这样的知识库存在。以前,当程序员使用汇编语言时,他们需要直接查看二进制代码,以了解计算机的具体操作。而在使用 FORTRAN 或 C 语言时,这种知识库就提升到了更高层次:源代码表达了程序员的意图,而编译器负责将其转化为正确的机器指令。不过,这种知识库的地位一直不太稳定。早期的编译器并不总是可靠的,尤其是在优化代码时更是如此(难道说优化编译器是 AI 的前身?)。而且,代码的可移植性也是个大问题:每个硬件制造商都有自己独特的编译器,带有各自的特性和扩展。在面对程序运行失败的问题时,汇编语言常常是最后的求助手段。这种决定性知识库的有效性通常局限于特定的供应商、计算机和操作系统。为了使高级语言在不同的计算平台上都表现出一致的行为,推动了语言标准和规范的发展。
如今,掌握汇编语言(assembler)的人已经寥寥无几。你只在编写设备驱动程序(device drivers)或与操作系统内核(operating system kernel)的某些复杂部分打交道时才需要它。尽管编程方式已经发生了变化,但编程的结构依旧没变。特别是在使用 ChatGPT 和 Bard 这样的工具时,我们仍然需要一个确定性的知识源,但这个源头已经不再是汇编语言了。无论是使用 C 语言还是 Python,你都可以通过阅读代码来准确理解程序的功能。如果程序表现出意外的行为,这很可能是因为你对语言规范的某个细节理解有误,而不是编译器或解释器的问题。这一点至关重要,因为它是我们能够成功调试程序的基础。源代码以一种合理的抽象层次告诉我们计算机在做什么。如果程序未按预期运行,我们可以分析并修改代码。这可能需要你重新研读 Kernighan 和 Ritchie 的著作,但这是一个已被广泛理解并可控的问题。现在,我们不必再深入研究机器语言了——这实在是件好事,因为随着指令重排序(instruction reordering)、推测执行(speculative execution)和长管线(long pipelines)的出现,理解机器级别的程序比 20 世纪 60、70 年代要复杂得多。我们需要这种抽象层。但这个抽象层也必须是确定性的,即完全可预测和每次编译运行时行为一致。
那么,为什么抽象层必须是确定性的呢?因为我们需要确切知道软件的具体功能。无论是 AI 还是其他计算任务,它们都依赖于计算机能够可靠且反复地执行操作,无论是百万次、十亿次还是万亿次。如果你不确定软件会做什么,或者每次编译时它的行为都可能不同,那么你就无法以此为基础来构建业务。更不用说维护、扩展或增加新功能了,如果每次接触它都会改变,你也无法对其进行有效的调试。
自动化代码生成尚未达到我们对传统编程所期望的可靠性水平。Simon Willison 将这称为“基于感觉的开发”(vibes-based development)。目前,我们还是依靠人工来测试和修正代码中的错误。更重要的是,在开发过程中,你可能需要多次生成代码才能找到解决方案;你不太可能仅凭第一次的提示结果就直接进入调试阶段,就像你不可能一次就用 Python 写出一个完美的复杂程序一样。为一个重要的软件系统编写提示是一项挑战,这些提示往往非常长,需要多次尝试才能得到正确的结果。使用当前的模型,每次生成代码,你都可能得到不同的结果。Bard 甚至提供了几种不同的选择。这个过程并不可重复。如果每次生成和测试的程序都不一样,你怎么理解程序在做什么?如果下一个版本的程序可能与前一个完全不同,你如何判断自己是否在向着解决方案前进?
我们可能会认为,通过将 GPT-4 的“温度”设置为 0,可以控制其回应的变化;这里的“温度”指的是回应之间的变化度(或创新性、不可预测性)。但事实并非如此简单。温度调节只在一定范围内有效,其一限制是提示必须保持一致。如果改变提示以帮助 AI 生成正确或设计精良的代码,就超出了这个范围。另一个限制是模型本身不能更改——但实际上,模型一直在变化,且这些变化超出了程序员的控制范围。所有模型终将更新,而更新后的模型可能生成完全不同的代码。这意味着即使更新了模型,也无法保证生成的代码保持一致。新的模型可能会产生全新的源代码,这些代码需要根据其特性独立理解和调试。
因此,自然语言提示不能成为确保程序确定性的依据。这并不意味着 AI 生成的代码无用;它可以作为一个良好的起点。但在某个阶段,程序员需要能够重现和分析错误:这就是需要可重复性并不能接受意外的时刻。在这一点上,程序员应避免再次从自然语言提示中生成高层次代码。AI 在这里仅起草初稿的作用,这可能比从零开始节省了一些努力。从版本 1.0 升级到 2.0 同样面临类似的挑战。即便是最大的上下文窗口也无法包含整个软件系统,因此必须逐个文件地进行工作——这和我们目前的工作方式相似,但区别在于源代码成为了确定性的关键。
此外,对于语言模型来说,区分哪些部分可以更改、哪些部分应保持不变是一个挑战。比如指令“仅修改这个循环,但不要改变文件的其他部分”可能有效,也可能无效。
这种情况并不适用于像 GitHub Copilot 这样的编程助手。Copilot 的名称恰如其分:它是飞行员的助手,而非飞行员本人。你可以精确地指示 Copilot 完成特定的任务和位置。而当你使用 ChatGPT 或 Bard 来编写代码时,你的角色不是飞行员或副驾驶,而是乘客。你可以指示飞行员将你飞往纽约,但从那一刻起,飞行员就掌控了一切。
那么,生成式 AI(Generative AI)是否会变得足够优秀,以至于能够跳过高级语言直接生成机器代码?一个提示是否可以替代高级语言中的代码?我们已经看到了包含提示库的工具生态系统的出现,而且这些库无疑也包含了版本控制功能。生成式 AI 最终可能会取代日常脚本编程语言,如“从这个电子表格的两列生成图表”。但对于更大型的编程项目来说,需要记住的是,人类语言的价值在于其模糊性,而编程语言之所以有价值,正是因为其明确无误。随着生成式 AI 更深入地渗透到编程领域,我们可能会看到人类语言的风格化方言出现,这些方言具有更明确的语义;这些方言甚至可能成为标准化并得到文档化。但所谓的“具有更明确语义的风格化方言”实际上就是提示工程的另一种说法。如果你想对结果进行精确控制,提示工程并非易事。我们仍然需要一个确定性的存储库,在编程堆栈中一个不会出现意外的层次,一个在代码执行时能够明确告知计算机将执行什么操作的层次。至少目前来看,生成式 AI 还无法承担这一任务。
注脚
如果你在 1980 年代就活跃于计算机行业,你可能会记得有过“逐一重现 VAX/VMS FORTRAN 的错误行为”的需求。