AI 에이전트의 조용한 성능 저하(Silent Regressions)를 포착하기 위한 평가 스위트(Eval Suite) 구축기
요약
자율 코딩 에이전트의 시스템 프롬프트 수정으로 인한 미세한 성능 저하를 방지하기 위한 평가 스위트(Eval Suite) 구축 방법을 소개합니다. LLM의 주관적 판단 대신 골든 세트와 결정론적 검증 함수를 활용한 객관적 평가 체계 구축을 강조합니다.
핵심 포인트
- 시스템 프롬프트 수정 시 의도치 않은 성능 저하(Silent Regression) 발생 가능성
- LLM 판사 대신 AST 검사, 테스트 실행 등 결정론적 함수 활용 권장
- 정답이 명확한 '골든 세트(Golden Set)' 구축의 중요성
- 단순 통과 여부가 아닌 베이스라인 대비 차이점(Diff) 비교 필요
요약 (TL;DR)
나의 자율 코딩 에이전트(autonomous coding agent)가 약 2주 동안 조용히 성능이 저하되었지만, 아무도 나에게 알려주지 않았습니다. 에러도, 크래시(crash)도 없었습니다. 그저 제가 직접 파헤쳐 보기 전까지는 눈치채지 못할 정도로 출력이 약간 더 엉성해졌을 뿐입니다. 저는 에이전트의 설정을 수정할 때마다 고정된
그 원인을 약 2주 전의 시스템 프롬프트(System Prompt) 수정 사항에서 찾아냈습니다. 당시 저는 메시지가 너무 길다는 다른 작은 불만을 해결하기 위해, 에이전트에게 "커밋 메시지를 더 간결하게 작성할 것"을 요청하는 문단을 추가했었습니다. 모델은 제가 예측하지 못한 방향으로 과잉 교정(Overcorrect)을 해버렸습니다. 간결함이 모호함으로 변질된 것입니다. 장황한 메시지를 줄이기 위해 추가한 동일한 지침이, 그 이후의 모든 메시지에서 구체성(Specificity)까지 조용히 깎아내 버렸습니다. 합리적으로 들리는 이유로 추가한 단 한 줄의 문장이, 제가 알아차리기 전까지 2주 동안 출력 품질을 저하시켰던 것입니다.
그 순간, 저는 "슬쩍 봤을 때 괜찮아 보인다"라는 기준을 품질의 척도로 신뢰하는 것을 그만두었습니다.
해결 방법
해결책은 화려하지 않습니다. 작고 지루한 평가 스위트(Eval Suite)를 만드는 것입니다. 모든 것을 판단하는 LLM 판사(LLM-judges) 프레임워크나 화려한 대시보드가 아닙니다. 정답이 알려진 태스크들의 골든 세트(Golden Set)를 구축하여, 자동으로 실행하고, 점수를 매기며, 마지막으로 알려진 베이스라인(Baseline)과 비교(Diff)하는 방식입니다.
1단계 — '느낌(Vibes)' 세트가 아닌 '골든(Golden)' 세트 구축하기
저는 에이전트의 실제 업무를 대표하는 약 20개의 태스크를 선정했습니다. 몇 가지 리팩터링(Refactor), 미묘한 엣지 케이스(Edge Case)가 포함된 버그 수정, 파일을 수정하기 전 세 개의 파일을 읽어야 하는 태스크, 특정 파일을 명시적으로 건드리지 말아야 하는 태스크, 그리고 "이 작업을 하기 전에 물어봐야 하는" 시나리오 몇 가지를 포함했습니다. 핵심 제약 조건은 다음과 같습니다: 모든 태스크는 단순히 "적절해 보이는가"가 아니라, 출력을 확인할 수 있는 객관적인 방법이 필요합니다.
# golden_tasks.py
GOLDEN_TASKS = [
{
...
각 check는 AST 검사, 테스트 실행, 디프(Diff) 단언(Assertion)과 같은 작고 결정론적인(Deterministic) 함수입니다. 느낌(Vibes)을 채점하는 또 다른 LLM 호출이 아닙니다. 저는 이 교훈을 아주 힘들게 배웠습니다(아래에서 더 자세히 설명합니다).
2단계 — 의미 있는 변경이 있을 때마다 실행하기
시스템 프롬프트, 도구 권한(Tool Permissions), 또는 모델 티어(Model Tier)를 수정할 때마다, 테스트 하네스(Harness)는 격리된 샌드박스(Sandbox)에서 전체 골든 세트를 실행하고 통과/실패 여부와 함께 몇 가지 소프트 시그널(Soft Signals)을 기록합니다. 여기에는 디프(Diff) 크기, 예상된 세트 외에 수정된 파일 수, 확인이 필요할 때 확인을 요청했는지 여부 등이 포함됩니다.
def run_eval(agent_config, tasks=GOLDEN_TASKS):
results = []
for task in tasks:
...
Step 3 — 기준선 대비 차이점 확인 (Diff against baseline), 고정된 기준치 대비가 아님
단순히 통과/실패 카운트만으로는 성능 저하(drift)를 파악하기 어렵습니다. 실제로 제가 겪었던 2주간의 성능 저하는 현재 실행 결과를 이전 실행 결과와 비교(diffing)했을 때 나타났습니다. 동일한 태스크가 통과했음에도 불구하고, 차이점 크기(diff sizes)가 30%씩 증가하거나, '확인 요청해야 함' 태스크가 조용히 확인 요청을 중단하는 경우 등이었습니다. 저는 지난 N번의 실행 결과를 저장하고, 하드웨어적인 통과/실패 여부가 변하지 않았더라도 소프트한 지표(soft metrics)가 특정 임계값보다 더 많이 움직이는 모든 태스크에 플래그를 지정합니다.
flowchart LR
A[설정 변경 (Config change)] --> B[샌드박스에서 골든 세트 실행 (Run golden set in sandboxes)]
B --> C[점수: 통과/실패 + 소프트 신호 (Score: pass/fail + soft signals)]
...
Step 4 — 실패를 경고가 아닌, 조치 가능한 형태로 만들기
이 시스템의 첫 버전은 단순히 '성능 저하 감지됨(REGRESSION DETECTED)'만 출력했고, 저는 무엇을 해야 할지 몰라 두 번이나 무시했습니다. 이제는 모든 실패가 특정 태스크, 실패한 특정 검사 항목, 그리고 해당 동일 태스크에 대해 이번 실행 결과와 지난 통과했던 실행 결과를 비교한 차이점(diff)으로 연결됩니다. 30초 안에 무엇이 바뀌었는지 파악할 수 없다면, 이 평가는 쓸모가 없습니다. 그저 무시하는 법을 배우는 또 하나의 경고로 전락할 뿐입니다.
배운 교훈 (Lessons Learned)
-
"여전히 작동한다(It still runs)"는 품질 기준이 아닙니다. 에이전트는 좀처럼 요란하게 실패하지 않습니다. 비교할 수 있는 기준점(Baseline)이 있기 전까지는 일반적인 변동성(Variance)처럼 보이는 방식으로 성능이 저하됩니다. 골든 세트(Golden set)가 없다면, 프롬프트를 조금 수정할 때마다 눈을 가리고 비행하는 것과 같습니다.
-
LLM을 사용하여 LLM을 채점하지 마세요. 적어도 단독으로는 말입니다. 제가 만든 첫 번째 버전은 "이것이 좋은 PR 설명인가?"를 점수 매기기 위해 두 번째 모델 호출을 사용했습니다. 이는 동일한 입력에 대해서도 실행할 때마다 결과가 일관되지 않았습니다. 코드는 바뀌지 않았는데 어떤 때는 7점, 어떤 때는 9점이 나왔습니다. 결정론적 체크(Deterministic checks)(테스트 스위트 통과 여부, 금지된 파일이 수정되지 않았는지 여부, 특정 함수가 존재하는지 여부 등)는 지루할 수 있지만, 측정하려는 대상 위에 자체적인 변동성을 쌓아 올리지 않습니다. LLM 채점은 진정으로 주관적인 사항을 위해서만 남겨두고, 그 경우에도 여러 샘플의 평균을 내십시오.
-
소프트 시그널(Soft signals)은 하드 패스/페일(Hard pass/fail)이 놓치는 것을 포착합니다. 저의 성능 저하(Regression)는 어떤 체크 항목도 통과하지 못하지 않았습니다. 기술적으로는 모든 것이 여전히 통과 상태였습니다. 만약 제가 고정된 임계값(Fixed threshold) 대신 기준점(Baseline)에 대비하여 지표를 추적하고 있었다면, 디프(Diff) 크기와 "예상보다 더 많은 파일을 수정함"이라는 지표가 2주 전에 이를 잡아냈을 것입니다.
-
골든 세트는 크고 평균적인 것이 아니라, 작고 적대적(Adversarial)이어야 합니다. 저는 과거의 작업 200개를 이 평가에 쏟아붓고 싶은 유혹을 느꼈습니다. 하지만 특정 실패 모드(Failure mode)를 포착하도록 설계된, 의도적으로 까다로운 20개의 날카로운 작업이 "단순한 함수 작성"의 변형인 200개의 작업보다 더 많은 것을 알려주었습니다. 커버리지 수치를 위해서가 아니라, 무엇을 잡아낼 수 있는지에 따라 작업을 선택하십시오.
-
변경 사항을 신뢰하기 전에 실행하세요. 일주일 치 작업을 이미 쌓아 올린 후에 실행하지 마십시오. 평가(Eval)가 가끔씩 실행하는 있으면 좋은 기능(Nice-to-have) 수준이라면 그 목적은 사라집니다. 평가는 "프롬프트를 수정했다"와 "에이전트를 실제 작업에 투입한다" 사이의 게이트(Gate)가 되어야 합니다. 그렇지 않으면 단계만 더 늘어난 '느낌 기반(Vibes-based) QA'로 돌아가게 될 뿐입니다.
다음 단계 (What's Next)
현재 골든 세트(Golden set)는 수동으로 큐레이션되고 있으며, 이는 제가 테스트할 것이라고 생각한 회귀(Regression)만을 포착한다는 것을 의미합니다. 다음 반복 단계는 실제 실패 사례로부터 새로운 골든 태스크(Golden tasks)를 자동 생성하는 것입니다. 에이전트가 프로덕션 환경에서 잘못된 동작을 하여 제가 수동으로 수정해야 할 때마다, 그 실패 사례는 세트의 새로운 영구 항목이 됩니다. 이를 통해 동일한 실수가 인지되지 못한 채 조용히 다시 침투하는 것을 방지할 수 있습니다.
또한, 모델 제공업체들이 내부적으로 모델을 업데이트하며 이는 제가 제어할 수 없는 설정 변경이기 때문에, 아무런 변경 사항이 없을 때도 정기적으로 평가(Eval)를 실행하는 방안을 검토하고 있습니다. 그리고 '직전 실행(Last run)'보다 더 긴 기간 동안 소프트 시그널(Soft-signal) 베이스라인을 추적하고 싶습니다. 일주일에 1%씩 서서히 발생하는 성능 저하는 단일 실행 비교(Single-run diff)로는 놓칠 수 있으며, 이것이 바로 이 프로젝트가 존재하는 목적이자 포착해야 할 드리프트(Drift)의 전형적인 모습입니다. 단일 이전 실행 대신 7일 이동 평균(Rolling seven-day) 베이스라인을 사용했다면, 아마도 제가 실제로 발견한 것보다 일주일 더 일찍 회귀를 포착했을 것입니다.
아직 의도적으로 하지 않고 있는 한 가지는 통과율(Pass rates)에 대한 완전한 통계적 유의성 검정(Statistical significance testing)입니다. 태스크가 20개뿐인 상황에서는 단 한 번의 불안정한 실패(Flaky failure)가 실제로는 노이즈임에도 불구하고 마치 추세(Trend)처럼 보일 수 있습니다. 저는 통계적 검정을 지원할 수 없는 표본 크기에 통계 수치를 억지로 붙이기보다는, 통계가 의미를 갖기 시작할 정도의 크기로 골든 세트를 키우는 쪽을 택하겠습니다.
마무리 (Wrap-up)
중요한 작업에 자율 에이전트(Autonomous agent)를 실행하고 있다면, "한 번 확인해 봤는데 괜찮아 보였다"는 것은 회귀 테스트 전략이 아닙니다. 결정론적 체크(Deterministic checks)와 베이스라인 비교(Baseline diffing)를 갖춘 골든 세트를 구축하는 데는 오후 시간 정도면 충분하며, 이는 '느낌 기반(Vibes-based) QA'로는 절대 잡을 수 없는 2주간의 조용한 드리프트(Silent drift)를 잡아낼 것입니다.
이 내용이 유익했다면, 실제 엔지니어링 작업에 에이전트를 실행하며 작성하는 더 많은 빌드 로그를 위해 저를 팔로우해 주세요. 만약 여러분만의 AI 에이전트 평가 환경을 구축했다면, 어떻게 점수를 매기고 있는지 진심으로 듣고 싶습니다. 댓글로 남겨주세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기