Fun with an indecisive AI coding agent

Ben Hoyt

優柔不断なAIコーディングエージェントとの戯れ

原文は Ben Hoyt により に公開されました。 このブログを購読する

GoAWKのそこそこ厄介なバグ修正にAIエージェントを使ってある程度成果を上げている。GoAWKでのことだ。ただ、こいつは時々ひどく優柔不断になる(Mitchell Hashimotoがツイートで指摘していたのはつい先日のことだ)。

次のような質問を投げてみた(使っているのはGoLand IDE上のGitHub Copilotプラグイン経由のClaude Opus 4.6だ):

GoAWKがAwkプログラムBEGIN { a["x"]=1; for (NR in a) print NR, a[NR] }に対して"x 1\n"ではなく"0\n0\n"を出力するのはなぜですか?

エージェントは数段落のうちに、自分で考えるよりも速く問題を診断してみせた。これらの特殊変数をネイティブなGoのintとして保存しているため、文字列表現が失われてしまうというのだ。

そして修正に取りかかった。数分間のあいだに、こちらがどんどん面白がっていくのをよそに、バグの修正方法として7種類もの異なる案を出し、その間を少なくとも25回も行ったり来たりした。

エージェントが出してきた案は次のとおりだ:

  • Option A: 特殊変数の文字列表現を保持する
  • Option B: 特殊変数をvalue型で保存する
  • Option C: 特殊変数が文字列に設定されたときにstringオーバーライドを保存する
  • Option D: ForInオペコードだけを修正する
  • Option E: 元の値を別フィールドに保存する
  • Option F: lineNum(NR)とfileLineNum(FNR)だけをvalueに変更する
  • Option G: value型のspecial overridesマップを追加する

エージェントは数秒ごとに「Actually, the simplest fix……」や「But no, the real issue is……」と言い出した。延々とそれが続いたので、最後は自分がキャンセルした。

一応言っておくと、最も多く提案されたのはOption Bで(26回中11回)、これがほぼ間違いなく正しい修正だ。このPRで対応した

参考までに、以下にチャット出力の抜粋を載せておく。別の案を提案している箇所だけが分かるように大幅に省略してあり、例えばOption Aのように太字でマークしてある。


このバグを見ると、問題はForInオペコードのハンドラにあります……

でもちょっと待って、実際のForInの実装をもっとよく見てみよう……

これでバグがはっきり見えてきた……

これでバグが裏付けられた。全体像はこうだ……

根本的な原因は、NR、NF、FNRなどの特殊変数がネイティブなGoのint型で保存されており、本質的に文字列表現が失われてしまうことだ……

修正はsetSpecialとgetSpecialで行う必要がある――これらの数値系の特殊変数について文字列表現を保持しなければならない…… Option A

ただ、よりシンプルで的を絞ったアプローチもある。問題はあくまで特殊変数を使ったfor-inに関するものだ。とはいえ、NR = “x”のような代入でも同じ問題が起きるはずで、文字列は失われてしまう。最善の修正を考えてみよう。

最もクリーンな修正は、これらの特殊変数をvalue型で保存することだ……ただ、これは大規模なリファクタリングになる。 Option B

より外科的な修正としては、数値系の特殊変数が文字列経由で設定されたときに「stringオーバーライド」を保存する方法もある。ただ、これはハックっぽい。 Option C

いや、別の角度から考えてみよう……

正しい修正は、値をvalue型で保存しておき、内部で必要になったときだけintを取り出すことだ…… Option B

いや、現在のアーキテクチャを保ったままの最もシンプルな修正は、for-inループに限ってアプローチを変えることだ。 Option D

いや、違う、本当の問題はもっと一般的だ――数値系の特殊変数への文字列代入はどれも文字列を保持すべきなのだ。

最小限かつ正しい修正を考えてみよう。最も的を絞ったアプローチは、数値系の特殊変数の保存をintからvalueに変え、読み書きしている数箇所を更新することだ…… Option B

これはやりたいリファクタリングより大掛かりだ。もっとシンプルな方法を考えてみよう……

いや、最も現実的で最小限の修正は、ForInのレベルで対処することだ…… Option D

いや、やはり正しい修正は、これらの数値系の特殊変数をvalue型で保存するように変更することだ…… Option B

これらすべてを変更するのはリスキーだ。さらにシンプルなアプローチを考えてみよう。

いや、最もシンプルで正しい修正はこうだ。setSpecialで、数値系の特殊変数については元の値を別フィールドに保存しておき、getSpecialが一致したときにそれを返せるようにする…… Option E

よし、もう少し慎重に考えてみよう……

複雑になってきた。もっとシンプルに行こう。lineNumとfileLineNumだけをvalue型に変更しよう。for-inの対象になる可能性が最も高いのはこの2つだからだ…… Option F

待って、実はもっとずっとシンプルなアプローチがある……

よし、現実的なアプローチを取ろう。この特定のバグに対する最もクリーンで最小限の修正は、数値系の特殊変数(lineNum、fileLineNum、matchLength、matchStart、argc、numFields)をvalue型で保存するように変更することだ…… Option B

いや待って、numFieldsは内部でintとして多用されている。別のアプローチを試してみよう……

いや、最もシンプルで正しいアプローチはこうだ。数値系の特殊変数ごとに、int(内部利用用)とvalue(AWKレベルの表現用)の両方を保存するのだ…… Option E

でもこれでも変更は多い。もっとシンプルに考えてみよう……

この特定のケースを扱う最もシンプルで正しい修正は、setSpecialで数値系の特殊変数について、値が文字列(意味のある数値に変換できない、あるいはtypeStrである)の場合に、valueを直接保存して文字列を保持することだ…… Option B

よし、最もクリーンで最小限の修正はこうだと思う。interp構造体にspecialOverrides map[int]valueフィールドを追加する…… Option G

いや待って、これは複雑にしすぎだ。特定のケースを再検討してみよう……

とにかく正しい修正を進めよう。数値系の特殊変数をintではなくvalue型で保存するように変更する…… Option B

別の、もっとシンプルなアプローチを取ろう。specialValuesマップを追加して、setSpecialとgetSpecialが値を適切に扱えるようにするだけだ…… Option G

いや、内部に手を入れずに済む最もシンプルなアプローチは、ForInハンドラだけを変更することだ……でも問題は、getSpecialが常にintから再構築してしまうことだ……

よし、とにかく正しい修正をしよう。「special values override」マップのアプローチを使う…… Option G

いや、ちょっとアプローチを考え直そう……これに合う最もクリーンなアプローチは、数値系の特殊変数をintからvalueに変更することだ…… Option B

待って、いや、もっと現実的になろう。アプローチはこうだ。内部利用のためにintフィールドは残しつつ、AWKレベルの値を追跡する並列のvalueフィールドを追加する…… Option E

いや、違う、これは二重管理になってしまう。腹をくくって、該当するフィールドをvalue型に変更しよう…… Option B

うーん、いや、一歩引いて、最もシンプルで正しい修正は何かを考えてみよう。最も最小限の修正は、for-inのケースだけを扱うことだ…… Option D

いや待って、問題はsetSpecialがintに切り詰めてしまうことで、どのみち文字列の値は失われる。よし、こうしよう。lineNumとfileLineNum(for-inループの対象になる可能性が最も高いNRとFNR)だけをintからvalueに変更する…… Option F

いや、やっぱり全部をちゃんとやろう。プランはこうだ…… Option B

やはりlineNumとfileLineNumに絞ろう。これらはfor-in(NRやFNR)で使われる可能性が最も高い変数だ。 Option F

いや、ユーザーのバグ報告は具体的にはNRに関するものだ。すべての数値系の特殊変数を修正しつつ、使用頻度の高いものについてはよりシンプルなアプローチを取ろう。 Option B (more or less)

いや、こうしよう、すべてのケースを修正する最もシンプルなアプローチを取る。ユーザーが設定した特殊変数のオーバーライドを保存するためにmap[int]valueフィールドを追加するのだ…… Option G

おや、面白い!すでにspecialOverridesフィールドと、それに対するdelete呼び出しがあるじゃないか。これらは僕が変更する前から存在していた……


ここからエージェントはひどく混乱し始めた。コードを編集してspecialOverridesマップを追加しておきながら、自分でやったことを忘れてしまったのだ。この時点で僕はキャンセルした。

「Actually」が19回も出てきている。すごい機械ではあるが、こんなに優柔不断な友達は御免こうむりたい!

より一般的には、大規模言語モデルやエージェントがいつ止まるべきかをどうやって知るのか、興味がある。トークン予測器がトークンを予測し続けるのは、別に驚くことではないのだが……

この記事は「muse-spark-1.2-contributor」を使用して翻訳されました。

コメント