AI编程的80%诅咒

背景

AI编程已经深入人心,无论是cursor还是trae都让编程看起来非常非常简单,编程全民化的时代看起来已经到来了。
我身边有两个完全不会编程的朋友,最近都不约而同地开始使用AI产品实现自己的idea。
看着他们实现自己的创意过程,跟他们不断地交流,并我加入一些自己的思考,最后形成了这个文章。

AI编程的两种方式

根据我观察下来,AI编程主要分为两种方式,创造和迭代。

创造一个Demo

AI最擅长的是创造第一个demo,当你有一个原型想要实现,或者你想验证一个想法是否可行的时候,把你的想法告诉AI,让AI为你来创造一个可运行的demo效果是非常非常令人惊讶的。
Bolt,v0还有一些其他的号称能截图来生成代码的AI,都可以做到这一点。

迭代你的项目

但是程序员的日常工作就是迭代自己手头的项目。通常来说越有价值的项目,越有迭代的可能。一个一次性创造完就不再更新的项目,要么是需求非常简单,要么是使用场景不负责,要么是运营者不上心。所以,以我为代表的老派程序员更多的是使用cursor或者类似的工具来迭代自己的老项目。

AI编程中的知识悖论

接下来我要写的就是我使用AI这一年所发现的非常反直觉的一件事情。
AI工具对于有经验的程序员的帮助要远远大于新手。
这个看起来很奇怪,不是说AI工具让所有人都可以编程了嘛。

事实上AI在我眼里更像是一个队伍里非常非常热情而不知疲倦的实习生。他们啥都敢写,啥都能写,写的特别多,但是从来不确定自己是否跑在正确的路上。你自己懂的越多,就能更好的利用他们。

我如何使用AI工具

  • 迅速完成一个原型,验证我的想法是否可行。
  • 给出AI明确的指令,让它完成一个非常小的任务。
  • 把自己想要学习的东西告诉AI,让AI给我一个答案,并根据自己的经验判断这个答案是否可靠。
  • 完成一些文档和注释工作。

我的产品朋友如何使用AI

  • 模糊的提出自己的需求,并接受AI生成的所有代码。
  • 忽略AI生成的代码中的安全和性能问题。
  • 在尝试新增功能时,跟AI反复描述自己的需求,期待AI某一次生成的代码可以跟自己的预期相符。
  • 出现Bug尝试问AI为何出现问题。随着Bug的复杂化,整个项目越来越失去鲁棒性。

AI编程的打地鼠游戏

我有一次远程会议观察了我的产品朋友如何使用AI编码,我没有发表任何意见,只是静静的看着他在玩一个AI编程的打地鼠游戏。

  • 他试图修复一个小 bug。
  • AI 提出一个看起来合理的修改建议。
  • 这个修复导致了其他问题。
  • 他让 AI 修复新的问题。
  • 结果又引入了两个新问题。
  • 然后再重复这个过程。

这个循环对非工程师来说尤其痛苦,因为他们缺乏理解问题实质的思维模型。当有经验的开发者遇到 Bug 时,他们可以凭借多年的模式识别经验来推理出可能的原因和解决方案。而如果没有这样的背景,基本上非工程师是在和自己不太理解的代码玩“打地鼠”的游戏。

80%诅咒

我总结的结果是非工程师在使用AI编程时常常会遇到令人沮丧的瓶颈。他们通常能在令人惊讶的短时间内完成 80% 的工作,但最后的 20% 却变成了一个收益递减的拉锯战。
如果把非工程师换成初级工程师。这个比例可以提高到95%。
那么如何解决这个问题呢。
我认为主要为两点:

  1. 这个项目不重要,它只关注前面80%的工作,不需要长尾投入,尽情的使用AI吧。它成倍的增加你的效率。
  2. 把AI当成一个学习工具,而不是编程工具。

未来

80%这个比例随着工具越来越强,会越来越高,有一天如果能达到99%,那么就会大力飞砖,不需要学习了。


我的AI工具使用个人经验

以下部分是我口语化的描述之后的AI优化版。先说结果目前我自己正在开发的项目比之前的质量更高,主要是ai用好了之后写出来的代码更清晰易懂,其次用这种方式对单个功能可以反复尝试不同的设计方案,找到最优设计。

总结如下

一、项目搭建与迭代策略

  • 先试水再成形:初期通过实验项目探索 AI 使用效果,积累经验后再正式搭建项目。
  • 结构设计需提前规划:正式开始时要一次性设计好项目结构和各功能模块,避免中途大改。
  • 目录和模块清晰划分:在文件目录、代码结构上尽量做到一开始就整洁规范,减少后期调整成本。

二、代码质量与审查

  • AI 生成代码需全面 Review:AI 生成的代码大多逻辑可通,但常有“绕远路”或使用不合适方法的情况。
  • Review 有助于理解与重构:通过人工审查代码,能提升对整体架构的理解,必要时可重写 Prompt 或手动修改。
  • 对于不满意的结果,要么重写 Prompt,要么自己重构,避免陷入无效修补的死循环。

三、控制开发粒度

  • 每次开发尽量小步快跑:由于需要代码审查,建议每次只让 AI 开发一个小而清晰的功能。
  • AI 生成逻辑清晰,适合模块化开发:AI 代码条理清晰,适合拆分模块逐步构建,提高可维护性。

四、复杂逻辑与 Bug 处理策略

  • 复杂逻辑建议人机协作

    • 先让 AI 搭建初步框架(约可达 80% 正确),
    • 难点或核心逻辑由人工主导修改,避免 AI 抓不到重点。
  • 复杂 bug 的处理上限:对话三轮

    • 遇到 bug,最多让 AI 尝试三轮;
    • 若仍未解决,及时人工介入,避免 AI 无效修改导致代码混乱。

s3中pdf文件预览和下载的设置

应用场景

使用学校的s3服务,需要控制文件在浏览器中是预览还是下载。

背景知识

通过url访问到的每一个文件,都会返回一个Content-Type,也就是所谓的MIME 类型(媒体类型)

MIME 类型 代表含义 浏览器默认行为
application/pdf 这是一个 PDF 文件 使用内置 PDF 查看器打开
application/octet-stream 这是一个“任意二进制流”(不明确的文件类型) 浏览器通常会提示下载而不是打开

解决方案

如果需要默认行为是预览,就设置媒体类型为application/pdf
如果需要默认行为是下载,就不设置媒体类型,默认的媒体类型就是application/octet-stream

代码示例

我用的golang的示例

1
2
3
4
5
6
_, err = svc.PutObject(&s3.PutObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(objectKey),
Body: file,
ContentType: aws.String("application/pdf"),
})

文件示例

预览PDF文件测试

下载PDF文件测试

原理图例

可以看到预览PDF中的Content-type是application/pdf
图片

可以看到下载PDF中的Content-type是application/octet-stream
图片

sharex和s3的配合

事情起因

最近尝试多写一点blog,写的文字多了自然要追求一下效率。
之前写blog时,图片的相对路径问题一直困扰我。
每次想贴一点图片都要为图片存在哪个目录下,如何写而困扰。

解决方案搜寻

最根本的方法就是使用图床,众所周知图床主要靠绝对文件名和标签来组织图片路径的,所以可以完美避开相对路径的问题。

上传图床

上传图床挺麻烦的,很多图床都要收费。搜索相关讨论时发现了sharex这个神器,截图+上传一体的。下载下来简单试用了一下非常的不错。
看了一下里面支持amazon的S3,想到学校里面提供了s3服务,遂申请了一个桶试试。

sharex配置

直接看图吧(ps:这张图片就是使用这个配置上传的)
sharex配置

展示最终效果

截图之后,url是自动进入剪切板的,然后需要查看这张图片的url也非常方便。
截图之后如何拿到url

使用油猴改变令你不爽的后台管理界面

管理系统改造

下图是我的一个后端管理系统的例子
管理端截图
个人觉得会话标题不太有用,又占用了我宝贵的视野。
除了期待后端系统自己变好以外,用油猴改造后端到自己想要的样子更直接一些。改造好了还能展示给管理系统维护方,直观的看到自己的想法。

油猴代码

用大模型写的,本身不具备啥参考价值。
比较需要关注的字段主要是match的用法,这个代表了作用域。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// ==UserScript==
// @name 隐藏“会话标题”列(同济后台)
// @namespace http://tampermonkey.net/
// @version 1.2
// @description 在 https://tjtx.tongji.edu.cn/tjtx/log 页面隐藏表格中的“会话标题”列
// @match https://tjtx.tongji.edu.cn/tjtx/*
// @grant none
// ==/UserScript==

(function () {
'use strict';

let targetColumnIndex = -1;

function hideColumnByHeaderText(headerText) {
const tables = document.querySelectorAll('.ant-table table');
tables.forEach(table => {
const headerCells = table.querySelectorAll('thead th');
headerCells.forEach((th, index) => {
if (th.textContent.trim() === headerText) {
targetColumnIndex = index;
th.style.display = 'none';
}
});

if (targetColumnIndex !== -1) {
const rows = table.querySelectorAll('tbody tr');
rows.forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length > targetColumnIndex) {
cells[targetColumnIndex].style.display = 'none';
}
});
}
});
}

function observeTableChanges() {
const observer = new MutationObserver(() => {
hideColumnByHeaderText('会话标题');
});

observer.observe(document.body, {
childList: true,
subtree: true
});

// 初始执行一次
hideColumnByHeaderText('会话标题');
}

window.addEventListener('load', observeTableChanges);
})();

效果展示

可以看到下图中会话标题不见了
改造后

使用校级问卷平台和企业微信机器人联动

目的

使用校级的问卷平台,因为是一个持续收集的问卷,肉眼可见填的人比较少,希望填报完了能给工作群一个提醒。
工作群里谁有空就处理下。

效果

1、用户填写问卷

填写问卷

2、企业微信群收到提醒

收到提醒

实现步骤

step1 建个机器人

  • 新建或者找到一个企业微信的群聊
  • 右上角···点击之后,选择添加群机器人
  • 点新创建一个机器人
  • 按照引导填写名字等等
  • 拿到Webhook地址,类似于https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=b42a4410-a3e4-xxxx-98cd-8d27c07xxxxxx,把这个地址存好。

step2 建个问卷

使用学校的校级问卷平台问卷平台新建一个问卷。

step3 设置自动化

创建完问卷之后,跳转到问卷设置,找到子菜单投放与分享,点开里面的自动化设置。
点击新增自动化任务,其他按照直觉填写,触发动作写推送到企业微信。
《企业微信机器人Webhook地址》一栏填写上面保存好的地址。其他按照直觉填写。

step4 测试吧

填写一个问卷,如果顺利就能在企业微信群里像我截图的那样看到这个消息啦。

使用vscode编辑远程服务器上容器中的文件

目的

见标题

实现方式

安装vscode插件

推荐安装Remote Development,里面直接包含了远程工作常用的四个组件。
其中Remote-SSH和Dev Containers是我们这次要使用的插件。

Remote-SSH到远程服务器上

Ctrl + Shift + P 打开命令面板,搜索Remote-SSH:Connect to Host....
运行它,第一次运行会提示使用密码来登录。建议使用证书登录免得每次都要输入密码。

ssh配置文件的地址一般是C:\Users\用户\.ssh,填写配置文件,配置文件建议参考下面格式。

1
2
3
4
5
Host 192.168.165.86
HostName 192.168.165.1
Port 2333
User root
IdentityFile ~/.ssh/id_rsa_sde

然后就会新开一个窗口,登录远程服务器上,在vscode左下角应该有如下ui代表链接到远程服务器上。
img.png
在终端中也可以确认自己直接连在远程服务器上。

在远程服务器上查看docker日志

docker ps可以查看到所有的container列表
使用命令docker logs -f sql-backend来实时查看产生的日志

登录到容器中

Ctrl + Shift + P打开命令面板,搜索Dev Containers:Attach to Running Containner...
会出现正在运行的容器列表,选择你要进入的容器。
会打开一个新的窗口来尝试进入容器,顺利的话应该会左下角有一个UI代表链接成功。
img2.png
这时在终端中可以看到已经进入了容器,并且通过打开文件夹或者文件,都可以直接操作容器中的内容了。

SSH配置文件的位置

一般在~/.ssh/config中。可以在vscode中查看。

从零学一套新技术栈

背景

从零学一套新技术栈,记录下需要的时间
先记录自己已经有的技术栈

前端相关

  • html,js,css懂得基本原理,大概能手撸一个单页面
  • vue2原理不太懂,能跟着教程或者照葫芦画瓢撸一个单页面
  • internet相关的基本原理都懂
  • 版本控制熟练
  • 包管理只会最普通的npm
  • 构建工具完全不懂
  • 鉴权协议熟练
  • Web安全原理略懂

这次想学以React为核心的,如何建立一个简单的单页面应用

1、调研什么是React,什么是Next.js,什么是TailwindCSS

  • 时常:2小时
  • 内容:基本上就看看各种论坛,各种博客

2、学习React基础

3、学习Next.js基础

4、暂时搁置

golang时区问题

现象

一个更新程序,打上的时间tag跟当前时间不一致,相差8小时。

原因

golang使用了UTC时间。
我的golang代码运行容器,容器中没有设置时区,所以使用的是UTC时间。

解决

尝试在golang中加入全局的时区设置,但是没有成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
// 加载东八区时区
loc, err := time.LoadLocation("Asia/Shanghai")
if err != nil {
fmt.Println("Error loading location:", err)
return
}

// 获取当前时间并转换为东八区时区
currentTime := time.Now().In(loc)

// 格式化时间
fmt.Println(currentTime.Format(time.DateTime))
}

会报错 Error loading location: unknown time zone Asia/Shanghai

经过排查,是依赖的镜像alpine:3.18中没有时区文件。
如果需要使用golang中的loadLocation,需要更换镜像。

最后使用debian:bullseye-slim镜像,并在init中,设置时区为Asia/Shanghai,问题解决。

总结

如果不想在dockerfile中安装tzdata设置默认时区,只能在程序启动时手工指定时区。

java时区问题

现象

程序进行迁移时,发现输出的时间差了8个小时

分析

1
2
3
4
5
FROM openjdk:8-alpine

COPY ./target/retire-manage-0.0.1-SNAPSHOT.jar app/retire-manage.jar

ENTRYPOINT ["java", "-jar" , "/app/retire-manage.jar", "-Duser.timezone=GMT+8"]

其实dockerfile中已经指定了”-Duser.timezone=GMT+8”,但是没有生效。
在程序中尝试打印如下日志,发现打印的是GMT的时间

1
2
LocalDateTime now = LocalDateTime.now();
System.out.println("Current time in GMT+8: " + now.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));

解决

在 Java 代码中显式设置时区

你可以通过以下代码来设置 JVM 的默认时区为日本时区。

在启动类中设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import java.util.TimeZone;

@SpringBootApplication
public class Application implements CommandLineRunner {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}

@Override
public void run(String... args) throws Exception {
// 设置默认时区为 Asia/Tokyo
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Tokyo"));
}
}

联通云无法访问自己ip的问题现象和总结

现象 无法访问虚拟机自己的校园网IP

联通云因为是云架构,自己有一套自己的内网IP,可以额外绑定一个校园网IP。
对于虚拟主机自己,如果观察ifconfig返回的网卡结果,可以发现,虚拟主机自己只知道自己的内网IP,是不清楚自己的校园网IP的。
ifconfig的结果
图上是ifconfig的结果,其中红圈为联通云内网ip

校园网ip
上图可以看到我这台虚拟机主机的校园网IP

展示问题复现方式

1
2
3
4
5
6
7
8
9
#启动一个最小化的http服务
docker run -d -p 80:80 nginx:alpine
#如果顺利会展示nginx的welcome界面
curl -m 3 192.168.165.86
#这里因为配置了一个-m 3,3s之内无结果会报一个访问错误
curl -m 3 192.168.165.86
curl: (28) Connection timed out after 3001 milliseconds
#这里因为使用了联通云自己的内网IP,所以也能顺利的拿到nginx的welcome界面
curl -m 3 172.28.1.61

根因归属:我没有那么懂网络,如果按照我个人理解,这一层NAT转换应该联通云来做。

什么场景会触发这个问题

如果因为是用了外部的服务注册与发现框架导致的,那么因为你拿到的ip是校园网ip,自己内部部署了两个以上的微服务,这两个微服务互相访问就会出现问题。
如果同一台机器内部署了多个应用,其中还绑定了不同的域名,就很容易出现这个问题。

解决方法

利用iptables来增加一层本机的NAT,先用下面的命令确认自己现在的转发情况
如果安装过docker会有一个docker的转发,剩下没特殊设置应该是空的

1
iptables -t nat -L OUTPUT

增加一条NAT路由规则

1
2
3
iptables -t nat -A OUTPUT -d 192.168.165.86 -j DNAT --to-destination 127.0.0.1
#再次确认是否成功添加
iptables -t nat -L OUTPUT

操作完之后,测试,应该就可以了

1
2
#这个命令现在会正常返回nginx首页了
curl -m 3 192.168.165.86

把上面的iptable的操作配置为开机自启动

1
2
3
4
5
6
#保存当前的iptable到配置中
iptables-save > /etc/sysconfig/iptables
#然后在系统的启动文件中,附加下面一行
echo "iptables-restore < /etc/sysconfig/iptables" | sudo tee -a /etc/rc.local > /dev/null
#没加过的话,加个可执行权限
chmod +x /etc/rc.local