2026年7月13日
为什么我们应该学会爱上机器人
为什么由 AI 协助开发的软件,作者仍然是那个做出架构决策并检查哪些条件必须成立的人。
那是 2022 年,也可能是 2023 年,我第一次接触大型语言模型。
它并不是我遇到的第一个语言模型。20 世纪 90 年代上大学时,我玩过用 Usenet 帖子训练的 n-gram 马尔可夫模型。我会给它们输入文字,再让它们回答。偶尔,它们也能生成一些看起来像英语的东西。
但这是我第一次遇到一个能给出连贯回答的模型。
我的第一个问题纯属虚荣:
Adam DePrince 是谁?
它告诉我,我是一名 17 岁的男性舞者。它显然把互不相关的新闻报道拼到了一起,其中有足够多的细节让我看出,关于我已故妹妹 Michaela 的材料很可能出现在它的训练数据里。大概是因为看到“Adam”这个名字,它便认定自己虚构出来的舞者应该是男性。
我的第二个问题是:
用 Python 实现一个生成斐波那契数列的函数。
它给了我那个经典的朴素递归解法:没有记忆化,慢得呈指数增长。
就在那一刻,我意识到,世界发生改变只是时间问题。
每一次抽象升级都像是在作弊
四年后,我在日常工作中大量使用 AI。我高产地,也许可以说是近乎强迫性地编写开源软件:有些项目解决我觉得有趣的问题,有些则解决我希望别人也需要解决的问题。
我使用 AI,但我仍然会说:“这是我写的。”
原因如下。
我 11 岁时开始走上计算机科学和数学这条路。类似微积分的想法,比如曲线下的面积、直线的斜率,已经在我脑子里盘旋。我理解三角学,也极其渴望知道计算机究竟怎样计算超越函数。
就在那时,我得到了第一台可编程计算机:TI-99/4A。
它内置 BASIC,使用盒式磁带。对不熟悉这台机器的人来说,那是一个糟糕透顶的编程环境:低分辨率显示器;运行在另一层解释型机器语言之上的 BASIC 解释器;一颗只有 256 字节高速暂存内存的处理器;以及 16 KiB 显存,而它又隔在一条过窄、等待状态多得离谱的总线之后。
我爱极了它。
我开始痴迷于数值算法。有一次,我让计算机连续运行十个小时,用 Gregory-Leibniz 级数近似圆周率。十小时里,它大约完成了 11 万次迭代,算出的结果大概精确到五六位。
后来,我得到一个带磁盘驱动器的 32 KiB 扩展机箱。突然之间,我有了随机访问文件,不必再用盒式磁带;有了 Extended BASIC 和汇编器;也有了 32 KiB 可通过总线寻址的内存,不再几乎完全依赖那一点点暂存区。
这台机器的性质变了,我的痴迷也开始呈现出今天仍能看到的样子。
我花了好几年,把本地报纸上的股票价格逐个敲进电脑,以便用 BASIC 回测交易策略。我又用汇编重写数值处理例程。
选择始终摆在那里:
容易写,还是跑得快。
几年后,上高中时,父母给我买了一台 PC。与 TI-99/4A 相比,它有真正像样的 BASIC 解释器、大得多的可用地址空间,以及货真价实的性能。
但那条分界线依然存在。慢的容易写,快的很难写。
后来,父母又给了我 Turbo Pascal 4.0。
我的世界再次改变。
突然之间,我拥有了接近汇编语言的性能、接近 BASIC 的易用性,而且编译后的程序在 IDE 外运行时,大约还有半兆字节堆空间可用。
我兴奋得发晕。我变得高产。我也觉得自己仿佛在作弊。
如果不写汇编,我还算真正的程序员吗?
我安慰自己说,我能写出比 Turbo Pascal 生成结果更好的汇编。
有一阵子,这甚至确实是真的。
生产力的提升极其巨大。我构建了后来在咨询工作中实际使用的工具包,其中包括一套易于复用的完整窗口系统。
我记得有一次,朋友们为了某个与课程有关的活动来到我家。出于某种我已经记不清的原因,我们需要打印定制标签。但我还记得自己的回答:
给我十五分钟。
我打开 Turbo Pascal,导入自己的图形模块,写了一个程序:输入一段文字,它就把标签打印出来。
这在 1991 年是很厉害的。
此后的每一次重大抽象进步,都给我同样的感觉。
计算机变好了。语言变好了。编译器变好了。到了 21 世纪初,编译器已经经常能生成比我随手写出的汇编更好的代码。如今,现代向量化编译器往往能发现连我都会错过的 SIMD 指令机会,尽管人仍然有大量可以参与的空间。
抽象层每升高一级,我就变得更高产。
Python 取代了 BASIC。
C++ 取代了汇编。
Nanobind 取代了 PEEK、POKE 和 CALL,成为我跨越语言边界的方式。
每次转变都丢掉了一点手艺,却给了我多得多的表达能力。
我的手指只是一条 50 波特的信道
即使有了这些改进,我仍然列着一长串想做的东西,却没有时间把它们做出来。
不是因为缺少想法。
也不是因为缺少能力。
问题在于,我的手指从根本上说只是一条有损的 50 波特通信信道,而我必须通过它传输海量样板代码。
Goblin Store 源于我十多年前就有的一个想法。
Memcached 把对象存放在内存里。但当它存储一个大对象时,其中绝大部分并不需要立刻可用。网络连接是一串字节流,被拆分成一个个数据包大小的块。如果我通过 10 Gbit/s 连接发送 1 MiB 数据,那么从第一个字节离开机器开始,最后一个字节大约要到 800 微秒后才能离开。
而 SSD 的响应时间可能只需 10 到 100 微秒。
如果仅仅因为第一个字节必须马上可用,就把大对象完整放在内存里,那是在浪费内存。
走向另一个极端同样有问题。
如果从机械硬盘提供一个大文件,你可能要等两到四毫秒才能拿到第一个字节。在 10 Gbit/s 连接上,这段寻道时间相当于网络本可以传输约 2.5 到 5 MiB 数据,却只能闲等。
Linux 内核会跟踪访问模式,尽量把热门文件留在内存里,同时让较冷的文件退出缓存。
但对于大对象服务来说,这个缓存单位错了。
你不需要把每个热门文件的全部内容都放在内存里。
你需要放在内存里的,是热门文件的头部。
当网络准备好发送第一千万个字节时,它早已有充分时间从存储设备赶到。把这个字节一直留在内存里,几乎没有好处。缓存这些尾部却有真实成本:要么更冷的对象因此要承受额外寻道,要么你得向云服务商购买实际上不需要的内存。
Goblin Store 就这样诞生了。
它把对象头放在锁定内存中,其余部分则使用异步 O_DIRECT 存储 I/O。请求到来时,它开始从存储设备流式读取尾部,使数据在网络传到相应位置时已经准备好。
思路很简单:
- 对象头放在内存。
- 中段放在 SSD。
- 非常长的尾部放在机械硬盘。
这是一个我想验证了十年的想法,却始终没有足够时间实现。
后来,我有了替我打字的 AI。
我描述架构,说明组件应当如何通信、该使用哪些 API、缓冲应该如何工作,以及边界应当划在哪里。AI 把中间的管道接了起来。
我用 Memcached 与 Goblin Store 做了基准对比。我给 Memcached 一块内存,然后让 Goblin Store 提供相同对象,同时把那块内存的大部分换成 SSD 和机械硬盘容量。
Goblin Store 与它正面对决,而存储成本却便宜约 6 至 7 倍。
你可能会说:“Extstore”,也就是 Memcached 自己基于 SSD 的对象存储。
对象头驻留内存的设计同样击败了它。
如果要靠自己通过那条 50 波特接口敲完整个系统,这项实验在经济上根本不可行。AI 接手机械工作之后,经济账变了。
做决定的人才是作者
这不意味着我只需对机器说一句“给我造个缓存”,然后转身走开。
作者仍然是我。
提交前我会阅读 diff。我会持续掌握架构、数据结构和不变量。我会与计算机争论,会拒绝它的实现,也会在它走向某个看似漂亮却与目标无关的方案时把它拉回来。
品味、纪律和设计决策,来自我 35 年的性能工程经验。
这些经验会体现在基准结果里。
AI 会做出愚蠢的决定。它会拐进奇怪的岔路。有时它缺少上下文,有时它会走认知捷径,把问题套在一个只有表面相似的东西上。
你仍然必须监督它。
你必须理解它在说什么。
你必须能认出它什么时候错了。
也许有一天,这会改变。但今天还没有。
下面是一个真实例子。
快、优雅,而且错了
在另一个项目里,我正在开发 Smith-Waterman 的 AVX-512 实现。除了我在 stride-align 中的工作之外,我不知道还有哪个特别优秀的 AVX-512 实现。
我让 AI 搭一些脚手架,再写一个初步实现。它照做了。
其中有一步,我需要把一个含 64 个有符号 8 位通道的 __m512i 规约成最小值。AI 很高兴地生成了一个标量循环。
起初,我怀疑它是不是知道一些我不了解的编译器自动向量化能力。也许编译器会识别遍历向量类型的循环,把反复调用 std::min 转换成正确的规约指令。
它没有。
我开始写一个分治式规约。
先把一组 32 字节与另一组 32 字节比较,留下 32 个候选值。
然后继续规约:
32 → 16 → 8 → 4 → 2 → 1。
算法很直接,Intel SIMD 却不是。数十年的扩展留下了一堆寄存器宽度、指令族、提取操作和助记符;具体该用哪个,取决于数据从哪里开始,又要到哪里结束。
我正准备翻开 Intel 软件开发手册查找确切的 intrinsic,突然想起来:
我有 AI。
记住函数名,恰恰是这类系统擅长的事。
于是我问:
写出在
__m512i中寻找最小有符号字节所需的 C++ intrinsic。
它生成了一棵紧凑的树形规约。
它也犯了一个微妙的错误。
最后几步使用了 _mm_srli_si128。该指令移动字节时,会用零填充新腾出的通道。对于通用的有符号字节最小值,这可能凭空制造一个输入中从未存在的值。如果所有真实通道都是正数,填入的零就会成为错误的最小值。
代码很快,很优雅,也错了。
这正是人仍须负责的原因。
Intel 也提供了便捷的内在函数 _mm512_reduce_min_epi8,但它最终仍会展开成一串指令。由于这段规约位于热路径上,我希望亲自查看并控制这串指令。
修正后的版本旋转字节,而不是引入零:
#include <immintrin.h>
#include <cstdint>
static inline int8_t reduce_min_epi8(__m512i v)
{
// 64 lanes -> 32 lanes
const __m256i lo256 = _mm512_castsi512_si256(v);
const __m256i hi256 = _mm512_extracti64x4_epi64(v, 1);
__m256i m256 = _mm256_min_epi8(lo256, hi256);
// 32 lanes -> 16 lanes
const __m128i lo128 = _mm256_castsi256_si128(m256);
const __m128i hi128 = _mm256_extracti128_si256(m256, 1);
__m128i m128 = _mm_min_epi8(lo128, hi128);
// Rotate rather than zero-fill. Each step doubles the
// number of original lanes represented by every result lane.
m128 = _mm_min_epi8(
m128, _mm_alignr_epi8(m128, m128, 8));
m128 = _mm_min_epi8(
m128, _mm_alignr_epi8(m128, m128, 4));
m128 = _mm_min_epi8(
m128, _mm_alignr_epi8(m128, m128, 2));
m128 = _mm_min_epi8(
m128, _mm_alignr_epi8(m128, m128, 1));
return static_cast<int8_t>(_mm_cvtsi128_si32(m128));
}
AI 替我省下了记忆和查找提取、比较 intrinsic 的机械劳动。我的审查补上了更重要的部分:判断实现究竟是否正确。
这就是两者的关系。
机器打字很快。
人知道哪些条件必须成立。
一支才华横溢的初级程序员队伍
与 AI 合作,感觉就像带领着一大群喝了过量咖啡、拥有博士水平的初级程序员。
它们很快,几乎读过一切,能够产出惊人的工作量。
但它们并不真正理解目标。
问题的一部分在于,我和大多数人一样,常常不会把自己觉得理所当然的事情说出来。我会漏掉环境约束、个人偏好、架构历史和业务目标。人类同事或许能在几个月的会议和茶水间闲聊中逐渐吸收这些东西。
AI 一开始拥有的,只有我设法写进提示里的内容。
这些系统会继续变好。但今天,我们所处的位置就是这样。
知识不是更不重要,而是更重要
AI 改变了我作为程序员的价值所在。
在机器能瞬间回忆起 SIMD intrinsic 名称的世界里,背诵这些名称的价值降低了。打字的技巧没那么重要了,记住每个函数精确拼写的价值也降低了。
深入的技术知识反而更重要。
例如,我创建了 DragonArray,为 Loongson 生态系统构建可用制品。
项目开始时,NumPy 支持 Loongson 的 128 位 LSX 向量扩展,却不支持 256 位 LASX。未来的 Google Highway 移植也许最终会改变这一点,但“最终”解决不了今天的问题。
移植工作有很大一部分是机械性的,AI 很擅长处理。
但机器总是用 Intel SIMD 的方式思考。
我不得不告诉它:
Loongson 有向量除法。用它。
还有:
Loongson 有类似 F16C 的指令。用它们搭建 float16 转换桥。
以及:
在某些 Loongson 服务器配置上,基础页大小是 64 KiB。这会产生 512 MiB 巨页,而不是你在 x86 上以为会得到的 2 MiB。检测页大小,并先在这里禁用 HugeTLB 支持,直到我们正确实现它。
机器知道怎样把代码敲出来。
我知道应当存在哪些代码。
这一区别并非无关紧要的表面差异,它就是作者身份。
当机器来写代码,人还剩下什么?
我曾经以能背下整套 8086 指令集为荣。我知道各种技巧,也知道这颗处理器与 NEC V20 有何不同。
转向编译型语言时,我失去了一些东西:亲手写下每条指令的手艺。
但我获得了高级语言的表达能力。
汇编变成了一种昂贵、耗费人力的奢侈品,只在问题真正值得时才使用。
同样的转变正在再次发生。
手写每一行代码很慢,而且正越来越像一种奢侈。
机器现在能完成其中很大一部分。
它做得不如我好,离不开监督,也免不了犯错。
但它往往已经足够好,让我可以把时间用在尽可能细致地说明实现、审查结果并修正结构上。
留给我的,正是我真正擅长的核心:
理解更大的机器。
理解各个部分如何组合。
理解那些无人写下的要求。
在基准测试或生产事故证明问题之前,就认出哪里不对劲。
决定应该构建什么。
决定不应该构建什么。
AI 让我得以构建自己认为有用的项目:占用更少内存、并在许多工作负载上超越现有实现的 Redis 兼容数据库;更合理利用存储层级的大对象存储;以及面向那些被行业大多数人忽略的架构的 SIMD 实现。
它也让我有余力去做一些傻乎乎的东西。
写下这些文字时,我正在开发一个远程服务器环境,它支持 agent 转发、套接字转发,也能通过 Mosh 会话传输大文件,因为对于它该如何压缩、又应该提供哪些功能,我有自己的看法。
每月大约 500 美元,AI 给我的感觉像是一支由 30 名程序员组成的团队,我可以带领他们去实现自己想象中的未来。
他们很快。
他们偶尔才华惊人。
他们经常荒唐可笑。
而且,他们仍然需要一个知道机器本该做什么的人。
所以我才会说,这些软件是我写的。