和优柔寡断的 AI 编程智能体找乐子
我最近用 AI 智能体帮我修复 GoAWK 里一些不太好修的 bug,取得了一定的成效。但它有时候非常优柔寡断(Mitchell Hashimoto 就在前几天在一条推文中提到过这一点)。
我向它提了这样一个问题(我用的是 Claude Opus 4.6,通过 GoLand IDE 里的 GitHub Copilot 插件调用):
GoAWK 对于 Awk 程序
BEGIN { a["x"]=1; for (NR in a) print NR, a[NR] }打印出的是"0\n0\n",而不是"x 1\n"。为什么?
它在几段话内就诊断出了问题,比我自己还快。它指出,我把这些特殊变量存成了 Go 原生的 int,导致丢失了原有的字符串形式。
然后它就开始尝试修复。在几分钟的时间里,我越看越觉得好笑——它一共给出了 7 种不同的修复方案,并在它们之间来回摇摆了不下 25 次。
它给出的方案有:
- 方案 A:为特殊变量保留字符串形式
- 方案 B:将特殊变量存为 value 类型
- 方案 C:当特殊变量被设为字符串时,存储字符串覆盖值
- 方案 D:专门修复 ForIn 操作码
- 方案 E:将原始值存在侧边字段中
- 方案 F:只把 lineNum(NR)和 fileLineNum(FNR)改成 value
- 方案 G:新增一个 value 类型的特殊变量覆盖映射
每隔几秒,它就会来一句“其实,最简单的修复方式是……”或者“不对,真正的问题是……”。就这样没完没了,直到我把它叫停。
公平地说,它提出最多次的是方案 B(26 次里有 11 次),而这也几乎可以肯定是正确的修复方式。我已经在这个 PR 里实现了。
感兴趣的话,我在下面贴出了聊天记录的精简版,只保留了它每次提出新方案时的片段。我用加粗做了标记,就像这样:方案 A。
看这个 bug,问题出在 ForIn 操作码的处理逻辑里……
等等,让我再仔细看看 ForIn 的实际实现……
现在我能清楚地看到这个 bug 了……
这就确认了这个 bug。完整的情况是这样的:……
根本原因在于,像 NR、NF、FNR 等特殊变量是以 Go 原生的 int 类型存储的,天生就会丢失字符串形式。……
修复需要在 setSpecial 和 getSpecial 里做——我们需要为这些数值型的特殊变量保留字符串形式。…… 方案 A
不过,还有个更简单、更有针对性的思路:这个问题具体是关于特殊变量的 for-in 循环。但实际上,任何像 NR = “x” 这样的赋值都会出现同样的问题——字符串会丢失。让我想想最好的修复办法。
最干净的修复方式是把这些特殊变量存为 value 类型……但那改动就大了。 方案 B
一个更精细的修复:当数值型特殊变量通过字符串赋值时,存一个“字符串覆盖值”。不过这有点投机取巧。 方案 C
其实,换个角度想想。……
正确的修复方式是把值存为 value 类型,只在内部需要时再提取出 int。…… 方案 B
其实,在保留现有架构的前提下最简单的修复方式是:专门针对 for-in 循环,我们可以换个做法。 方案 D
但不对,真正的问题更具普遍性——任何对数值型特殊变量的字符串赋值都应该保留字符串。
让我想一个最小但正确的修复。最有针对性的做法:把数值型特殊变量的存储从 int 改成 value,并更新少数几个读写它们的地方。…… 方案 B
这比我想做的重构要大。让我想个更简单的方法。……
其实,最务实、最精简的修复是在 ForIn 层面处理。…… 方案 D
其实,我觉得正确的修复就应该是把这些数值型特殊变量改成以 value 类型存储。…… 方案 B
把这些全改了风险很大。让我想个更简单的办法。
其实,最简单的正确修复是:在 setSpecial 里,对于数值型特殊变量,把原始值存在一个侧边字段里,这样 getSpecial 在匹配时就能返回它。…… 方案 E
好,让我再仔细想想。……
这变得有点复杂了。让我换个更简单的方法:只把 lineNum 和 fileLineNum 改成 value 类型,因为它们最有可能成为 for-in 的目标。…… 方案 F
等等,其实还有个简单得多的方法。……
好,我来采取务实的做法。针对这个特定 bug,最干净、最精简的修复是:把数值型特殊变量(lineNum、fileLineNum、matchLength、matchStart、argc、numFields)改成以 value 类型存储。…… 方案 B
等等,numFields 在内部大量被当作 int 使用。让我试试另一种方法。……
其实,最简单的正确做法是:对于每个数值型特殊变量,同时存储 int(供内部使用)和 value(供 AWK 层面表示)。…… 方案 E
但那改动还是很多。让我想个更简单的……
能处理这个特定情况的最简单的正确修复是:在 setSpecial 里,对于数值型特殊变量,如果值是字符串(无法转换为有意义的数字,或类型是 typeStr),就直接存一个 value 来保留字符串。…… 方案 B
好,我觉得最干净、最精简的修复是:给 interp 结构体加一个 specialOverrides map[int]value 字段。…… 方案 G
等等——这把事情搞复杂了。让我重新审视一下这个具体情况。……
还是直接做正确的修复吧。我会把数值型特殊变量改成以 value 类型存储,而不是 int。…… 方案 B
让我换个完全不同、简单得多的方法。我只要通过加一个 specialValues 映射,让 setSpecial 和 getSpecial 能正确处理 value 就行…… 方案 G
其实,不用动内部逻辑的最简单方法是:只改 ForIn 的处理逻辑。……但问题是 getSpecial 总是从 int 重新构造……
好,我就直接做正确的修复。我会用“特殊值覆盖”映射的方法:…… 方案 G
好吧,其实让我再重新考虑一下这个方案。……最干净、也最符合这个思路的做法是:我直接把数值型特殊变量从 int 改成 value。…… 方案 B
等等,其实让我再务实一点。这个方案是:保留 int 字段供内部使用,但额外加一个平行的 value 字段来跟踪 AWK 层面的值。…… 方案 E
其实不行,那样就是双重记账了。还是干脆把相关字段改成 value 类型吧。…… 方案 B
嗯,其实让我退一步,想想什么是最简单且正确的修复。最精简的修复:只处理 for-in 的情况。…… 方案 D
不对,问题是 setSpecial 会截断为 int,所以无论如何字符串值都会丢失。好,我的计划是这样的。只把 lineNum 和 fileLineNum(最有可能用在 for-in 循环里的——也就是 NR 和 FNR)从 int 改成 value。…… 方案 F
其实,我还是把它们全部正确地改掉吧。我的计划是:…… 方案 B
还是聚焦在 lineNum 和 fileLineNum 上吧,它们是最有可能在 for-in 中被用到的变量(NR 和 FNR)。 方案 F
其实,用户报告的 bug 明确是关于 NR 的。我来把所有数值型特殊变量都修了,但对那些重度使用的变量采取更简单的方式。 方案 B(大致如此)
其实,要不这样吧,干脆采取能修复所有情况的最简单做法:加一个 map[int]value 字段来存储用户设置的特殊变量覆盖值。…… 方案 G
有意思!已经有一个 specialOverrides 字段和对它的 delete 调用了。这些在我改动之前就存在了。……
到这时它已经开始非常混乱了。它已经改了代码,加了一个 specialOverrides 映射,却又忘了自己已经加过。到这个时候,我就直接把它叫停了。
里面出现了 19 个“其实”。这些机器确实令人惊叹,但我可不想有个这么优柔寡断的朋友!
更广义地说,我很想知道大语言模型和智能体是怎么知道该何时停下来的。对我来说,token 预测器会不断地预测下一个 token,这一点也不奇怪……
随机一篇博客
评论
登录后参与讨论