最近两个 AI 没搞定的 case 分析

2026-07-21

最近在使用中又遇到的两处 AI 没有搞定的 case,分析一下原因,记录下来,下次遇到类似的场景希望可以更容易速通。

第一个 case 是实现一个扩展的 raft 协议。

前阵子写过一篇博客,是关于 raft 如何支持 2F1A。这个东西有原始的论文, 论文中是有 TLA 写的正确性证明的。然后也有在 kubernetes 落地的, 所以可行性方面没问题。而且 etcd 那边已经有一个 PR 是可参考的实现。

我要做的就是让 AI 把这些抄过来,先抄 raft 层,然后再适配上层的应用。 先跟 AI 讨论好 design,AI 很快就把代码写好了。

然后是测试设计,像这种关系到正确性的系统,测试才是关键。我大概分了三块内容:

  • 第一块是单测覆盖率,给 AI 一个验收标准比如新引入的相关代码的测试覆盖 85%
  • 第二块是项目内的集成测试,给它约定好要的一些场景,比如 multi-raft 的 conf change
  • 第三块是混沌测试,这块是最重要的,给一个跑着的集群,故意注入各种网络分区,杀节点等故障,观察系统正确性和恢复时间

前两项测试 AI 都快速莽过去了,最关键也是最难的混沌测试那里,一直撞墙。 就是测试跑挂了之后,叫 AI 自己分析日志,自己定位原因并修复,这一步它一直都搞不定。

典型的症状就是,它自信满满地说,找到了 bug 是啥啥啥,然后说已修复。然后我来重跑,结果还是挂。 一开始我想莽过去,撞了几次墙之后我意识到,又遇到 AI 能力边界场景了。像这种莽不过去的场景,基本上就是 token maxing 都莽不过去的。 甚至一次 debug 过程,几乎可以把 5 小时限额直接清空,都还是做不出正确的 fix,纯粹烧 token。这时人就必须下场自己分析了。

AI 失败的过程是这样的,它大概可以通过日志看出来 raft 的行为失败了。但是失败在哪一步,它做的判断是不准确的。 位置判断的不准确,造成它的修复就不对,修错了位置,就会出现,有时候它把这处修复了,但其它问题又会在另外的地方冒出来 -- 因为它的根因判断就是错的。

举一些细节的例子,当两个 raft peer 之间网络 partition 了,然后 witness 在 term 9 给其中一个 peer 投票成为 leader,然后在 term 10 又给另一个 peer 投票成为 leader。 网络隔离后,term 9 的 leader 还在活动。你问他是不是脑裂了,它说是。问它这种情况是否合理,它说合理,然后又要去补一个 peer 跟 witness 之间的心跳,说加上这个心跳,就可以让 stale leader 给 witness 通信的时候, 意识到自己 stale 了,然后退回到 follower。一派胡言嘛!但是如果人类没有自己的主见,就会说,“哦,好,去修复吧”。

再比如 extended raft 那边 witness 本来是不需要存 log index 的。但是 AI 分析 bug 的时候, 它会觉得,这里 witness 给 term 10 leader 投票是因为 witness 没存 log index,没有用 log index 跟普通 peer 的 term index 去比较,导致的 witness 在不应该给 peer 投票的时候投了票。 于是它又去给 witness 加一个 log index 的持久化字段。这其实是标准 raft 的行为,但是不是 extended raft 论文的行为。AI 产生了幻觉,它用自己可能训练时就已经知道的 raft 知识,去判断我们给它的一篇新的论文中的知识。 如果程序员不自己读一读 extended raft 论文,就没法识别这里它搞错了,只能听经的“哦,好,去修复吧”。

经过很长的日志分析的上下文之后,它会忘记什么样的行为是正确的。然后它会引入自己认为的正确标准。 由于 raft 协议比较复杂,在第一步出错之后,后续好些位置的其它错误会接踵而来。AI 判断错了位置,再用自己认为正确修复方式修复, 然后几轮修复之后,会越错越离谱,改得面目全非。这时我必须每一轮都提醒它,判断要以论文原文为正确标准,要确认论文在该具体场景下的行为。

即使我这样引导,它还是无法在每个地方都做对。无论是根据日志推导,判断出错是发生在哪一步开始,还是根据论文确认最早出问题的这一步的正确操作是什么, 只有中间有一个小环节没做对,放大到后期都会错得很远。

所以最终这个过程我只能自己来判断,我让 AI 分析完日志时间线之后,自己去核实时间线,纠正它第一步出错是发生在哪里,再确认论文中正确的做法是怎么做,这样才把问题最终一步步的修完了。 这个场景就是只能人类一步一步纠偏,很累。而且 AI 对论文的理解也很多时候有问题,人类还需要自己先理解,知道什么样是正确,才有能力去纠正偏。

老实说,我并不认为 AI 更强大之后,能处理这类的 case,如果人类放弃了思考,认知托管给 AI,会是挺灾难的。 这类场景对人的要求很高。普通人如果只是一直活在 AI 的舒适区,就会一直高呼 AI 牛逼。

AI 也有确实牛逼的地方。还是在这个 case 里面遇到的一个,原始论文中,给 witness 发消息是一个 handle witness message 函数, 这个函数是一个 CAS 操作,访问 WitnessStorage 记录下 term subterm 等字段信息,原论文说推荐用 s3 或者 ntfs 都可以。 我抽象成了一个 WitnessStorage 的接口,具体的实现可以用 s3 或者 etcd 或者随便其它的能支持 CAS 写的高可用存储。

这里有一个小细节是,访问 WitnessStorage 是跨网络的请求并且可能卡,原论文中 handle witness message 是一个同步操作,CAS 成功了就是成功了。 但是我的实际代码中必须拆成异步的,因为如果 handle witness message 访问 WitnessStorage 卡住,就把 raft 线程给卡住了,其它 raft 消息都处理不了。

然后 AI 就在这里揪出来一个 bug!同步转异步之后,返回的 CAS 成功了,不表示真的成功了。在发送请求到异步收到结果的过程中,有可能出现其它的 WitnessStorage 访问插入, 使得这个 CAS 成功的结果是过时的!确实 AI 有牛逼的时候,找出真实的 bug 而我没考虑到。

第二个 AI 没搞定的 case 是编译器自举。

也是前面我写过博客了,tinyactor 是我用 AI 写的一个玩具的 actor 模型的语言。字节码虚拟机,每个 actor 独立 GC,

最初我用 lisp 弄了一个内核出来,后面我要做语言的自举,其实是大部分参考 gleam 的语法,lisp 退化到内部 IR 或者说 AST 的地位,用户使用的时候还是会用更友好的语法,而实现是途经了 lisp 最终到 C 实现的虚拟机。 这边一开始都是用 C 弄出来,从 parser 到 compile 之类的。等上层越来越成熟之后,我就想,该是时候做 bootstrap 了。上层全部用 tinyactor 语言自身来写,包括 parse / compile / type check 这些,只保留 VM 和 runtime 去用 C 写。

结果我发现 bootstrap 这个场景让 AI 搞,它也是一直撞墙。

这个场景麻烦在什么地方?浮沙筑高台。底层的 runtime 其实不那么稳,都是 AI 写的底子。虽然给它很多 test case 然后暴力莽过去的,能跑,但不知道什么边边角角就跑出问题来。

举个例子,AI 写代码的时候,像 list length 这种,它用的非尾递归方式写的,在跑 repo 中单测一类的,它都跑得过去。但是一旦规模大了,非尾递归写法就爆栈了。 典型的例子就是,在 bootstrap 的时候,有些代码的 AST 就是比较大的,然后到一个临界点就爆栈。你说它错呢,这算法本身也没写错。关键是爆栈之后,我们的报错信息是没有为什么的。 这不像一个成熟编译器,有良好的错误信息,所以 AI 拿到的信息就是,做某一步,然后挂了,也没有提示是怎么挂的。所以这就比 AI 用 C 语言写东西有挑战得多。

像这类似的场景还很多,比如 actor 开始的初始化栈我只给了 512 字节,这样我启动 1M 个 actor 也就才 512M,比 Go 要省多了。但是全部对象都在这个空间里面分配,而 parser 的时候读文件那一步, 直接需要分配超过 512 字符串,就挂了。AI 改就直接把栈加倍加倍再加倍,就想绕过去一些问题。

也确实遇到了一些 GC 相关的 bug,AI 实现出来的 GC 并不稳定,把有些变量没有加到 GC Root 里面,导致 GC 那边释放了对象,然后访问时出错。AI 能查这一些问题,我还是挺佩服的。 但问题是什么?我们的目标其实是做自举,而 AI 在做的过程中,就遇到这一类的坑,然后去修 GC,做着做着,也不知道自己在做什么了,GC 也没彻底修好。 说到底还是那句话:浮沙筑高台。AI 前面打的基础太薄弱了,不像人打出来的基础。然后再在这个基础上往后推,就发现摇摇欲坠。

再然后,自举本身有很复杂的地方。步骤是很麻烦的,首先要用 C 实现一遍,C 实现的把 parser.ta codegen.ta typecheck.ta 几个文件都编译出来,然后 VM 再用这些编译结果,再去编译自己。

其实非常需要一个中间步骤,对比由 C 编译出来的 bytecode 文件,跟加载 bytecode 后再用 bytecode 里面的 compile 函数,编译出来的 bytecode 文件,是否一模一样。 但是 AI 在这一步做得很不好,它没有这个方法论。在这一步不做对,又是一个浮沙筑高台。

再有就是生成 bootstrap 文件的过程,AI 把 VM 搞破坏掉了。首先是我们用 C 编译出来的 parser / codegen 的 bytecode,它们被打包了变成 bootstrap 文件,VM 启动后加载。 跑测试我们用的这一份 bootstrap 文件去跑测试的,测试能过。 然后,我们再用这个 bootstrap 文件中的 compile 函数再次 compile,生成一份新的 bootstrap,这一步就有问题了,生成出来的 bootstrap 文件跟原始的 C 生成出来的那一个,并不一致。

AI 拿这个错的 bootstrap 去跑测试,发现有许多测试跑不过。它不知道错误是因为,它的 bootstrap 文件本身不对了,它已经把 bootstrap 搞坏了,所以跑出来的测试也不对。 但它就是会去分析测试本身,一直到死都找不出来咋挂的。

更气人的是什么?它用有问题 bootstrap 文件,覆盖掉了正确的 bootstrap 文件,在那之后,测试就再也跑不过了。 然后它说,“已经完成了。那些跑不过的测试,是原本就存在的!不属于要修改的范围”... 我它妈一口老血喷出来!就这情况,要是想让 AI 莽过去,怎么可能。

老实说,AI 能把 bytecode VM 能搞出来,也是挺惊艳的了。只是它的上限还不够人类这么强大。尤其是对于全局的理解能力和一些软件开发方法轮。 它没法淘汰所有的程序员,但是这个过程对程序员的效率还是提升了特别多的。在它不撞墙的时候,已经写了许多的代码了。 所以只要在 AI 特定的搞不定的点上面,回归一点点古法编程,人 + AI 还是可以兜得住。把 AI 当工具使用就好了。

AI