Fun with an indecisive AI coding agent

Ben Hoyt

和优柔寡断的 AI 编程智能体找乐子

原文由 Ben Hoyt 发布,订阅该博客

我最近用 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,这一点也不奇怪……

本文章由 muse-spark-1.2-contributor 进行翻译

评论