基于全栈框架实践,探讨 AI 代码生成与传统编码方式的未来演进。提供第一手的思路碰撞。
我们正在开发一种配置语言/DSL,用于构建与 React 和 Node.js 集成的 web 应用。多次有人问我们:"为什么要为 web 应用开发创建一种新语言呢?Github Copilot 不是很快就会为开发人员生成所有代码了吗?"。
以下是我们对这一情况的看法,以及我们认为未来可能的样子。
这篇文章曾在 HackerNews 上趋势排行 - 你可以在这里看到讨论。
为了加快开发速度,我们提出了 IDE 自动补全的概念 - 例如,如果你在使用 React 并开始输入 componentDid,IDE 会自动提示补全为 componentDidMount() 或 componentDidLoad()。除了节省按键次数,也许更有价值的是能够看到当前作用域内有哪些方法/属性可供使用。IDE 对项目结构和代码层次的理解也使重构变得容易得多。
虽然这已经很棒了,但我们如何更进一步呢?传统的 IDE 支持是基于人工编写的规则,如果我们想让 IDE 能够为我们实现常见函数,手工编目和维护会太多了。
如果有某种方式让计算机分析我们迄今为止写的所有代码,并自己学会如何自动补全我们的代码,而不是我们做所有的艰苦工作就好了...
开玩笑的部分撇开不说,我们实际上已经实现了这一点!得益于机器学习的最新进展,IDE 现在可以做一些真正很酷的事情,比如根据函数名称和随附的注释来提议函数的完整实现:
这真是太令人惊叹了!上面的例子是由 Github Copilot 驱动的 - 它本质上是一个在大量公开可用代码上训练的神经网络。我不会深入讨论它如何在幕后工作的技术细节,但有很多很好的文章涵盖了背后的科学原理。
看到这一点,出现了问题 - 这对编程的未来意味着什么?这只是 IDE 自动补全的强化版本,还是更多的东西?如果我们可以只在注释中输入我们想要的内容就可以了,我们是否还需要继续费力手工编写代码呢?
当思考 ML 代码生成如何影响整体开发过程时,有一件事需要考虑,这在查看令人印象深刻的 Copilot 示例时往往不会立即想到。
在这篇文章的目的上,我不会深入探讨代码质量、安全性、法律和隐私问题、定价及类似性质的其他问题,这些问题在 ML 代码生成的早期阶段经常被提出。让我们假设所有这些都已解决,看看接下来会发生什么。
问题是 - 代码生成后会发生什么?谁对它负责,谁会在未来维护和重构它?
虽然 ML 代码生成有助于编写初始代码,但它除此之外几乎无能为力 - 如果该代码在未来需要维护和修改(如果有人使用该产品,就会这样),开发人员仍然需要完全拥有并理解它。
想象一下,我们只有汇编语言,但 IDE 补全工作得真的很好,你可以说"实现一个按升序排序数组的函数",它会完美地生成所需代码。一旦你需要将排序改为降序时,这仍然是你想要在将来回到的东西吗😅?
换句话说,这意味着 Copilot 和类似的解决方案不会降低代码复杂性,也不会减少构建功能所需的知识量,它们只是帮助更快地编写初始代码,并将知识/示例更接近代码(这真的很有帮助)。如果开发人员盲目接受生成的代码,他们只是在创造技术债务并推迟它。
如果 Github Copilot 和其他解决方案不能解决我们学习如何编码的所有麻烦,以及详细理解通过 JWT 的会话管理如何工作,那么什么可以呢?
抽象 - 这就是程序员数十年来一直处理代码重复和降低复杂性的方式 - 通过创建库、框架和语言。这就是我们从原始 JS 和直接 DOM 操作发展到 jQuery,最终到达 React 和 Vue 等 UI 库的方式。
引入抽象不可避免地意味着放弃一定程度的强大性和灵活性(例如,在 Python 中求和时,你不需要确切指定将使用哪些 CPU 寄存器),但重点是,如果做得正确,在大多数情况下你既不需要也不想要这样的强大性。
不对代码片段负责的唯一方式就是它根本不存在。
因为一旦屏幕上的像素改变了颜色,这就是你必须担心的事情,这就是为什么所有框架、语言等的主要好处是更少的代码 == 更少的决定 == 更少的责任。
拥有更少代码的唯一方式就是做出更少的决定,并向计算机提供更少的关于如何执行某项任务的细节 - 理想情况下,我们只会说明我们想要什么,我们甚至不会关心它如何完成,只要它在我们拥有的时间/内存/成本边界内(所以我们可能也需要说明这些)。
让我们看一下 web 应用世界中非常常见的(也是每个人的最爱)功能 - 身份认证(yaay ☠️ 🔫)!典型的代码看起来像这样:
import jwt from 'jsonwebtoken'import SecurePassword from 'secure-password'import util from 'util'import prisma from '../dbClient.js'import { handleRejection } from '../utils.js'import config from '../config.js'const jwtSign = util.promisify(jwt.sign)const jwtVerify = util.promisify(jwt.verify)const JWT_SECRET = config.auth.jwtSecretexport const sign = (id, options) => jwtSign({ id }, JWT_SECRET, options)export const verify = (token) => jwtVerify(token, JWT_SECRET)const auth = handleRejection(async (req, res, next) => { const authHeader = req.get('Authorization') if (!authHeader) { return next() } if (authHeader.startsWith('Bearer ')) { const token = authHeader.substring(7, authHeader.length) let userIdFromToken try { userIdFromToken = (await verify(token)).id } catch (error) { if (['TokenExpiredError', 'JsonWebTokenError', 'NotBeforeError'].includes(error.name)) { return res.status(401).send() } else { throw error } } const user = await prisma.user.findUnique({ where: { id: userIdFromToken } }) if (!user) { return res.status(401).send() } const { password, ...userView } = user req.user = userView } else { return res.status(401).send() } next()})const SP = new SecurePassword()export const hashPassword = async (password) => { const hashedPwdBuffer = await SP.hash(Buffer.from(password)) return hashedPwdBuffer.toString("base64")}export const verifyPassword = async (hashedPassword, password) => { try { return await SP.verify(Buffer.from(password), Buffer.from(hashedPassword, "base64")) } catch (error) { console.error(error) return false }}
这只是后端代码的一部分(仅适用于用户名和密码方法)!如你所见,这里有相当多的灵活性,我们可以做/指定以下内容:
选择身份认证的实现方法(例如基于会话或 JWT)
选择我们想用于令牌(如果使用 JWT)和密码管理的确切 npm 包
解析身份认证头,并为每个值(Authorization、Bearer 等)指定如何响应
为每个可能的结果选择返回代码(例如 401、403)
选择密码的解码/编码方式(base64)
一方面,在我们的代码中拥有那种级别的控制和灵活性真的很酷,但另一方面,要做出相当多的决定(== 错误),特别是对于像身份认证这样常见的东西!
如果有人后来问"你为什么恰好选择 secure-password npm 包,或为什么选择 base64 编码?"我们可能应该用除了"嗯,有一篇来自 2012 年的 SO 帖子看起来很合理,它有将近 50 个赞同。嗯,现在找不到了。加上,它的名称中有'secure',这听起来不错,对吧?"以外的东西来回答。
另一个要牢记的事情是,我们还应该跟踪事情如何随时间变化,并确保几年后我们仍在使用最佳实践,并且包会定期更新。
如果我们尝试应用上述原则(更少的代码、更少的详细指令、说明我们想要什么而不是如何完成),身份认证的代码可能看起来像这样:
auth: { userEntity: User, externalAuthEntity: SocialLogin, methods: { usernameAndPassword: {}, google: {} }, onAuthFailedRedirectTo: "/login", onAuthSucceededRedirectTo: "/dashboard" }
基于此,计算机/编译器可以处理上面提到的所有东西,然后根据抽象级别,提供某种接口(例如表单组件或函数)来与我们自己的 React/Node.js 代码"挂钩"(顺便说一下,这正是它在 Wasp 中的实际工作方式)。
我们不需要关心底层使用的确切包或加密方法 - 这是我们信任的抽象层的作者和维护者的责任,就像我们信任 Python 知道在汇编级别上最好如何对两个数字求和,以及它与该领域最新进展保持同步一样。当我们依赖内置数据结构或依靠垃圾收集器来很好地管理我们程序的内存时,也会发生同样的事情。
别担心,它仍然都在这里,你可以随心所欲地生成所有代码!需要理解的主要要点是 ML 代码生成和框架/语言开发相辅相成,而不是相互替代,而且都在这里停留,最终对开发者社区来说是一个巨大的胜利 - 他们会继续让我们的生活更轻松,让我们能做更多有趣的东西(而不是第 n 次实现身份认证或 CRUD API)!
我认为这里的演变是一个循环(或实际上是一个向上的螺旋,但这超出了我的绘图能力):
语言/框架存在、主流,很多人使用它
模式开始出现(例如实现身份认证或进行 API 调用)→ ML 捕捉它们,通过自动补全提供
其中一些模式成熟并变得稳定 → 抽象的候选者
出现新的、更抽象的语言/框架
这意味着我们在两个方面都在赢 - 当语言流行时,我们可以从 ML 代码生成中获益,帮助我们更快地编写代码。另一方面,当我们不想重复/处理的代码模式出现并变得稳定时,我们会获得一种全新的语言或框架,允许我们编写更少的代码并关心更少的实现细节!
*为了不带偏见,还有其他解决方案提供类似的功能 - 例如 TabNine、Webstorm 有自己的、Kite、GPT Code Clippy (OSS 尝试) 等,但 Github Copilot 最近引起了最大的轰动。
写这篇文章的灵感来源
Is GitHub Copilot a blessing, or a curse? (fast.ai) - 关于 Github Copilot 的客观且非常写得好的概述,带有真实的例子
6 Reasons Why You Should Avoid GitHub Copilot and "Fly Solo" Instead - 提出并质疑 ML 代码生成和 Github Copilot 的潜在缺点
Github Copilot Wants to Play Chess Instead of Code - 一种对 Github Copilot 的新颖方法,其中它被用作对话伙伴而不是编写代码!
Conversational Programming - 一篇具有前瞻性的文章,提议了一个未来,其中 AI 将充当"陪练伙伴"并通过迭代帮助我们达到最优解决方案
Jeremy Howard、Maxi Contieri、Mario Kostelac、Vladimir Blagojevic、Ido Nov、Krystian Safjan、Favour Kelvin、Filip Sodic、Shayne Czyzewski 和 Martin Sosic - 感谢你们的慷慨评论、想法和建议!你们让这篇文章更好了,并确保我不会过度使用梗😄。