자율 코딩 에이전트를 위한 관측성(Observability) 구축 방법: 5가지 교훈
요약
자율 코딩 에이전트의 실행 과정을 투명하게 모니터링하기 위한 관측성(Observability) 구축 방법을 다룹니다. 에이전트의 사후 보고에 의존하지 않고, 도구 호출 단계에서 독립적인 구조화된 로깅 레이어를 구축하여 '조용한 실패'를 방지하는 전략을 제시합니다.
핵심 포인트
- 에이전트의 요약 보고는 오류가 있을 수 있으므로 독립적인 로그가 필수적임
- 추론 과정이 아닌 에이전트와 도구 사이의 인터페이스에서 로그를 캡처해야 함
- 타임스탬프, 도구 인자, 결과 상태, 지속 시간을 기록할 것을 권장함
- 성공, 에러 외에도 'noop(아무 작업도 하지 않음)' 상태를 기록하는 것이 매우 중요함
- 검색과 안정성을 위해 .jsonl 형식의 추가 전용(append-only) 로깅을 활용함
요약 (TL;DR)
저는 한 번에 몇 시간씩 무인으로 작동하는 자율 코딩 에이전트(autonomous coding agent)를 운영하고 있습니다. 몇 달 동안 저는 에이전트가 "시작"과 "완료" 사이에서 _실제로 무엇을 하고 있었는지_에 대해 거의 아무런 가시성을 확보하지 못했습니다. 저는 에이전트의 도구 호출(tool calls) 위에 얇은 구조화된 로깅(structured-logging) 레이어를 구축했고, 이를 통해 디버깅 과정을 고고학적 발굴 작업에서 단순히... 읽는 작업으로 바꾸었습니다. 제가 무엇을 기록했는지, 무엇을 건너뛰었는지, 그리고 저에게 가장 많은 시간을 낭비하게 만든 실수는 무엇이었는지 소개합니다.
문제점
제 에이전트는 제가 지켜보지 않는 동안 긴 세션을 실행합니다. 파일을 읽고, 코드를 수정하고, 명령어를 실행하며, 결국 "인증 모듈을 리팩토링했으며 모든 테스트를 통과했습니다"와 같은 요약과 함께 결과를 보고합니다.
문제는 이 요약이 _틀릴 수도 있는 바로 그 에이전트에 의해 생성된다_는 점입니다. 만약 에이전트가 단계를 조용히 건너뛰었거나, 파일을 잘못 읽었거나, 잘못된 함수를 "수정"했다면, 최종 보고서는 여전히 자신감 있고 깔끔하게 작성됩니다. 저는 실행 중에 실제로 어떤 일이 일어났는지에 대한 독립적인 기록이 없었습니다. 오직 에이전트가 스스로에 대해 사후적으로 서술한 이야기만 있을 뿐이었습니다.
이 문제로 한 번 크게 데인 적이 있습니다. 에이전트는 리팩토링에 성공했다고 보고했습니다. 테스트도 통과(green)했습니다. 하지만 알고 보니 세션의 이전 단계에서 편집 사항을 적용하는 데 조용히 실패했고, 따라서 "통과된 테스트"는 변경되지 않은 이전 코드를 테스트하고 있었던 것입니다. 아무것도 충돌하지 않았고, 아무것도 잘못되어 보이지 않았습니다. 단지 주장한 것보다 조용히 적은 일을 했을 뿐입니다.
그때 깨달았습니다: 조용한 부분적 실패(silent partial failure)는 요란한 실패보다 더 나쁘다는 것을 말입니다. 충돌(crash)은 어디를 살펴봐야 할지 알려줍니다. 하지만 조용히 건너뛴 단계는 아무것도 알려주지 않으며, 최종 보고서는 누락을 통해 당신에게 적극적으로 거짓말을 합니다.
저는 에이전트가 나중에 들려주는 이야기와는 독립적으로, 에이전트가 취한 실제 행동 순서를 볼 수 있는 방법이 필요했습니다.
해결 방법
저는 에이전트의 추론(reasoning) 내부가 아니라, 에이전트와 도구(tools) _사이_에 위치하는 로깅 레이어를 추가했습니다. 이 차이는 매우 중요합니다. 만약 로그가 에이전트가 스스로에 대해 작성하는 것이라면, 요약이 틀릴 수 있는 것과 똑같은 방식으로 틀릴 수 있습니다. 하지만 로그가 도구가 실제로 실행될 때마다 하네스(harness)에 의해 방출(emit)되는 것이라면, 그것은 서술이 아니라 사실입니다.
제가 기록하는 것들
모든 도구 호출(tool call)에 대해, 저는 다음 항목들을 캡처합니다:
- 타임스탬프 (Timestamp)
- 도구 이름 및 인자(arguments)의 비식별화된 요약 (비밀 정보나 전체 파일 내용은 포함하지 않으며, 무엇이 일어났는지 식별할 수 있을 정도만 포함)
- 결과 상태: 성공(success), 에러(error), 또는 "noop" (마지막 항목이 가장 가치 있는 카테고리로 밝혀졌습니다)
- 지속 시간 (Duration)
import json
import time
...
각 실행은 .jsonl 파일에 추가(append)됩니다 — 한 줄당 하나의 JSON 객체 형식입니다. 이 형식은 생각보다 훨씬 중요합니다. 추가 전용(append-only)이며, 크래시 안전(crash-safe, 마지막 줄이 잘리더라도 이전 줄을 손상시키지 않음)하고, 데이터베이스 없이도 일반적인 jq나 Python을 사용하여 매우 쉽게 검색(greppable)할 수 있기 때문입니다.
"noop" 상태가 실제 해결책이었습니다
이 모든 일의 시작이었던 버그 — 조용히 건너뛰어진 수정(edit) — 는 제가 noop 상태를 추가한 순간 드러났습니다. 실행은 되지만 아무것도 변경하지 않는(대상 텍스트를 찾지 못했거나, 파일이 이미 원하는 상태인 경우) 수정 도구는 성공적인 수정(successful edit)과 같지 않습니다. 제가 이 둘을 구분하기 전에는, 제 머릿속에서 둘 다 그저 "성공(success)"으로 나타났습니다. 하지만 이를 분리한 후에는, 이전의 실패한 실행이 즉시 다음과 같이 나타났을 것입니다:
{"tool": "edit_file", "args": "auth.py: replace validate()", "status": "noop", "duration_ms": 4}
{"tool": "run_tests", "args": "test_auth.py", "status": "success", "duration_ms": 812}
noop 직후에 그것이 수정했어야 할 대상에 대해 success가 나타나는 것은, 일단 눈에 보이기 시작하면 매우 큰 위험 신호(red flag)입니다. 에이전트가 작성한 최종 요약문만 가지고 있었을 때는 이 문제가 보이지 않았습니다.
세션 시각화하기
구조화된 로그를 갖추고 나니, 단일 세션을 하나의 시퀀스(sequence)로 추적하는 것이 쉬워졌습니다:
sequenceDiagram
participant Agent
participant Harness
...
이것이 렌더링된 것을 보는 것만으로도 약 5초 만에 격차(gap)가 명확히 드러났습니다. 에이전트의 산문 형태 요약문을 읽으며 세 번의 별도 디버깅 세션을 거치는 동안 놓쳤던 부분입니다.
샘플링, 모든 것을 다 하지는 마세요
저는 파일 전체 내용이나 명령의 전체 출력(full command output)을 로그로 남기지 않습니다. 그렇게 하면 트레이스(trace) 파일의 크기가 급격히 커져서, 제가 피하려고 했던 "아무도 읽지 않는 텍스트의 벽" 문제를 다시 불러올 것이기 때문입니다. 대신 요약된 문자열(diff의 처음 약 100자, 명령 이름 및 종료 코드, stdout 제외)을 기록합니다. 특정 단계에 대해 전체 세부 정보가 필요한 경우에는 해당 단계만 수동으로 다시 실행합니다. 트레이스의 역할은 어디를 살펴봐야 할지 알려주는 것이지, 전체를 재현(replay)하는 것이 아닙니다.
읽는 대신 트레이스에 쿼리하기
로그가 구조화된 후 바뀐 또 다른 점은, 로그를 위에서 아래로 "읽는" 것을 멈추고 "쿼리(querying)"하기 시작했다는 것입니다. 다음과 같은 한 줄짜리 명령어를 사용하면 "오늘 무언가 조용히 지나간 것이 있는가?"라는 질문에 약 1초 만에 답할 수 있습니다.
cat agent_trace.jsonl | jq -r 'select(.status == "noop") | "\(.tool) \(.args)"'
매 세션이 끝날 때마다 이 명령어를 실행하기 시작하자, 이전에는 완전히 놓쳤던 패턴이 눈에 띄었습니다. 대략 12번의 세션 중 1번꼴로 중간 어딘가에서 쓰기 도구(write tool)에 대해 최소 하나 이상의 noop이 발생하고 있었습니다. 대부분의 경우 파일이 이미 원하는 상태에 있었기에 무해했습니다. 하지만 이러한 noop 이벤트 중 약 4분의 1은 실제 문제를 숨기고 있었는데, 이는 이 모든 노력을 시작하게 만든 것과 동일한 유형의 버그였습니다. 쿼리를 사용하기 전에는 그 비율을 추정할 방법조차 없었으며, 며칠 뒤에 증상을 발견하는 것에만 의존해야 했습니다.
또한 세션당 도구 호출(tool calls) 횟수를 이동 평균(rolling count)으로 추적하며, 정상 범위를 크게 벗어나는 모든 사항에 플래그를 지정합니다. 보통 20~40회의 도구 호출을 수행하던 세션이 갑자기 3회만 수행하고 멈춘다면, 200회를 수행하는 세션만큼이나 의심스럽습니다. 두 경우 모두 작업이 유난히 쉽거나 어려웠기 때문이 아니라, 상위 단계(upstream)에서 루프를 깨뜨렸음을 의미하는 경우가 많기 때문입니다.
오버헤드를 정직하게 유지하기
타당한 반론입니다. 이 모든 로깅(logging) 작업이 단순함을 유지하려는 바로 그 시스템에 오버헤드(overhead)와 복잡성을 더하는 것 아닌가요? 실제로 로깅 레이어는 40줄 미만의 코드이며, 쓰기 작업 자체는 한 자릿수 밀리초(milliseconds) 내에 완료됩니다. 도구(tool)가 호출될 때마다 단 한 번의 open().write() 호출이 발생할 뿐이며, 네트워크 왕복(network round-trip)이나 외부 서비스도 필요하지 않습니다. 무언가 미묘하게 잘못되었을 때, 기억과 스크롤백(scrollback)을 뒤져가며 세션을 재구성하는 데 소요되던 몇 분(때로는 몇 시간)에 비하면 이 비용은 미미한 수준입니다.
교훈 (Lessons Learned)
-
추론(reasoning) 내부가 아니라 경계(boundary)에서 로깅하세요. 에이전트가 스스로에 대해 작성하는 로그는 에이전트 자신의 사각지대(blind spots)를 그대로 물려받습니다. 반면, 도구가 실제로 실행될 때마다 하네스(harness)가 방출하는 로그는 에이전트가 무엇이 일어났다고 믿는지와 무관한 실측 데이터(ground truth)입니다.
-
"No-op"은 별도의 상태를 가질 가치가 있습니다. 성공/실패(Success/fail)만으로는 부족합니다. 오류 없이 실행되었지만 아무것도 변경하지 않은 도구는 별개의 중요 사례입니다. 보통 진짜 버그는 바로 이곳에 숨어 있으며, 이를 "성공"으로 묶어버리는 바람에 저는 몇 주 동안 제 버그를 놓치고 말았습니다.
-
영리한 방식보다 추가 전용(Append-only) 방식이 낫습니다. 처음에는 작은 SQLite 로그를 사용하려 했으나 즉시 더 취약하다는 것을 깨달았습니다. 쓰기 도중 충돌(crash)이 발생하면 상태가 손상될 수 있었고, 이를 검사하려면 별도의 도구가 필요했습니다. 단순한 JSON Lines 파일은
cat과jq외에는 아무것도 필요하지 않았고, 충돌 후에도 잘 살아남았으며, 실행 중에 실시간으로tail을 통해 확인할 수 있었습니다. -
덤프(dump)하지 말고 요약하세요. 모든 로그 라인에 전체 stdout/diff를 포함하면 트레이스(trace)를 읽기 어려워지고 저장 비용도 많이 듭니다. 어디를 살펴봐야 할지 알 수 있을 정도로만 로그를 남기고, 실제로 상세 정보가 필요할 때 해당 특정 단계만 다시 실행하세요.
-
시각화는 데이터 모델을 만든 후에 구축하세요. 첫날부터 화려한 대시보드(dashboard)를 만들고 싶은 유혹을 느꼈습니다. 하지만 실제로 중요했던 것은 로그 스키마(schema)를 먼저 제대로 잡는 것이었습니다. 즉, 상태 카테고리(status categories)와 무엇을 하나의 "이벤트(event)"로 간주할 것인가를 정하는 것이었습니다. 일단 그것이 견고해지자, 원시 로그(raw log)로부터 만든 단 다섯 줄짜리 시퀀스 다이어그램(sequence diagram)이 제가 상상했던 대시보드보다 훨씬 유용했습니다.
다음 단계 (What's Next)
저는 noop 탐지기를 능동적인 체크(active check)로 전환하는 작업을 진행 중입니다. 즉, 사후에 제가 직접 트레이스(trace)를 눈으로 확인하는 대신, 하네스(harness)가 편집/쓰기 도구(edit/write tool)에서 noop이 발생할 경우 실행 중 경고를 표시하도록 하여, 에이전트가 이를 기반으로 잘못된 서사를 쌓아가기 전에 조용한 noop이 드러나도록 하는 것입니다.
또한 시간 기반의 이상 징후 플래그(duration-based anomaly flags)도 추가하고 싶습니다. 평소 2초가 걸리던 단계가 갑자기 40초가 걸린다면, 기술적으로는 "성공"했을지라도 다시 살펴볼 가치가 있기 때문입니다. 그리고 작업 유형별로 작은 롤링 베이스라인(rolling baseline, 평균 도구 호출 횟수, 도구당 평균 소요 시간)을 유지하고 싶습니다. 이를 통해 "이상 징후"를 단일한 전역 임계값(global threshold)이 아닌 해당 작업 자체의 이력과 비교하여 측정하고자 합니다. 현재의 전역 임계값 방식은 실제로 규모가 큰 작업에서 제가 원하는 것보다 더 많은 오탐(false positives)을 발생시키고 있습니다.
이 모든 것이 화려할 필요는 없습니다. 이 모든 과정의 밑바탕에 깔린 교훈은 관측성(observability)에 대시보드나 벤더(vendor)가 필요하지 않다는 것입니다. 관측성에는 관찰하려는 대상으로부터 독립적인, 지루하고 구조화된, 추가 전용(append-only) 사실 로그(fact log)가 필요할 뿐입니다.
마무리 (Wrap-up)
만약 어떤 종류의 자율 에이전트(autonomous agent)를 무인으로 실행하고 있다면, 에이전트가 작성한 요약본만을 발생한 일에 대한 유일한 기록으로 신뢰하지 마세요. 도구 호출(tool calls)을 독립적으로 기록하고, "아무것도 하지 않음"에 별도의 상태를 부여하며, 새벽 2시에도 grep으로 검색할 수 있을 만큼 형식을 지루할 정도로 단순하게 유지하세요.
이 글이 공감이 되었다면, Dev.to에서 저를 팔로우해 주세요. 저는 자율 코딩 에이전트를 구축하고 실행하며 얻은 실질적이고 때로는 고통스러운 교훈들에 대해 글을 씁니다. 여러분의 로깅 설정은 어떤 모습인지 궁금합니다. 댓글로 남겨주세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기