우유부단한 AI 코딩 에이전트와의 유쾌한 경험
원문은 Ben Hoyt님이 에 게재했습니다. 이 블로그 구독하기
최근 GoAWK의 까다로운 버그들을 고치는 데 AI 에이전트의 도움을 받고 있는데, 어느 정도 성과는 있었다. 하지만 가끔은 AI가 매우 우유부단하다는 느낌을 받는다(미첼 해시모토도 바로 며칠 전 트윗에서 이 점을 언급했다).
나는 AI에게 다음과 같은 질문을 던졌다(나는 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"을 출력합니다. 왜 그런가요?
AI는 몇 문단 만에, 내가 직접 파악하는 것보다 훨씬 빠르게 문제의 원인을 진단했다. NR, NF 같은 특수 변수들을 네이티브 Go int 타입으로 저장하고 있어 문자열 표현이 유실된다는 것이었다.
그리고는 수정 작업에 들어갔다. 몇 분 동안 나는 점점 더 재미있다는 생각이 들었는데, AI는 버그를 고치는 방법으로 7가지 서로 다른 옵션을 내놓고는 그 사이를 25번도 넘게 왔다 갔다 했다.
AI가 내놓은 옵션들은 다음과 같다:
- 옵션 A: 특수 변수에 대해 문자열 표현 유지
- 옵션 B: 특수 변수를 value 타입으로 저장
- 옵션 C: 특수 변수가 문자열로 설정될 때 문자열 오버라이드 저장
- 옵션 D: ForIn opcode만 따로 수정
- 옵션 E: 원본 값을 별도 필드에 저장
- 옵션 F: lineNum(NR)과 fileLineNum(FNR)만 value로 변경
- 옵션 G: value 타입의 special overrides 맵 추가
AI는 몇 초마다 “사실 가장 간단한 수정 방법은…”이라거나 “아니, 진짜 문제는…”라며 말을 바꿨다. 나는 결국 작업을 취소할 때까지 이 과정이 계속됐다.
그나마 다행인 점은 가장 자주 제안한 것이 옵션 B(26번 중 11번)였다는 것이고, 그게 거의 확실히 올바른 수정 방법이라는 것이다. 이 PR에서 해당 수정을 적용했다.
참고로 아래에 채팅 출력의 일부를 첨부한다. 다른 옵션을 제안하는 부분만 추려서 대폭 편집했으며, 이 글에서처럼 옵션 A와 같이 굵게 표시했다.
이 버그를 살펴보니, 문제는 ForIn opcode 핸들러에 있습니다…
잠깐, ForIn의 실제 구현을 좀 더 자세히 살펴보겠습니다…
이제 버그가 명확하게 보입니다…
이로써 버그가 확인됐습니다. 전체 상황은 이렇습니다: …
근본 원인은 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
좋습니다, 좀 더 신중하게 생각해 보겠습니다…
점점 복잡해지고 있습니다. 더 간단한 접근법을 써 보겠습니다. for-in의 대상이 될 가능성이 가장 높은 lineNum과 fileLineNum만 value 타입으로 바꾸는 겁니다… 옵션 F
잠깐, 사실 훨씬 더 간단한 방법이 있습니다…
좋습니다, 실용적인 접근법을 써 보겠습니다. 이 특정 버그에 대한 가장 깔끔하고 최소한의 수정 방법은 숫자형 특수 변수(lineNum, fileLineNum, matchLength, matchStart, argc, numFields)를 value 타입으로 저장하도록 바꾸는 것입니다… 옵션 B
잠깐, numFields는 내부에서 int로 많이 사용됩니다. 다른 접근법을 시도해 보겠습니다…
사실 가장 간단하면서도 올바른 접근법은 이렇습니다. 각 숫자형 특수 변수마다 int(내부용)와 value(AWK 레벨 표현용)를 모두 저장하는 것입니다… 옵션 E
하지만 이것도 여전히 변경할 부분이 많습니다. 더 간단하게 생각해 보겠습니다…
특정 케이스를 처리하는 가장 간단하면서도 올바른 수정 방법은 다음과 같습니다. setSpecial에서 숫자형 특수 변수에 대해, 값이 문자열이라면(의미 있는 숫자로 변환할 수 없거나 typeStr인 경우) value를 직접 저장해 문자열을 보존하는 것입니다… 옵션 B
좋습니다, 가장 깔끔하고 최소한의 수정 방법은 interp 구조체에 specialOverrides map[int]value 필드를 추가하는 것이라고 생각합니다… 옵션 G
잠깐, 이건 너무 복잡하게 만드는 겁니다. 특정 케이스를 다시 살펴보겠습니다…
그냥 제대로 된 수정을 진행하겠습니다. 숫자형 특수 변수들을 int 대신 value 타입으로 저장하도록 바꾸겠습니다… 옵션 B
다른, 훨씬 더 간단한 접근법을 써 보겠습니다. specialValues 맵을 추가해서 setSpecial과 getSpecial이 값을 제대로 처리하도록 만들겠습니다… 옵션 G
사실 내부를 건드리지 않고 할 수 있는 가장 간단한 방법은 ForIn 핸들러만 바꾸는 것입니다… 하지만 문제는 getSpecial이 항상 int로부터 값을 다시 만들어낸다는 점입니다…
좋습니다, 그냥 제대로 된 수정을 하겠습니다. “special values override” 맵 방식을 사용하겠습니다… 옵션 G
좋습니다, 사실 접근법을 다시 고려해 보겠습니다… 이에 맞는 가장 깔끔한 방법은 숫자형 특수 변수들을 int에서 value로 바꾸는 것입니다… 옵션 B
잠깐, 사실 좀 더 실용적으로 가 보겠습니다. 내부용으로는 int 필드를 그대로 유지하되, AWK 레벨 값을 추적하는 병렬 value 필드를 추가하는 방법입니다… 옵션 E
아닙니다, 이건 이중으로 장부를 관리하는 셈입니다. 그냥 과감하게 관련 필드들을 value 타입으로 바꾸겠습니다… 옵션 B
음, 사실 한 걸음 물러서서 가장 간단하면서도 올바른 수정 방법이 무엇인지 생각해 보겠습니다. 가장 최소한의 수정 방법은 for-in 케이스만 처리하는 것입니다… 옵션 D
아닙니다, 문제는 setSpecial이 값을 int로 잘라내서 문자열 값이 어차피 유실된다는 점입니다. 좋습니다, 계획은 이렇습니다. for-in 루프의 대상이 될 가능성이 가장 높은 lineNum과 fileLineNum(NR과 FNR)만 int에서 value로 바꾸겠습니다… 옵션 F
사실 그냥 전부 제대로 처리하겠습니다. 계획은 다음과 같습니다… 옵션 B
for-in에서 사용될 가능성이 가장 높은 변수인 lineNum과 fileLineNum(NR과 FNR)에만 집중하겠습니다. 옵션 F
사실 사용자의 버그 리포트는 구체적으로 NR에 관한 것입니다. 모든 숫자형 특수 변수를 고치되, 많이 사용되는 것들에 대해서는 더 간단한 접근법을 쓰겠습니다. 옵션 B (라고 할 수 있습니다)
사실 그냥 모든 케이스를 고칠 수 있는 가장 간단한 방법을 쓰겠습니다. 사용자가 설정한 특수 변수 오버라이드를 저장하기 위해 map[int]value 필드를 추가하는 겁니다… 옵션 G
재밌네요! 이미 specialOverrides 필드와 이를 삭제하는 호출이 있습니다. 이것들은 제가 수정하기 전부터 존재하던 것들입니다…
이쯤 되자 AI는 완전히 혼란에 빠지기 시작했다. 코드를 편집해 specialOverrides 맵을 추가해 놓고는, 정작 자신이 그렇게 했다는 사실을 잊어버린 것이다. 이 시점에서 나는 작업을 취소했다.
“사실(Actually)”이라는 말이 19번이나 나왔다. 정말 놀라운 기계들이지만, 이렇게 우유부단한 친구가 있다면 사귀고 싶지는 않을 것 같다!
더 일반적으로 말하자면, 대규모 언어 모델과 에이전트가 언제 멈춰야 하는지를 어떻게 아는지가 궁금하다. 토큰 예측기가 계속해서 토큰을 예측한다는 사실 자체는 그리 놀랍지 않다…
글을 무작위로 읽기
댓글
로그인하고 댓글 남기기