都什么年代了,还在用传统方式写代码?
都什么年代了,还在用传统方式写代码?
前言
还在把 AI 当作搜索引擎的替代品,有问题才问 AI,没问题就在那边吭哧吭哧地撸代码?如果是这样,那你真的 OUT了!现在正经人谁还自己一行行地写代码啊,都是 AI 生成的代码——没有 AI 我不写(手动滑稽)。
本文将搁置争议,秉持实用主义,讨论在 AI 可以辅助我们编码的情况下,应采取什么样的实践,从而利用好工具,提高工作效率。
本文将分享 AI 时代的编程新实践,教你如何从一个 "Ctrl + C"、 "Ctrl + V" 工程师,变成一个 "Tab + Enter" 工程师🤣。
开发流程
软件的一般研发流程为:
- 需求分析
- 程序设计
- 代码编写
- 软件测试
- 部署上线
我们在这里主要关心步骤2~4,因为与 AI 结合得比较紧密。虽然需求分析也可以借助 AI,但不是本文的重点,故不做讨论。
程序设计
经过需求分析、逻辑梳理后,在编写实际代码前,需要进行程序设计。
此环节的产物是设计文档,是什么类型的设计文档不重要,重要的是伪代码的输出。
虽然《Code Complete》早就推荐过伪代码的实践,但对此人们容易有一个误区:认为写伪代码花的时间,已经够把实际代码写好了。但 AIGC 时代,此问题可以轻松破解:AI 写代码的速度肯定比人快,因此,只要能找到方法能让 AI 生成符合需求的代码,就值得花时间去研究。而伪代码,就是让 AI 快速生成符合期望的实际代码的最好方式。
为什么这么说呢?因为想要让 AIGC 符合期望,恰当的 Prompt 必不可少。但如何写好这个 Prompt,需要提供多少上下文,才能让 AI 更好地理解我们的意图,这是需要技巧、需要调试的。而经过精心设计的伪代码,本身已经提供了足够的上下文,且意图足够聚焦,减轻了对 Prompt 的要求,又提高了 AIGC 的成功率。
我们来看一下伪代码示例:
plainText = JSON.stringify(data)
digest = hash(plainText) // 防篡改
secret = Symmetric.getKey()
cipherText = encryptText(data, secret) // 防内容泄密
pair = Asymmetric.getPair()
cipherSecret = encryptKey(secret, pair.getPrivateKey()) // 防密钥泄露
return {
digest,
cipherText,
cipherSecret,
pair.getPublicKey(),
}
上述代码主要表达的内容是:
- 对初始数据进行加密
- 返回加密后的相关内容
- 涉及到了一系列的算法
对于伪代码的编写,有如以建议:
- 不要纠结类型,不要局限于某一种编程语言的语法,不用试图写能编译通过的代码——这只会限制了你的表达
- 命名一定要准确,符合领域术语,这一点很关键。这一点可以通过查找资料、看书、问 AI 来实现。千万不要只是生硬地汉译英,不专业的表达会妨碍 AI 的理解
代码编写
代码编写分为以下几步:
- 把伪代码转换成目标语言可执行的代码
- 根据项目情况,对生成的代码进行改造
- 利用 AI 编码辅助工具编写剩余的代码
生成真实代码
让 AI 生成代码的 prompt 很简单,示例如下:
please transform the following pseudo code into practical Java code ```
your code```
根据实际情况,把 Java 替换成 Node.js/Python/Go 即可。
使用 chatGPT 结果截图:
完整代码如下:
import com.fasterxml.jackson.databind.ObjectMapper;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import java.security.*;
public class EncryptionExample {
public static void main(String[] args) throws Exception {
// Prepare the license data
LicenseData licenseData = new LicenseData();
licenseData.setLicenseId("123");
licenseData.setLicenseType("Standard");
// Convert the license data to JSON string
ObjectMapper objectMapper = new ObjectMapper();
String plainText = objectMapper.writeValueAsString(licenseData);
// Hash the plain text
String digest = hash(plainText);
// Generate a symmetric key
SecretKey secretKey = generateSymmetricKey();
// Encrypt the license data with the symmetric key
byte[] cipherText = encryptText(plainText, secretKey);
// Generate an asymmetric key pair
KeyPair keyPair = generateAsymmetricKeyPair();
// Encrypt the symmetric key with the private key
byte[] cipherSecret = encryptKey(secretKey.getEncoded(), keyPair.getPrivate());
// Print the results
System.out.println("Plain Text: " + plainText);
System.out.println("Digest: " + digest);
System.out.println("Cipher Text: " + new String(cipherText));
System.out.println("Cipher Secret: " + new String(cipherSecret));
}
public static String hash(String input) throws NoSuchAlgorithmException {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(input.getBytes());
return bytesToHex(hash);
}
public static SecretKey generateSymmetricKey() throws NoSuchAlgorithmException {
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
keyGenerator.init(128);
return keyGenerator.generateKey();
}
public static byte[] encryptText(String data, SecretKey key) throws Exception {
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, key);
return cipher.doFinal(data.getBytes());
}
public static byte[] encryptKey(byte[] secret, PrivateKey privateKey) throws Exception {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
return cipher.doFinal(secret);
}
public static KeyPair generateAsymmetricKeyPair() throws NoSuchAlgorithmException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
return keyPairGenerator.generateKeyPair();
}
public static String bytesToHex(byte[] bytes) {
StringBuilder result = new StringBuilder();
for (byte b : bytes) {
result.append(String.format("%02x", b));
}
return result.toString();
}
public static class LicenseData {
private String licenseId;
private String licenseType;
public String getLicenseId() {
return licenseId;
}
public void setLicenseId(String licenseId) {
this.licenseId = licenseId;
}
public String getLicenseType() {
return licenseType;
}
public void setLicenseType(String licenseType) {
this.licenseType = licenseType;
}
}
}
把上述代码 copy 下来,放到工程中,根据需要改造即可。
这里特别要说下,强烈推荐使用原版 AI,而不是寻找平替,同样的 prompt,下图是某一中文平替输出的结果:
只生成了函数声明,没有生成函数实现。二者对比,未免相形见绌。
辅助编程工具
改造的过程中,少不了 AI pair programming tools。对此,我推荐使用 Amazon 的 CodeWhisperer,原因很简单,跟 GitHub Copilot 相比,它是免费的😃。
CodeWhisperer 的安装可以看文末的安装教程,我们先来看一下它是怎么辅助我们编码的。
第一种方式是最简单的,那就是什么都不管,等待智能提示即可,就好像 IDEA 原来的提示一样,只不过更智能。
下图示例中,要把原来的中文异常提示,修改成英文,而我只输入了两个字符 IM
, 就得到了智能提示,补全了完整的英文字符串!
可以注意到,上图的智能建议一共有 5 条,相应的快捷键为:
- 方向键 ->,查看下一条提示
- 方向键 <-,查看上一条提示
- Tab,采用该提示
- Esc,拒绝提示
我们再来看第二种 CodeWhisperer 的使用方式,编写注释,获得编码建议。
最后一种就是编写一个空函数,让 CodeWhisperer 根据函数名去猜测函数的实现,这种情况需要足够的上下文,才能得到令人满意的结果。
软件测试
AI 生成的内容,并不是完全可信任的,因此,单元测试的重要性变得尤为突出。
对上述代码编写测试代码后,实际上并不能一次通过,因为前面 AI 生成的代码参数有误。
此时需要一边执行单测,一边根据结果与 AI 进行交互:
经过修改,最终测试用例通过👏!
总结
本文通过案例,展示了 AI 如何结合软件研发的流程,提升我们的编程效率的。
其中,个人认为最重要的是编写伪代码与进行单元测试。有趣的是,这两样实践在 AIGC 时代之前,就已经被认为是最佳实践。这给我们启示:某些方法论、实践经得起时间的考验,技术更新迭代,它们历久弥新。
对于AI编程的到底应该怎么看,其实 GitHub Copilot 的产品标语已经很好地概括了:Your AI pair programmer。在结队编程里,由两个人一起来进行编程活动。一个是 Navigator,负责指导、审查,另一个 Driver,负责具体的代码编写。AI 的出现,其实是取代了 Driver 的角色,而另一个充当 Navigator 角色的人,仍然是不可或缺的。
最后,AI 是否能进一步渗透我们的工作流,还有待探索。此文作引抛砖引玉之用,期待大家的后续分享。
附:CodeWhisperer 安装
下载 2023 年的 IDEA,打开 Plugins Marketplace,找到 AWS Toolkit
安装完成、重启 IDEA 后,点击左下角,按下图所示操作:
如果第一次使用,就点击 1 处进行注册,如果已经有账号了,就点击 2 处使用自己的账号登录。
注册、登录、授权成功后,出现如图所示页面,即可使用 CodeWhisperer。