跳到主要内容
没有那张图,审不出 AI 的 bug

没有那张图,审不出 AI 的 bug

原石
原石

· 阅读约 5 分钟

都在说 AI 写的代码要 review。审一直都要审。问题不在审不审。

先贴代码。

struct entry {
    uint64_t seq;
    uint32_t klen;
    uint32_t vlen;
    char     data[];
};

我那个小存储引擎的 WAL 记录格式。seq 单调递增,data 里是 key 和 value 拼在一起的字节。重启时把 WAL 从头读一遍,往空的 memtable 里重放。

重放函数:

static int replay_put(store *s, struct entry *e)
{
    struct skv *old = mem_get(s->mem, e->key);
    if (old && old->seq >= e->seq)
        return 0;                  /* 手里的这条更旧,跳过 */
    return mem_put(s->mem, e->key, e->seq, e->data, e->vlen);
}

那个 >= 是这段的全部内容。

WAL 里同一个 key 会出现很多次。后写的盖先写的。顺序读下来天然就对。麻烦在重启:崩之前写进 WAL 的记录,有一部分可能已经被 compaction 刷进 sst,而 memtable 是空的。你按顺序重放,等于拿一条更旧的记录去盖一个更新的状态。少写那个比较,重启之后一个已经删掉的 key 会活回来。

我为这个吃过一次亏。半夜在它上面坐了两个小时。它现在长在我脑子里。所以 AI 把重放逻辑拿给我,我一眼能看到那个比较在不在。

这不是经验丰富。是我脑子里那份状态迁移的图,比它多一条边。

模型给的是这个:

for (i = 0; i < n; i++) {
    struct entry *e = entries + i;
    mem_put(s->mem, e->key, e->seq, e->data, e->vlen);
}

干净,对称,能跑。测试也全绿。因为测试是重放一次就断言,不会重启两次,更不会在两次之间插一次 compaction。它甚至知道有 seq 这个字段,传进去了,只是没拿它去挡旧记录。

它缺的那件事,刚好是唯一一件需要你在脑子里跑一遍时序才能看出来的事。

这段我改了三遍。第一版根本没比较 seq,因为当时脑子里只有一个念头:WAL 里的顺序就是真实顺序。改到第三版才反应过来,那个顺序只在一次进程生命周期内成立。

现在我把这种判断写在文件开头,当不变量挂着:

/* 不变量:memtable 里任何一个 key 的 seq,
 * 都不小于任何已落盘层里同 key 的 seq。
 * 唯一可能违反它的路径:replay。 */

于是我大概知道,我能外包出去的部分到哪儿为止。样板代码可以给,一个 for 循环、一个 getter、一个序列化结构体,随便。因为样板不携带状态。一件事一旦开始携带状态——谁先谁后、谁覆盖谁、失败之后停在哪一步——它就已经不是样板了。它需要那份图。图没有办法外包。

那条线在哪,得你自己画。画不出来,说明你还没想清楚这东西在干什么。

这跟“要不要审”是同一件事的另一面。

审一个 diff,你的动作是什么。不是找 bug。找 bug 只是副产品。真正的动作是:读这些改动,在脑子里重建一份“这块东西现在长什么样”。读完之后,你得能不看代码说出它的不变量,说出下次改它要先碰哪里。

这份图才是 review 的产出。它后面是给你自己用的——改它、debug 它,都要靠它。那两个被顺手抓出来的 bug,是这张图吐出来的渣。

所以一次提交是三千行,问题不是“审得不够仔细”。是这张图挂不上。三千行里没有哪一部分是你看着长出来的。读完脑子里还是原来那张图,什么也没加进去。到这一步,大家就开始装作读过了,或者干脆丢给一个 AI 审查代理。

让一个没有图的家伙,去检查另一张图的缺失。

有人在评论区讲过他遇到的事:某个 git 代理把两个内容不同的文件报告成一致。他重新跑了一遍系统自带的 diff 才看出改动。我信。链条上每一环都可能在报“看起来对”。这不是哪个环节不可靠的问题。是每个环节都在相信上一环。

初级信模型,高级信初级。中间每一层的图都缺一条边。缺的那条边,恰好是用来发现缺边的那条。所以整条链子上没有一个地方会报错。每一层都觉得自己审过了。

还有一件我想起来就烦的事:跟 AI 一起 debug。

它给你三到五条“先检查这个、再检查这个”。你查完,它给下一批。你查完,它再给下一批。整个过程是一次线性搜索。

但你本来会干的是另一件事——手上有一个假设,设计一个能砍掉一半可能性空间的实验,一刀下去排除掉整片区域。这个实验从哪来?从你脑子里那份故障图来。你知道哪一段是刚改过的,知道哪个组件耦合最少,所以先怀疑它。

那份图的作用不是让你变聪明。是给你一个排序。它告诉你在哪个位置下第一刀。

模型每一轮都在重新问你一遍症状,因为它每一轮都没有那份图。你被拖进它的线性搜索里,一轮一轮陪着它试。最累的不是找 bug,是你被迫放下自己的搜索顺序,去跑它的清单。

我现在改不回来一个习惯:不看别人复述的 diff,看真实的 diff。不看摘要,不看“我改了这几处”。这有点蠢。三千行的时候我也看不动。但让一个我没有图的家伙替我先看一遍,我更不信它。

所以最后就一条规则:凡是准备提交的东西,先问自己不看代码能不能把它现在长什么样讲出来。讲不出来,别提交,坐下来想明白再说。想不出那行 >=,就别指望 review 的时候有人替你想到。

说句题外话。我那个玩具存储引擎,核心数据结构改了三版。第一版我什么都想要,通用、灵活、什么都能换。第三版就剩两件事,存一个键,取一个键。它现在跑得最快,也最好读。

能一眼看出来的东西,是因为你先想过了。这个没有捷径。

原石
原石

把代码当文章写的系统工程师,以源码立论、单线程式拒绝复杂度。

查看主页 →