LLM 将根本改变软件工程
讨论大模型对软件工程的长期影响。观点通用但缺乏新见解和具体实践指导。
讨论大模型对软件工程的长期影响。观点通用但缺乏新见解和具体实践指导。
听说过 ChatGPT4、Bing、GPT3 这样的大语言模型吗?我相信你肯定听说过。
在围绕这些技术的炒作中,我经常碰到一种观点——认为这些技术有害,理由各不相同("它们是随机鹦鹉"、"它们生成废话"、"它们不会推理"、"它们会编造事实"、"它们或许会取代初级开发者,但不会取代资深开发者"),虽然从技术上讲这些说法都有一定道理,但这样的观点其实错过了更大的问题:如果你的工作是编写软件,这些技术确实有效。
事实上,它们的效果好到我相信我们正在经历软件开发方式的根本性转变。这将对几乎一切产生深远影响。讽刺的是,在所有职业中,编程可能是最容易被这些技术取代的工作。这篇文章主要写给我的程序员读者:我们正处在一个关键时刻,作为程序员,我们需要在资本主义大肆利用这些技术之前,以我们自己的方式去理解和掌握它们。
我曾写过关于大语言模型对务实型程序员来说是一次范式转变的文章,并开始深入分享我如何使用这些模型及相关应用的见解。我也写过我对这项技术的伦理立场看法,在本文中不再赘述这方面内容。
我认为我们正处在软件开发方式一场巨大革命的起点。我们还不知道如何使用这些工具:我们刚刚发现了外星牵引机技术。许多评论者试图像使用普通老旧的园艺耙一样来使用它,然后因为它彻底破坏了他们的花坛就把它否定了。
我希望能分享一些我在用大语言模型编程时获得的洞察。我发现,培养一种实践、一种方法论、一种工作流对任何智力工作来说都是关键的,无论是软件开发、写作还是音乐创作。因为编程与生产性的团队合作紧密交织(尤其在我们的资本主义背景下),这种实践必须被共享。编程就是协调个人的工作以创造共同的产出物,成功取决于我们的协调能力。
我认为用大语言模型编程即将在软件架构、系统架构、编程实践、沟通模式和组织结构方面引发一场激进的转变。这是个令人兴奋的时代,因为我们正是那些有能力塑造未来编程方式的人。
我首先把自己视为一名程序员。从 5 岁起我就想按按钮让机器做事,计算机一直是我的一个自闭症兴趣所在。甚至我的音乐、绘画、写作都与这些奇妙的机器密不可分。我写过数百万行代码,几乎每一天都会推送几次提交。说这些是要说明我对有效的东西感兴趣,而且我尝试过很多很多东西。我对编程的关心程度足以说明一点:如果大语言模型不好用,我就不会每天都在用它。
从大语言模型进入测试版以来,我就一直在大量使用 Copilot,从 ChatGPT 向公众开放的那周起,我就开始在实际工作中使用它来解决各种问题。我主要编写我称之为"无聊的胶水代码",从事"系统编程"工作,即构建系统(操作系统、嵌入式系统、分布式系统、物流、供应链)。另一个定义可能是"用许多不同的复杂方式编写 memcpy"。
我热爱"质朴"的编程语言:我最喜欢的是 PHP 和 Javascript、Java、Go,以及(原因超出本文范围:Common Lisp)。对我来说,"质朴"意味着你看到的就是作者本意,不一定要很精致:这些语言会显式表达它们的上下文。就像你祖父建造、你母亲升级、现在由你继承的餐桌一样:有点笨重,油漆有所剥落,但近百年来一直尽职尽责没有失败过。Copilot 在这些语言上表现超群。它应该使用的模式往往非常"扁平",而且经常在周围的代码中出现。一个符号的含义不受隐藏特性、抽象或模块系统的影响。不是说这是语言的"复杂性"或抽象级别导致了更差的结果,而是从训练语料中推断你的代码应该是什么样子更加困难。
这就是 Copilot 能够表现出色的原因(虽然我在 2022 年夏天尝试过用它写 Haskell 和 Rust,但之后就没试过了。Copilot 从那以后取得了令人印象深刻的进展,所以我的看法可能已经完全过时。我已经习惯了 Copilot 在我写完开头几个词后诡异地推断出我想做什么的样子。事实上,我注意到我在编程过程中的物理肌肉记忆已经改变了。如果我的网络断掉了,我——天哪!——不得不自己手打代码,我必须进行心理转换。一开始我会写 3 个词,然后期望 10 行代码被自动生成出来。过了几秒钟我才意识到我那个神奇的朋友 Frank(我的 Copilot)已经无故缺席了。
大语言模型的一个常见批评是它们经常输出错误的代码。这是真的(ChatGPT4 大幅提高了标准,但也不难让它输出错误的代码)!然而,仅仅停留在这一点上,我认为没有仔细看。快速写出的、容易被纠正的错误代码,其实就是另一个名字的好代码。
我的大部分编程工作都涉及用长篇幅来写琐碎的想法。我会从"我需要把这里的数据复制到那里"开始(这就是为什么我称它为"memcpy 编程"),然后详细说明 HTTP 调用这个、Promise 那个、SQS 事件在这里、批处理任务在那里。仅仅写下注释"调用 HTTP api /api/products 并将结果发送到我们的 SNS 主题 /products"就足以让 Copilot 基本上完成整个事情。
因为我不是火箭科学家,这些方法会是这样的(伪 Javascript 代码):
http.GET("/api/products").
then((products) => if (validateProducts(products)) {
this.sns.enqueue()
} else {
fail("invalid products")
})
如果你在写完上面的注释后对 Copilot 进行自动补全,它很可能会生成:
http.GET("/api/products").
then((products) => if (validator.checkProducts(products)) {
const sns = new SNS(this.topic)
sns.enqueue(products)
} else {
throw Exception("invalid products")
})
执着于它用了 validateProducts 而不是 validator.checkProducts 这个事实,我认为是错过了要点。实际的工作收益是我现在通常能在 10 分钟内完成原本需要 2 小时的东西。
我认为这个事实的含义远远超过"好吧,现在我们刚好替代了代码猴子"这个层面。我认为能够以这样的速度编写繁琐的代码会关闭许多反馈循环,从而产生能改变我们构建软件方式的涌现效应。
虽然 Copilot 非常渴望发现你的代码库,但它需要看到你喜欢什么风格。它需要看到你已有的 API 和辅助方法,以及要导入哪些包。
解决方案是什么,如果你想要 tab 补全 90% 左右你打算编写的代码?访问几个你想让 Copilot"学习"的文件,或者写一个它应该生成的示例。如果你想让 Copilot 流利地使用一个晦涩的库,只需浏览一下它的源代码,访问几个示例,然后回去疯狂地 tab 补全你的方式来完成一个可工作的应用。
同样的技术对 ChatGPT 也有效。你想要为 ChatGPT 获得不错的输出?只需预先复制粘贴大量示例代码。复制粘贴你的类定义、你的类定义注释、也许一些 DDL、一些示例 CSV。经常粘贴,每次它偏离时都粘贴。取它的代码,纠正它,粘贴回去。复制粘贴、上下文、上下文就是这里需要的东西。
这是我们遇到的第一个方法论的改变。不仅需要为人类编写 API 文档,我们还需要编写代码(和工具!)使 API 对 LLM 可被发现且"可理解"。在大多数情况下,两者相辅相成。清晰简洁的注释能为 LLM 提供它在训练集中见过的上下文,从而帮助它推断出正确答案。清晰简洁的 API,加上有意义的命名,让我们能高效地(token 越少越好)向工具传达我们的意图。
我认为我们会开始看到这样的实践:文档既可被人类阅读,也容易被"机器解析"(简短、简洁,包含针对具体任务的少样本示例)。与其使用 UML、ODL、SOAP、Swagger、JSON Schema 这样的形式化语言,我们会回到简单、直白的 README,提供简短的概述和几个使用示例。这会奏效不是因为简单更好(我们之所以不断重新发明这些东西是有原因的)。它奏效是因为 README 能很好地编码我们的"人类"意图,源代码以极其详细的方式体现我们想让系统做的事,LLM 可以结合这两者生成更"形式化"的代码供机器解释,或生成更少细节的文本供人类解释。
我认为我们还没有意识到,最有效的人类沟通方式现在也是与机器沟通的有效方式。
当然,这是双向的。大语言模型在将糟糕的注释转换成精心编写的、表达清晰的段落方面效果惊人。它们能在我说一声"请"的时间内生成 5 个有趣的示例。它们能在重构后几秒钟内更新现有文档以匹配更新后的 API(之所以现在还显得麻烦,只是因为目前需要在 ChatGPT 中来回复制粘贴)。Copilot Labs 正在试验"brushes",但由于我使用 Intellij 而不是 Copilot,我并没有经常使用它们。
它们可以用同样的方式更新代码以匹配文档改动,或从简洁、精心编写的文档中生成有效的代码。甚至,你可以在终端中 curl 几个端点,不做任何编辑直接将整个内容粘贴到 ChatGPT。然后你可以让它创建一个包含 mock、单元测试、示例和文档的 API 库。这在大多数情况下会输出比我自己写的更好的内容。
我不再费力手动编写任何数据处理/API 包装/结果验证代码。我最近需要与 Google Tag Manager 建立服务器间集成。我只是将网页复制粘贴到一个简单的 3 行提示中,现在就能用一个简单的 shell 命令生成 PHP 类、TypeScript 接口、事件日志解析器和 SQL 序列化。
现在精心编写的文档既是基础又几乎是免费的,我们该做什么?我们变成写手?我们变成编辑?
个人而言,我认为是的。没有理由不编写一流的文档(或通过制表符补全的方式完成),文档风格和文档质量会变得和我们在花括号前放多少个空格一样可自动化和可检查,代码注释再也不会过时(为什么你的 IDE 不标记与代码行为不匹配的文档呢?)。6
如果我们变成编辑,这是否意味着学习成为程序员现在从一开始就是关于学习阅读、批评和纠正代码?这些是我们在过去 30 或 40 年专业软件工程爆炸以来痛苦地转化为实践的技能,通常仅保留给"高级"阶层,初级开发者忙于与编译器较劲。但现在做一个初级开发者实际上意味着变成一个批判性的读者。代码审查是新的编程。7
使用 LLM 会教会你一件事:软件架构关乎模式匹配,而这些模式相当简单。问题是,默认情况下,以 ChatGPT 为例,它类似于一个"设计面试吹牛者"。它会自信地使用大量聪慧的词汇,在白板上画出正确的图表,但完全无法做出有实质意义的观点。问 ChatGPT 如何设计某个应用会导致令人毛骨悚然地相似的大堆陈词滥调(ChatGPT4 在这里已经设置了更高的标准...)。
说正确的话很容易,查找有效的事件驱动架构长什么样很容易,但更难的是搞清楚到底需要做什么、什么容易、什么困难、什么在真实场景中有效、什么失效。但是,使用上面展示的技术,一旦你开始问 ChatGPT 如何通过勾勒潜在 API、充实基础设施、决定使用哪些协议来构建具体应用,你经常会得到大量看起来合理的、具体的代码。
我发现生成"看起来合理"的代码本身就很有用。我不需要信任代码是否正确——整体的结构和凭感觉给了我一种这个东西如何工作、什么是有问题的、什么是巧妙的感觉。它有大量的优质代码可以依靠以发现有趣的模式和命名良好的类;当我想到什么的时候我可以引导它。我基本上有了一个相当"智能"的橡皮鸭随时可用。8 与 ChatGPT 进行头脑风暴感觉很像坐在白板前和同事一起想象事情,除了你经常在最后得到相当接近可工作的代码。虽然我从未和一个人类与 ChatGPT 进行过"三方"白板/橡皮鸭会话,但我认为这将成为一些人的常规做法。
ChatGPT 很擅长生成原型,无论大小。问它一个话题,它不仅会回答,还通常会提供一个用你选择的语言完全可运行的示例。它可能执行也可能不执行,但那是我不必键入的大量真正无聊的东西。一旦 LLM 输出了一个玩具示例,我可以将这个简单的示例改造成许多其他东西:
一个完整的命令行应用
一份文档示例。
一个简单的网页 UI("为一个 post 到 /api/product 并将得到的 JSON 显示为表格的字段编写 HTML。然后编写 CSS 将其样式设置为类似 GeoCities 的页面。"就是这样做的...)
一个用于 CICD 的 Docker 容器
LLM 将探索成本降低到几乎为零。
我最近想为 OBS 编写一个插件,如果我在 1 分钟内没有关闭模态框就停止录制。我以前从未编程过 OBS,但在三小时内我能够做以下事情:
编写一个看起来很有前景的 Python 脚本
与 OBS 搏斗,直到我意识到尝试让 arm64 python 工作太令人沮丧了(浪费了 1h...)
用 LUA 重写脚本并让它工作
意识到我认为是奇怪幻觉的东西实际上是在 LUA 中做类似模态框的最佳方式。决定它不好。
编写一个通过 websocket 控制录制的 Go CLI 应用。
用 Go 编写一个跨平台 UI,向我展示模态框和各种其他按钮来控制录制
我能够尝试两个死路(老实说,这不是 LLM 自身的错),最后得到了一个健壮的可运行工具,我将继续扩展它。我讨厌编写 UI,我讨厌与我不知道的模糊 API 搏斗。我以前只有在某些事情变得极其令人恼火以至于我再也受不了时才写工具。我有一个我正在积极尝试克服的坏习惯:构建个人工具就像它们是为生产规模设计的一样(意思是:在做了 3 天"专业"软件工程后,我筋疲力尽,工具最终落到了沟里,充满承诺却未完成。)
这对专业编程意味着什么是你现在可以编写代码、编写很多代码、编写疯狂数量的代码,然后直接扔掉。没有人会因为你与 ChatGPT 的对话生成了 5000 行代码然后关闭标签页而责怪你。但事实是,你编写了 5000 行代码并决定它们不值得。你上一次这样做是什么时候?
如果形成一种标准实践:先为当前问题拟定一份简洁、表述清晰的说明(参见上一节),然后让 LLM 分别给出 Go 微服务架构草图、Rust 同步多线程草图、TypeScript Deno 版本,甚至可能还有 Lambda 版本,会怎么样?如果让它为 AWS 生成 Terraform,同时也为 Azure 和 GCP 生成,又会怎么样?如果一份架构提案必须先尝试过至少 A、B、C、D 方案并最终选定其一,才能进入评审阶段,而不是与同事无休止地争论 A 和 B 哪个更好,又会怎么样?我们都知道自己存在偏见。我们都有一些坚定的观点,但能够支撑这些观点的证据往往只有一个样本。我通常会在看到实际的代码草图之后更容易被说服。
我们过去常常嘲讽“以说话的速度编写代码”,但如今这已经成为现实。
这就引出了下一项我认为将极其有益的实践。我们都知道工具很重要,也知道高效的工具很难创建,还知道管理层既不在乎工具,也不理解我们为什么需要工具。LLM 让我们能够以所谓的“说话速度”构建工具。我现在可以花 30 到 45 分钟与 ChatGPT 交谈,然后得到一个相当可靠的工具。过去,这大概需要我花 4 到 5 个小时编程,也就是说,考虑到会议、代码评审、午休和各种打断,工作必须分散到两三个工作日完成。而这通常意味着这个工具最终根本不会被构建出来。
以下是我在过去三个月中构建的一些工具:
sqleton——将 SQL 查询作为命令行应用程序运行的工具
escuse-me——用于 Elasticsearch 的同类工具
geppetto——用于 GPT API 的同类工具
biberon——用于 bibtex 的同类工具(非常简陋,但它完成了一项任务)
majuscule——Twitter 话题标签分词原型,带有动态 HTML 调试 UI
一个在我离开电脑时停止 OBS 录制的脚本
许多工作中使用的实用工具:将任意文档转换为代码(参见上面的 Google Tag Manager 示例)。解析并分析搜索日志。针对上述搜索日志的 SQL 构建器。功能完整的 GTM 服务端实现。用于管理横幅及其资源的工具。一个应用程序:通过 OCR 处理发票,使用 GPT 清理结果,在 ElasticSearch 中搜索 SKU,并渲染出与我们库存相匹配的整洁发票;为我进行过的每一次数据湖调试工作创建的报告生成器;Google Tag Manager JSON 导出的文档生成器
将任意文档转换为代码(参见上面的 Google Tag Manager 示例)。
解析并分析搜索日志。
针对上述搜索日志的 SQL 构建器。
功能完整的 GTM 服务端实现。
用于管理横幅及其资源的工具。
一个应用程序:通过 OCR 处理发票,使用 GPT 清理结果,在 ElasticSearch 中搜索 SKU,并渲染出与我们库存相匹配的整洁发票
为我进行过的每一次数据湖调试工作创建的报告生成器
Google Tag Manager JSON 导出的文档生成器
现在,我可以针对每一个想解决的小痛点,每天构建两个高质量工具。编程体验因此发生了多么根本性的变化,我简直无法用语言表达。
我认为,抽象能力主要是通过观察并亲自尝试足够多的具体用例习得的,在此过程中,更高层次的结构才会逐渐形成。抽象是一把双刃剑,因为不合适的抽象会带来持续不断的摩擦。LLM 让我们既能在“模糊抽象”的层次上工作(抽象尚未成熟到可以形式化,但它正在形成,并影响着描述问题时使用的措辞),又能快速探索具体实现;不过,要想“控制”LLM,最好的办法还是先在头脑中扎实地理解问题。
在使用对话式 LLM 时,我亲身经历过一个非常现实的缺点:人们很容易不断地围绕一个问题聊天、修修补补,期待模型最终会在某个时刻“搞懂”。当你自己都没有真正理解试图解决的问题时,这种情况会更加严重。你的注意力会从开展富有成效的对话,转移到被幻觉绊倒,并在其误导下进行毫无意义的追查。在这种情况下,我发现自己会彻底断开互联网连接。然后,我只依靠离线文档和一本书,直到对自己面对的问题有了更清晰的认识。
掌握了“真正的”知识之后,向 LLM 提出正确的问题,就能带来效率高得多的会话。任何把“提示词工程”贬低为荒谬学科的人,都没有花足够多的时间尝试编写有效的提示词。
我希望未来的对话式智能体能够识别并提示这些低效的恶性循环。目前,ChatGPT 只会继续与你互动,但我可以想象,它将来可能会在某个时刻停下来,为你指出合适的教程和资源。另一种选择是主动询问更多细节。ChatGPT4 似乎取得了相当令人印象深刻的进步(我只认真使用了两天,所以还无法形成真正可靠的看法)。这个领域教会我的一件事,就是不要对下一代产品的能力妄下定论。它可能仍然存在当前模型所具有的那些根本性问题,但也可能变得“足够好”,以至于从所有实际用途来看,那些问题已经无关紧要。
我认为,工具开发的一个主要方向将是“持续代码评审”。模型可以观察你构建软件的过程,推断你的意图和思维结构,并针对你的方法提供反馈。它可以标记拼写错误、安全问题以及不符合惯用方式的 API 用法。ChatGPT4 能够纠正我的代码,并自行将问题相当有效地拆分为接口和函数,这令我印象深刻。事实上,我甚至会说,在局部范围内,ChatGPT4 是一个比我优秀得多的程序员。它掌握了多得多的惯用写法,不会忘记安全问题,还能以采样 token 的速度生成单元测试。
有些人认为,这些模型会导致充斥着安全错误的劣质 Stack Overflow 代码泛滥,他们忽视了这些模型在自己擅长的事情上进步得有多快。我认为,这是因为人们很容易忘记,如今互联网上已经可以找到如此之多的优质代码。优秀代码必然会在其训练语料库中得到更加广泛的传播,而该语料库也几乎肯定经过了严密审查和调整。从 ChatGPT3.5 到 ChatGPT4,在软件架构“论述能力”上的飞跃,已经极其清楚地证明了这一点。
我会使用一系列提示词,让模型针对安全问题、我遗漏的边界情况以及表述不清晰的文档提供反馈。目前,我仍然必须手动将这些内容复制粘贴到 ChatGPT 中,补充缺失的上下文,并完善它给出的答案。然而,这只是一个工程问题。模型本身已经表现得非常出色。它远胜于我职业生涯中收到过的大多数代码评审,而且反馈是即时的,还会同时给出复现已发现问题的示例和修复建议。
现在,基本上已经更容易……