멈추지 않고 클릭만 반복하는 LLM 에이전트 디버깅: 사후 분석 (Postmortem)
요약
브라우저 자동화 에이전트 BrowserPilot이 특정 동작을 무한 반복하는 루프 현상을 분석하고 해결한 과정을 다룹니다. DOM pruning 과정에서 상태 변화 정보가 누락되어 모델이 동일한 환경으로 인식했던 원인을 규명했습니다.
핵심 포인트
- 에이전트의 무한 루프는 모델의 오류가 아닌 컨텍스트 전달 오류일 수 있음
- 스크린샷만으로는 모델이 실제로 수신한 추론 컨텍스트를 파악하기 어려움
- 구조화된 로깅(Structured Logging)을 통해 LLM 호출과 DOM 상태를 추적해야 함
- DOM pruning 시 상태 변화 속성이 삭제되지 않도록 주의가 필요함
저는 Playwright와 LLM을 결합하여 실제 웹사이트를 탐색하는 브라우저 자동화 에이전트인 BrowserPilot을 구축했습니다. FastAPI 백엔드, 실시간 작동을 관찰할 수 있는 WebSocket UI, 그리고 의사결정을 담당하는 Gemini 2.0 Flash로 구성되어 있습니다. 작업을 지정하면 에이전트가 계획을 세우고, 클릭하고, 타이핑하며, 결과를 보고합니다.
그런데 에이전트가 루프(loop)에 빠졌습니다. 40번 이상의 도구 호출 (tool calls) 동안 진전 없이 똑같은 버튼을 계속해서 반복해서 클릭했습니다. 이것은 제가 그 원인을 어떻게 찾아냈는지, 그리고 이런 일이 다시 조용히 발생하지 않도록 무엇을 구축했는지에 대한 기록입니다.
증상
에이전트는 특정 페이지 상태에 도달하면 "클릭 (click)" 동작을 수행하고, 앞으로 나아가는 대신 다음 턴에 정확히 동일한 "클릭 (click)" 동작을 다시 수행했습니다. 그리고 또 반복했습니다. 에이전트는 충돌(crashing)하는 것이 아니라, 확신에 차서 똑같은 잘못된 행동을 반복하며 그동안 API 호출을 계속 소모하고 있었습니다.
WebSocket UI를 통해 실시간으로 지켜보는 것만으로는 "멈춰 있다"는 사실 외에 별다른 정보를 얻을 수 없었습니다. 스크린샷은 반복되는 단계 사이에서 동일해 보였습니다. 예외는 없었습니다. 명백하게 망가진 상태도 없었습니다. 그저 똑같은 호출을 계속하면서 그것이 작동하지 않고 있다는 사실을 전혀 알아차리지 못하는 모델이 있을 뿐이었습니다.
"스크린샷만 보는 것"이 충분하지 않았던 이유
저의 첫 번째 본능은 각 단계에서 모델에 전달되는 스크린샷을 눈으로 직접 확인하는 것이었습니다. 스크린샷은 괜찮아 보였습니다. 같은 페이지, 같은 버튼이었습니다. 제가 단순히 눈으로 봐서는 알 수 없었던 것은, 각 턴마다 모델이 실제로 컨텍스트 (context)로서 무엇을 받았는가 하는 점이었습니다. 이전 동작의 결과가 전달되었는가? DOM 스냅샷 (DOM snapshot)이 클릭이 등록되었음을 정확하게 반영하고 있는가? 프롬프트 (prompt)가 이전 시도들에 대해 모델에게 알려주고 있는가?
스크린샷은 세상의 모습을 보여줍니다. 하지만 모델의 추론 (reasoning) 과정이나, 모델이 추론을 위해 실제로 무엇을 받았는지는 보여주지 않습니다. 저에게 필요했던 것은 첫 번째가 아니라 두 번째였습니다.
트레이스 (trace) 구축하기
저는 모든 LLM 호출 — 요청(request)과 응답(response) — 에 대해 구조화된 로깅 (structured logging)을 추가하여 llm_calls.jsonl 파일에 저장했습니다. 각 라인에는 전송된 전체 프롬프트 (prompt), 요청된 도구 호출 (tool calls), 해당 시점의 DOM 상태, 타임스탬프가 포함됩니다. 특별한 기술은 아니었지만, grep이나 diff로 확인할 수 있는 추가 전용 (append-only) 구조화된 로그였습니다.
그것을 확보하고 나니, 버그는 더 이상 보이지 않는 존재가 아니었습니다. 연속된 항목들을 diff(차이 분석)해 본 결과 실제 문제가 드러났습니다. 바로 DOM pruning (DOM 가지치기) 단계였습니다. 이 단계는 토큰 사용량을 적절하게 유지하기 위해 모델로 전송하기 전 페이지 구조를 다듬는데, 클릭이 발생한 후 변경된 state (상태) 속성을 삭제하고 있었습니다. 모델에게는 12번째 턴과 13번째 턴이 동일하게 보였습니다. 유일하게 변한 것이 바로 pruning (가지치기)되어 사라진 바로 그 부분이었기 때문입니다.
클릭 자체가 실패한 것이 아니었습니다. 클릭이 성공했는지에 대한 에이전트의 시각 (view) 이 실패한 것이었습니다. 에이전트는 무언가 변했다는 신호를 전혀 받지 못했기에, 에이전트의 관점에서는 동일한 동작을 반복하는 것이 합리적인 판단이었습니다.
실제 수정 사항
이 과정을 통해 세 가지 변경 사항이 도출되었으며, 중요도가 높은 순서대로 나열하면 다음과 같습니다.
1. 상태 관련 속성을 보존하도록 pruning (가지치기) 로직 수정. DOM pruner (DOM 가지치기 도구)는 정확성보다 토큰 효율성을 우선하여 설계되었습니다. 저는 주변의 다른 요소들을 다듬더라도 상태 변화를 흔히 나타내는 속성들(aria-expanded, checked, disabled, 대상 요소의 class 차이 등)은 항상 유지하도록 로직의 균형을 재조정했습니다.
2. "지난 동작 이후 무언가 변했는가"에 대한 명시적인 신호 추가. 모델이 가공되지 않은 DOM diff (DOM 차이)로부터 상태 변화를 추론하도록 맡기는 대신, 이제 서버 측에서 가벼운 diff를 계산하고 프롬프트에 직설적인 한 줄 요약을 주입합니다: 변경됨 / 변경되지 않음, 그리고 변경되었다면 무엇이 변경되었는지에 대한 정보입니다. 계산 비용이 저렴하며, 모델이 직접 변화를 알아챌 필요 없이 그저 반응하기만 하면 된다는 것을 의미합니다.
3. 최후의 보루로서의 반복 방지 장치 (repetition guard) 추가. 동일한 인자를 가진 동일한 tool call (도구 호출)이 상태 변화 없이 3회 연속 발생할 경우, 에이전트는 이제 루프를 탈출하여 대안 전략을 시도하거나, 조용히 계속 진행하는 대신 정체 상태를 표면화합니다. 이것은 근본 원인에 대한 해결책이라기보다는, 아직 발견하지 못한 다음 근본 원인에 대비한 보험입니다.
일반화할 수 있는 부분
특정한 버그는 제 실수였습니다. 잘못된 것을 잘라내는 가지치기(pruning) 함수가 문제였습니다. 하지만 이 버그의 형태는 브라우저 에이전트(browser agents)에만 국한된 것이 아닙니다. LLM 에이전트는 오직 자신이 전달받은 내용에 대해서만 추론할 수 있으며, "스크린샷에서 보기에 괜찮다"는 것이 "실제로 모델의 컨텍스트(context)에 존재한다"는 것과 동일하지는 않습니다. 에이전트가 루프(loop)를 돌거나, 정체되거나, 반복하는 상황이 발생할 때마다 가장 먼저 해야 할 일은 출력을 뚫어지게 쳐다보는 것이 아닙니다. 모든 단계에서 입력을 있는 그대로 기록(log)하고, 턴(turn) 간의 차이(diff)를 비교하는 것입니다. 만약 모델에게 두 개의 턴이 동일하게 보인다면, 당연히 모델은 동일하게 행동할 것입니다. 질문은 언제나 "모델이 왜 멍청하게 행동하는가"가 아니라, "왜 그것들이 동일하게 보이는가"여야 합니다.
또한, 작업 중에 가동 시간(uptime) 확보를 위해 프로바이더 폴백 체인(provider fallback chain, Gemini → Groq)을 추가했는데, 이것이 두 번째 이유로 유용하다는 사실을 알게 되었습니다. 즉, 동일한 트레이스(trace)를 다른 모델로 실행해 보는 것이 정체 현상이 프롬프트/컨텍스트 문제인지(두 모델 모두 루프를 도는 경우), 아니면 Gemini 특유의 기이한 동작인지(하나의 모델만 루프를 도는 경우)를 빠르게 확인(sanity-check)할 수 있는 방법이었습니다. 단순한 장애 극복(failover) 수단이 아니라, 디버깅 도구로서의 교차 프로바이더 비교(Cross-provider comparison)인 셈입니다.
*저는 Python을 사용하여 백엔드 및 브라우저 자동화 시스템(FastAPI, Playwright, LLM 통합)을 구축합니다. 유사한 에이전트 신뢰성(agent-reliability) 문제를 겪고 있거나 이에 대해 논의하고 싶다면, https://www.linkedin.com/in/amarjit-jim-978322371로 연락해 주세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기