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 這個 opcode
  • 選項 E:在側邊欄位儲存原始值
  • 選項 F:只把 lineNum(NR)和 fileLineNum(FNR)改成 value
  • 選項 G:新增一個 value 型別的特殊覆寫 map

它每隔幾秒就會說「其實,最簡單的修法是……」或「但不對,真正的問題是……」。就這樣沒完沒了,直到我把它取消掉。

公平來說,它最常提的是選項 B(26 次裡有 11 次),而那也幾乎可以確定才是正確的解法。我已經在這個 PR 中實作了。

為了讓大家參考,我在下方附上對話紀錄的節錄版本。內容經過大幅刪減,只保留它提出另一種選項的時刻。我用粗體標示出來,像這樣:選項 A


看這個 bug,問題出在 ForIn opcode 的處理器裡……

但等一下,讓我更仔細看看實際的 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 map,讓 setSpecial 和 getSpecial 能正確處理 value 就好…… 選項 G

其實,不需要動到內部邏輯的最簡單做法是:只要改 ForIn 的處理器就好。……但問題是 getSpecial 總是從 int 重建……

好,讓我直接做正確的修法。我會採用「特殊值覆寫」map 的做法:…… 選項 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 map,卻又忘了自己做過這件事。到這個時候,我就直接把它取消掉了。

整段對話裡出現了 19 次「其實」。這些機器真的很厲害,但我可不希望有個這麼優柔寡斷的朋友!

更廣泛來說,我很想知道大型語言模型和代理究竟是怎麼知道該何時停下來的。對我而言,token 預測器會一直預測下一個 token,一點也不讓人意外……

本文章由 muse-spark-1.2-contributor 進行翻譯

留言