오작동하는 AI 코딩 에이전트를 디버깅하는 방법: 나의 4단계 플레이북
요약
AI 코딩 에이전트의 오작동을 해결하기 위한 4단계 디버깅 플레이북을 소개합니다. 에이전트의 실패는 증상이 나타나는 시점이 아닌, 모델의 가정이 현실과 어긋난 이전 단계에서 발생한다는 점에 주목하여 체계적인 디버깅 방법을 제시합니다.
핵심 포인트
- 에이전트의 버그는 증상이 나타나기 몇 단계 전의 '발산 지점'에서 발생함
- 최소 단위의 프롬프트로 오류를 재현하여 디버깅 범위를 압축해야 함
- 트랜스크립트를 분석하여 모델의 가정과 실제 데이터의 차이를 비교함
- 단순히 프롬프트에 규칙을 추가하는 미신적 방식에서 벗어나 논리적 수정 필요
요약 (TL;DR)
AI 코딩 에이전트는 일반적인 소프트웨어처럼 실패하지 않습니다. 이들은 확신에 차서 실패하며, 버그는 보통 피해가 나타나는 지점보다 세 단계(turns) 앞선 곳에 위치합니다. 자율적인 Claude Code 설정을 수개월 동안 운영한 끝에, 저는 4단계 디버그 플레이북을 정립했습니다: 최소 단위로 재현하기 → 트랜스크립트(transcript)에서 발산이 일어난 턴(turn) 찾기 → 모델의 가정과 실제 데이터의 차이(diff) 비교하기 → 적절한 계층에서 수정하기. 이 포스트에서는 제 에이전트가 JSON 에러 응답을 읽고 그것을 성공으로 판단해 버렸던 실제 사례를 통해 이 플레이북을 설명합니다. 🐛
문제 (The Problem)
일반적인 프로그램이 고장 나면 스택 트레이스(stack trace)가 나타납니다. 이는 특정 줄(line)을 가리키고, 당신은 그 줄을 수정하면 됩니다.
AI 에이전트가 고장 나면... 완료된 작업이 나타납니다. 쾌활한 요약 보고서가 옵니다. 어쩌면 테스트 결과가 초록색(pass)으로 뜰 수도 있습니다. 그러다 이틀 뒤에야 에이전트가 존재하지도 않는 설정 로더(config loader)를 "리팩터링(refactored)"했거나, 내내 403 에러를 반환하고 있던 API 호출을 기반으로 기능을 구축했다는 사실을 발견하게 됩니다.
저는 Claude Code(2026년 중반 빌드, Node.js 22.x 기반)로 구축된 완전 자율 구현 시스템을 운영하고 있습니다. 이 시스템은 작업을 계획하고, 코드를 편집하고, 테스트를 실행하고, 보고를 수행하며, 대부분의 과정이 사람의 개입 없이 진행됩니다. 즉, 무언가 잘못되었을 때 그 과정을 지켜본 인간(human in the loop)이 없다는 뜻입니다. 저에게 남은 것은 사후 결과와 트랜스크립트(transcript)뿐입니다.
초기에 저의 "디버깅"은 다음과 같았습니다:
- 이상한 출력을 발견한다
- 전체 작업을 다시 실행하며 뚫어지게 쳐다본다
- 프롬프트에
절대로 나쁜 짓을 하지 마라와 같이 대문자로 된 규칙을 추가한다 - 다음 주 화요일에 녀석이 똑같은 나쁜 짓을 다시 하는 것을 지켜본다
이것은 디버깅이 아닙니다. 그것은 미신입니다. ⚠️
전환점은 에이전트의 실패가 증상이 나타나는 지점에서 발생하는 경우가 거의 없다는 사실을 깨달은 것이었습니다. 증상은 최종 출력에 나타나지만, 원인은 모델의 내부 세계관이 현실과 일치하지 않게 된 특정 이전 턴(turn)에 있습니다. 그 턴 이후의 모든 과정은 모델이 잘못된 사실을 바탕으로 완벽하게 논리적으로 행동하는 과정일 뿐입니다.
따라서 과업은 "왜 출력이 나쁜가?"가 아닙니다. "모델의 믿음과 현실이 정확히 어느 턴에서 갈라졌는가?"입니다. 이러한 관점의 전환이 저에게 반복 가능한 프로세스를 제공했습니다.
내가 해결한 방법: 4단계 플레이북
flowchart TD
A[증상: 잘못된 출력] --> B[1단계: 최소한의 프롬프트로 재현]
B --> C[2단계: 트랜스크립트(Transcript)를 따라가며 분기점(Divergence turn) 찾기]
...
1단계: 최소한의 프롬프트로 재현하기
전체 작업을 다시 실행하고 싶은 충동을 억제하세요. 40턴(turn)에 달하는 세션은 건초더미와 같습니다. 대신, 오작동을 여전히 유발하는 가장 작은 단위의 프롬프트를 추출하세요.
제 에이전트가 데이터베이스 마이그레이션 (database migration)을 망쳤을 때, 전체 작업은 "새로운 결제 필드를 엔드 투 엔드(end to end)로 구현하라"였습니다. 최소한의 재현 (minimal repro) 결과는 다음과 같았습니다:
Read migrations/ and tell me the current schema version.
그 단 하나의 지시문이 버그를 재현했습니다. 에이전트가 존재하지 않는 스키마 버전을 보고한 것입니다. 40턴의 과정이 단 한 번으로 압축되었습니다. 이제 저는 몇 분이 아닌 몇 초 만에 반복 (iterate)할 수 있는 무언가를 갖게 되었고, 이 실패가 결제 로직과는 전혀 상관이 없다는 사실을 알게 되었습니다.
경험 법칙: 만약 재현 (repro)에 3턴 이상이 걸린다면, 계속해서 더 줄이세요. 버그는 예상보다 훨씬 더 자주 최소화 과정에서도 살아남습니다. 왜냐하면 버그는 대개 당신이 걱정하던 복잡한 추론 (reasoning)에 있는 것이 아니라, 인지 (perception) 과정(파일 읽기, 도구 출력 파싱 등)에 있기 때문입니다.
2단계: 트랜스크립트(Transcript)를 따라가며 분기점(Divergence turn) 찾기
Claude Code는 전체 세션 트랜스크립트(디스크 상의 JSONL 파일 — ~/.claude/projects/ 디렉토리를 확인하세요)를 보관합니다. 이것이 바로 비행 기록 장치 (flight recorder)입니다. 대부분의 사람들은 이를 절대 열어보지 않습니다. 열어보세요.
저는 한 가지 질문을 염두에 두고 트랜스크립트를 읽습니다: "모델이 각 도구 호출 (tool call) 직후에 무엇이라고 주장했는가?" 당신이 찾아야 할 것은 모델이 요약한 도구 결과가 바로 위에 있는 원본 결과 (raw result)와 일치하지 않는 첫 번째 턴입니다.
스크롤을 대폭 줄여주는 팁 하나를 드리자면, 도구 결과와 그 뒤에 이어지는 어시스턴트 (assistant) 텍스트만 따로 추출하는 것입니다:
jq -r 'select(.type == "tool_result" or .type == "assistant")
| .content // .text' session.jsonl | less
두 화자 사이의 diff(차이점)를 보듯 위에서 아래로 읽어보세요. 도구는 하나의 이야기를 하고, 모델은 또 다른 이야기를 합니다. 두 이야기가 일치하지 않는 첫 번째 지점이 바로 divergence turn(발산 지점)입니다. 그 이후의 모든 과정은 독수리과(fruit of the poisoned tree)의 결과물일 뿐입니다. 분석하는 데 시간을 낭비하지 마세요.
Step 3: 도구가 반환한 값과 모델이 가정한 값의 차이(Diff) 비교하기
정말 바보 같으면서도 아주 교훈적인, 제가 가장 좋아하는 실전 사례를 하나 들려드리겠습니다.
제 에이전트는 내부 서비스에 webhook(웹훅)을 등록해야 했습니다. 서비스는 다음과 같이 응답했습니다:
{
"ok": false,
"error": "duplicate_endpoint",
...
HTTP 상태 코드: 200. 당연하게도 서비스는 에러를 200 상태 코드로 반환했습니다. 🙃
에이전트의 바로 다음 메시지: "Webhook(웹훅)이 성공적으로 등록되었습니다. 알림 핸들러(notification handler)로 넘어갑니다."
그 후 에이전트는 null인 webhook id를 기반으로 알림 핸들러를 만드는 데 6번의 turn(턴)을 소비했습니다. 여기에는 전체 코드 경로를 조용히 건너뛰게 만드는 if (webhookId) 가드(guard) 구문 작성까지 포함되었습니다. 참고로 테스트는 모두 통과(Green tests)했습니다. 가드 구문 덕분에 아무것도 실행되지 않았고, 실행되지 않는 것은 실패할 수도 없었기 때문입니다.
발산 지점(divergence turn)에서 diff(차이점)는 잔인할 정도로 명확했습니다:
- 도구의 현실 (Tool reality):
"ok": false,"id": null - 모델의 가정 (Model assumption): 등록 성공, ID 존재
왜 이런 일이 발생했을까요? 모델은 200 + JSON 본문 + 메시지 필드의 "registered"라는 단어를 보고 패턴 매칭(pattern-matched)을 수행했습니다. 즉, 훑어 읽기(Skim-reading)를 한 것입니다. 솔직히 말해서, 새벽 2시에 지친 인간이 저지르는 것과 똑같은 실패 유형입니다.
이 단계는 실패를 분류하는 단계입니다. 제 경험상 거의 모든 에이전트 버그는 다음 세 가지 유형 중 하나입니다:
- 모호성 버그 (Ambiguity bug) — 지침이 진정으로 두 가지 해석을 모두 지원했고, 모델이 그중 다른 하나를 선택한 경우
- 검증 버그 (Verification bug) — 정보가 사용 가능했음에도 모델이 확인하지 않은 경우 (제 웹훅 사례)
- 도구 계약 버그 (Tool contract bug) — 도구의 출력이 오독을 적극적으로 유도하는 경우 (제 웹훅 사례이기도 합니다. 에러를 포함한 200 응답 API는 인간과 모델 모두에게 함정입니다)
Step 4: 적절한 계층에서 수정하기
이 단계는 과거의 저를 포함하여 모두가 실수하는 단계입니다. 본능적으로는 항상 프롬프트 규칙(prompt rules)으로 해결하려 합니다: "항상 ok 필드를 확인하세요!" 하지만 각 실패 유형에는 적절한 계층(layer)이 있습니다:
모호성 버그 (Ambiguity bugs) → 지시사항을 수정하세요. CLAUDE.md 또는 작업 프롬프트(task prompt)에 있는 모호한 문장을 다시 작성하세요. 정확한 문장 하나가 다섯 개의 경고 문단보다 낫습니다. 만약 대문자로 규칙을 쓰고 있다면, 근본적인 문장이 여전히 모호할 가능성이 높습니다.
검증 버그 (Verification bugs) → 프롬프트 규칙이 아닌 결정론적 체크(deterministic check)를 추가하세요. 프롬프트 규칙은 확률적(probabilistic)입니다. 모델은 이를 '대체로' 따릅니다. "대체로"로는 충분하지 않은 모든 상황에 대해서는, 체크 로직을 모델 외부로 빼야 합니다. 저는 에이전트가 등록 호출(registration calls) 후에 반드시 실행해야 하는 아주 작은 응답 검증기(response validator)를 추가했습니다:
# validate-response.sh — 에이전트가 그냥 지나치지 못하도록 명확하게 실패를 알림
ok=$(jq -r '.ok' response.json)
if [ "$ok" != "true" ]; then
...
0이 아닌 종료 코드(non-zero exit code)는 오독하는 것이 불가능합니다. 모델은 JSON 블록 안의 문장은 무시할 수 있지만, 실패한 명령은 무시할 수 없습니다.
도구 계약 버그 (Tool contract bugs) → 도구를 수정하세요. 만약 도구가 에러를 200 응답으로 반환하거나, 중요한 10줄 대신 4,000줄을 쏟아낸다면, 그 어떤 프롬프팅으로도 이를 영구적으로 해결할 수 없습니다. 저는 문제가 되는 서비스 클라이언트(service client)를 래핑(wrap)하여 에러가 실제 에러로 나타나도록 만들었습니다. 그 버그는 에이전트에게도, 저에게도 다시는 나타나지 않았습니다.
그런 다음, 수정 사항을 확인하기 위해 Step 1의 최소 재현(minimal repro)을 다시 실행하세요. 재현 과정이 단 한 번의 턴(turn)으로 이루어지기 때문에, 이 작업은 몇 초밖에 걸리지 않습니다. 만약 최소화(minimization) 과정을 건너뛰었다면, 단 한 줄의 수정을 테스트하기 위해 40턴짜리 세션을 다시 실행해야 할 것이며, 극심한 지루함 때문에 검증을 중단하게 될 것입니다. 최소화는 이 전체 루프를 지속 가능하게 만드는 핵심입니다.
교훈 (Lessons Learned)
-
증상은 하류(downstream)에서 나타납니다; 상류(upstream)를 디버깅하세요. 최종적인 잘못된 출력은 버그가 존재하는 지점인 경우가 거의 없습니다. 발산(divergence)이 일어난 턴(turn)을 찾아내고, 그 이후의 모든 것은 무시하세요.
-
에이전트는 추론(reasoning)보다 인지(perception) 단계에서 더 많이 실패합니다. 저는 잘못된 로직을 디버깅할 것이라 예상했습니다. 하지만 실제로 제가 반복해서 디버깅한 것은 잘못된 '읽기'였습니다: 훑어본 도구 출력(tool output), 잘못 파싱된 에러(misparsed errors), 가정된 파일 내용 등입니다. 인지 실패(perception failures)에 최적화되도록 디버깅 방식(및 도구)을 개선하세요.
-
프롬프트 규칙은 모호함을 위한 것이며, 종료 코드(exit codes)는 안전을 위한 것입니다. 만약 특정 실패가 진정으로 다시 발생해서는 안 된다면, 해결책은 결정론적(deterministic)이어야 합니다 — 즉, 검증기(validator), 훅(hook), 또는 래퍼(wrapper)를 사용하는 것입니다. 엄격한 체크가 필요한 곳에 프롬프트 규칙을 추가하는 것은 동일한 버그를 두 번 마주하게 되는 가장 빠른 방법입니다.
-
에러를 포함한 200 OK API는 결국 에이전트를 속이게 될 것이며, 이는 확실합니다. 대충 훑어보는 인간을 속이는 것이라면 모델도 속입니다. 도구 계약(tool contracts)을 수정하는 것은 에이전트 디버깅에서 가장 영향력이 크면서도 가장 덜 화려한 작업입니다.
-
트랜스크립트(Transcripts)는 당신의 스택 트레이스(stack trace)입니다 — 이를 읽는 습관을 들이세요. 정답은 거의 항상 JSONL 파일 내의 평문(plain text) 속에 놓여 있습니다. 10분 동안 읽는 것이 한 시간 동안 다시 실행하며 추측하는 것보다 훨씬 낫습니다.
다음 단계 (What's Next)
이 플레이북은 현재 수동이며, 2~3단계는 자동화가 절실히 필요합니다. 저는 '트랜스크립트 회의론자(transcript skeptic)'를 실험 중입니다. 이는 완료된 세션을 따라가며 어시스턴트의 주장이 이전의 도구 결과와 일치하지 않는 턴을 표시하는, 더 저렴한 두 번째 에이전트 패스(agent pass)입니다. 초기 결과는 유망합니다. 지난주에 제가 발견하기 전에 테스트 실패를 잘못 읽은 사례를 잡아냈습니다. 이것이 안정화된다면, 그것이 다음 포스팅의 주제가 될 것입니다.
마무리 (Wrap-up)
모델을 실패의 단위로 취급하는 것을 멈추고, '턴(turns)'을 실패의 단위로 취급하기 시작할 때까지 에이전트 디버깅은 마치 주술(voodoo)처럼 느껴졌습니다. 작게 재현하고, 발산 지점을 찾고, 현실과 믿음의 차이(diff)를 확인하며, 올바른 계층(layer)을 수정하세요. 그것은 단지 디버깅일 뿐입니다 — 이제 스택 트레이스는 트랜스크립트입니다.
만약 Claude Code나 다른 자율 에이전트 (autonomous agent)를 실행 중이라면: 다음에 에이전트가 당혹스러운 행동을 할 때, 다시 프롬프트를 입력(re-prompt)하지 마세요. 트랜스크립트 (transcript)를 열고 해당 턴 (turn)을 찾으세요. 💡
이 내용이 유익했다면, Dev.to에서 저를 팔로우해 주세요 — 저는 프로덕션 환경에서 자율 코딩 에이전트 (autonomous coding agents)를 구축하고 실행하는 과정, 즉 성공 사례와 실패 사례, 그리고 그 과정에서 얻은 플레이북 (playbooks)에 대해 정기적으로 글을 쓰고 있습니다. 아직 사용해 보지 않으셨다면, Claude Code를 가져가서 에이전트가 당신을 놀라게 하도록 해보세요. 그다음 그것을 디버깅하세요. 🚀
여러분의 에이전트가 가장 자신만만하게 틀렸던 일은 무엇인가요? 댓글로 알려주세요 — 실전 경험담 (war stories)을 수집하고 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기