131개의 테스트, 4개의 레이어, 실행당 $0.03: 나의 AI 에이전트 평가 하네스 (Eval Harness)
요약
AI 에이전트의 '조용한 실패(silent failure)'를 방지하기 위해 구축한 4단계 평가 하네스를 소개합니다. 유닛 테스트가 잡아내지 못하는 의미론적 드리프트를 포착하기 위해 131개의 테스트를 실행하며, 저비용으로 에이전트의 성능을 검증합니다.
핵심 포인트
- 전통적 유닛 테스트는 확률론적 AI의 의미론적 드리프트를 포착하기 어려움
- 4개의 레이어와 131개의 테스트를 통해 에이전트의 성능 저하를 검증
- Groq와 Claude 3.5 Sonnet을 조합하여 실행당 $0.03의 저비용 달성
- 프롬프트 변경이 구조적 오류 없이 데이터 누락을 유발하는 사례 분석
원래 AIdeazz에 게시되었습니다 — 정식 링크와 함께 이곳에 교차 게시되었습니다.
저는 '조용한 실패 (silent failure)'를 배포했습니다. 사용자 메시지에서 구조화된 데이터 (structured data)를 추출하도록 설계된 저의 프로덕션 AI 에이전트가 중요한 필드에 대해 null을 반환하기 시작했습니다. 에러도 아니고, 예외 (exception)도 아닌, 그저 null이었습니다. 27개의 모든 유닛 테스트 (unit tests)를 통과했습니다. 저의 CI/CD 파이프라인은 초록불이었습니다. 제가 가진 모든 지표에 따르면 에이전트는 "작동 중"이었습니다. 10개의 사용자 상호작용을 수동으로 점검한 후에야 문제를 발견할 수 있었습니다. 이 순간 저는 새로운 기능을 만드는 것을 멈추고 AI 에이전트 평가 하네스 (evaluation harness)를 만들기 시작했습니다.
현재 저의 하네스는 에이전트의 모든 변경 사항에 대해 4개의 별도 레이어에 걸쳐 131개의 테스트를 실행합니다. 속도를 위해 Groq를 활용하고 복잡한 평가를 위해 Claude 3.5 Sonnet을 사용하여, 전체 실행당 평균 비용은 $0.03가 듭니다. 이 설정은 유닛 테스트가 근본적으로 잡아낼 수 없는 문제들을 포착합니다. 왜냐하면 AI 에이전트의 실패는 명백한 코드 오류보다는 종종 의미론적 드리프트 (semantic drift) 또는 _성능 저하 (performance degradation)_로 나타나기 때문입니다.
문제점: 의미론적 드리프트 (Semantic Drift)와 조용한 실패 (Silent Failures)
전통적인 유닛 테스트는 결정론적 (deterministic) 코드를 검증합니다. add(2, 2)는 항상 4를 반환해야 합니다. 하지만 AI 에이전트는 확률론적 (probabilistic) 영역 내에서 작동합니다. 프롬프트 (prompt)의 변경, 새로운 시스템 메시지, 또는 심지어 다른 LLM 버전조차 코드 구조를 깨뜨리지 않으면서 출력을 미묘하게 변화시킬 수 있습니다.
저의 초기 실패는 완벽한 사례였습니다. 에이전트의 작업은 맞춤형 AI 에이전트 구축을 위한 사용자 요청을 파싱(parse)하는 것이었습니다. 한 필드는 target_platform (예: "Telegram", "WhatsApp", "Web")이었습니다. 저는 출력되는 JSON의 "명확성과 간결함 (clarity and conciseness)"을 강조하도록 시스템 프롬프트 (system prompt)를 업데이트했습니다. 장황함을 줄이기 위한 이 겉보기에 무해한 변경은, 스키마 (schema)에서 이를 명시적으로 요구함에도 불구하고, LLM이 문맥상 플랫폼이 "명백하다"고 느끼면 때때로 target_platform을 누락하게 만드는 결과를 초래했습니다. 에이전트의 Python 코드는 여전히 json.loads()를 호출하고 data['target_platform']에 접근했습니다. 키(key)가 누락되었을 때, 파싱 로직에 따라 None 또는 null로 기본 설정되었습니다. KeyError도, TypeError도 발생하지 않았습니다. 그저 null이었을 뿐입니다.
이것이 바로 의미론적 드리프트 (semantic drift)입니다. 에이전트의 _코드 실행 (code execution)_이 아니라, 작업에 대한 에이전트의 _해석 (interpretation)_이 변한 것입니다. 함수의 입력과 출력에 집중하는 유닛 테스트 (Unit tests)는 이를 잡아낼 수 없습니다. 유닛 테스트는 파서 (parser)가 작동하는지는 검증하지만, LLM이 _항상 해당 키를 생성하는지_는 검증하지 못하기 때문입니다.
나의 평가 하네스 (Evaluation Harness)의 4개 레이어
저의 AI 에이전트 평가 하네스 (evaluation harness)는 다양한 시나리오에 걸쳐 에이전트의 _행동 (behavior)_과 _출력 품질 (output quality)_에 집중함으로써 131개의 테스트로 프로덕션 준비 상태 (production readiness)를 검증합니다. 저는 커스텀 Python 스크립트와 테스트 케이스 관리를 위한 PostgreSQL 데이터베이스를 사용하여 Oracle Cloud Infrastructure (OCI)에서 이 테스트들을 실행합니다.
레이어 1: 입력 견고성 (Input Robustness) (38개 테스트)
이 레이어는 에이전트가 사용자 입력의 변동을 어떻게 처리하는지에 초점을 맞춥니다. 이는 에이전트가 흔히 발생하는 실제 환경의 편차에 직면했을 때, 시스템이 중단되거나 쓰레기 값 (garbage)을 생성하지 않도록 보장하는 것에 관한 것입니다.
- 질문 방식의 변형 (Variations in phrasing): 동일한 질문을 서로 다른 방식으로 묻는 15개의 테스트 (예: "Telegram bot을 만들어줘", "TG용 봇이 필요해", "Telegram 에이전트 부탁해").
- 정보 누락 (Missing information): 필수 필드가 누락된 10개의 테스트로, 에이전트가 명확한 질문을 던지거나 특정 "불완전함 (incomplete)" 상태를 반환하는지 확인합니다.
- 모호한 요청 (Ambiguous requests): 모호하거나 모순된 지침이 포함된 8개의 테스트로, 우아한 처리 (graceful handling) 또는 확인 과정을 체크합니다.
- 엣지 케이스 (Edge cases): 최대 길이 입력, 특수 문자 또는 빈 메시지에 대한 5개의 테스트.
예를 들어, 예약을 하도록 설계된 에이전트에게 "예약해줘"라고 요청했을 때, "null 날짜, null 시간에 예약되었습니다."가 아니라 "날짜와 시간은 언제인가요?"라고 응답해야 합니다.
레이어 2: 출력 충실도 (Output Fidelity) (52개 테스트)
이것은 의미론적 드리프트 (semantic drift)를 포착하는 핵심입니다. 에이전트의 출력이 단순히 구문론적 정확성 (syntactic correctness)뿐만 아니라, 의도된 의미 (intended meaning) 및 _구조 (structure)_와 일치하는지 검증합니다.
- 스키마 준수 (Schema adherence): 출력된 JSON이 모든 필수 필드와 올바른 데이터 타입을 포함하여 Pydantic 스키마를 엄격히 따르는지 확인하는 20개의 테스트입니다. 이는
pydantic.validate_json과 중첩된 구조에 대한 커스텀 체크를 사용합니다. - 콘텐츠 정확성 (Content accuracy): 참조 LLM (추론 능력을 고려하여 Claude 3.5 Sonnet 사용)이 원래의 프롬프트 및 사람이 정의한 "골드 스탠다드 (gold standard)" 정답과 비교하여 에이전트의 출력을 평가하는 15개의 테스트입니다. 예를 들어, 에이전트가 요약을 수행해야 하는 경우, 참조 LLM은 요약이 주요 지점들을 잘 포착했는지 확인합니다.
- 완전성 (Completeness): 출력에 기대되는 모든 정보 조각이 포함되어 있는지 확인하는 10개의 테스트입니다. 이를 통해 저의
target_platformnull문제를 발견할 수 있었습니다. - 간결성/장황함 (Conciseness/Verbosity): 출력이 특정 길이 또는 세부 사항 요구 사항을 충족하는지 평가하는 7개의 테스트입니다. 빠른 답변을 위해 설계된 에이전트라면 에세이를 써서는 안 됩니다.
콘텐츠 정확성 테스트는 매우 중요합니다. 저는 다음과 같은 프롬프트를 사용합니다: "사용자의 요청: '{user_input}', 에이전트의 출력: '{agent_output}', 그리고 기대되는 출력 특성: '{expected_characteristics}'가 주어졌을 때, 에이전트의 출력이 정확하고 완전한지 평가하세요. 'PASS' 또는 'FAIL'로 응답하고 그 뒤에 짧은 설명을 덧붙이세요." 이러한 평가들은 종종 Claude에서 실행되기 때문에, 여기서 실행당 $0.03의 비용이 발생합니다.
레이어 3: 도구/함수 호출 정확성 (Tool/Function Call Correctness) (27개 테스트)
저의 많은 에이전트는 요청을 전문화된 하위 에이전트(sub-agents)로 라우팅하거나 외부 API를 호출하는 멀티 에이전트 시스템 (multi-agent systems)입니다. 이 레이어는 이러한 상호작용을 검증합니다.
- 올바른 도구 선택 (Correct tool selection): 주어진 의도에 대해 에이전트가 올바른 도구를 호출하는지 확인하는 10개의 테스트입니다 (예: "회의 일정 잡기"가
email_tool.send가 아닌calendar_tool.schedule을 호출하는지 확인). - 올바른 인자 (Correct arguments): 도구에 전달되는 인자들이 사용자의 요청으로부터 올바르게 추출되었는지, 그리고 도구의 스키마 (schema)에 따라 형식이 지정되었는지 확인하는 12개의 테스트입니다. 이는 LLM (Large Language Models)에서 흔히 발생하는 실패 지점입니다.
- 오류 처리 (Error handling): 도구 실패(예: API 타임아웃, 잘못된 인증 정보)를 시뮬레이션하고, 에이전트가 이를 우아하게 처리하는지, 사용자에게 알리는지, 또는 재시도하는지 확인하는 5개의 테스트입니다.
저는 환경을 제어하고 결정론적인 (deterministic) 결과를 보장하기 위해 이러한 테스트 중에 외부 API를 모킹 (mock) 합니다.
레이어 4: 성능 및 비용 (Performance and Cost) (14개 테스트)
정확성에 관한 것은 아니지만, 이는 프로덕션 (production) 환경에서 매우 중요합니다.
- 지연 시간 벤치마크 (Latency benchmarks): 다양한 입력 복잡도에 대한 엔드 투 엔드 (end-to-end) 응답 시간을 측정하는 5개의 테스트입니다. 저는 p50, p90, p99 지연 시간을 추적합니다. 저의 목표는 Groq에서 단순 요청에 대해 2초 미만으로 유지하는 것입니다.
- 토큰 사용량 (Token usage): 상호작용당 비용을 모니터링하기 위해 입력/출력 토큰 수를 기록하는 5개의 테스트입니다. 이는 프롬프트 비대화 (prompt bloat) 또는 LLM의 장황한 응답을 식별하는 데 도움이 됩니다.
- 속도 제한 처리 (Rate limit handling): LLM 제공업체의 속도 제한 (rate limits)에 도달하는 상황을 시뮬레이션하고, 에이전트의 재시도 로직 또는 폴백 (fallback) 메커니즘을 검증하는 4개의 테스트입니다.
현재 저의 설정은 단순한 파싱 (parsing) 및 라우팅 (routing)에 있어 속도와 비용 효율성이 뛰어나기 때문에, 대부분의 에이전트에서 초기 LLM 호출을 위해 Groq를 사용합니다. 더 복잡한 추론 (reasoning) 또는 평가 (evaluation) 작업은 Claude 3.5 Sonnet으로 라우팅됩니다. 이러한 하이브리드 (hybrid) 접근 방식은 중요한 부분에서 높은 품질을 유지하면서도 에이전트 상호작용당 평균 비용을 낮게 유지합니다.
하네스 구축: 인프라 및 워크플로우 (Workflow)
저의 하네스는 Python 애플리케이션입니다. 테스트 케이스는 PostgreSQL 데이터베이스에 저장되어 관리가 용이하고 버전 관리가 가능합니다. 각 테스트 케이스에는 다음 항목이 포함됩니다:
test_idagent_idlayer(예: 'input_robustness')test_nameuser_inputexpected_output_characteristics(LLM 평가를 위한 자연어 설명)expected_json_schema(엄격한 스키마 검증용)expected_tool_call(도구 호출 검증용)status(Pass/Fail)details(에러 메시지, LLM 평가 근거)
에이전트에 변경 사항(예: 프롬프트 업데이트, 새로운 도구 정의)이 생기면 전체 하네스 실행을 트리거합니다. 스크립트는 테스트 케이스를 반복하며 에이전트를 호출한 다음, 각 레이어에 적합한 평가 로직을 실행합니다.
LLM 기반 평가(Layer 2)의 경우, 사용자 입력, 에이전트 출력, 기대 특성을 입력받아 PASS/FAIL 및 그 이유를 반환하는 전용 EvaluationAgent(그 자체로 AI 에이전트)를 사용합니다. 이 EvaluationAgent는 강력한 추론 능력을 갖춘 Claude 3.5 Sonnet을 기반으로 작동하는 것이 일반적입니다.
결과는 다시 데이터베이스에 기록되며, 간단한 웹 대시보드(내부용으로 Streamlit을 사용하여 구축)를 통해 제공됩니다. 테스트 실패 시 즉시 배포가 차단됩니다.
테스트를 하지 않았을 때의 비용
target_platform이 null인 문제는 몇 시간의 디버깅 시간과 좌절한 사용자, 그리고 신뢰 상실이라는 대가를 치르게 했습니다. 만약 프로덕션(Production)에 적용되기 전, 더 일찍 이 문제를 발견했더라면 그 영향은 미미했을 것입니다. 131개의 테스트를 실행하는 데 드는 회당 $0.03의 비용은, 프로덕션 이슈를 디버깅하는 데 소요되는 엔지니어링 시간이나 오작동하는 에이전트로 인한 잠재적 매출 손실에 비하면 사소한 비용입니다.
하네스(Harness)를 구축하는 데 들어간 초기 투자 시간은 약 2주였습니다. 여기에는 테스트 레이어(Test layers) 설계, 데이터베이스 설정, 그리고 평가 로직(Evaluation logic) 작성이 포함되었습니다. 이는 새로운 기능을 출시할 시간을 희생한 것이었지만, 절대적으로 필요한 과정이었습니다. 이는 진지한 AI 에이전트 개발을 위한 기초적인 인프라(Infrastructure) 조각입니다. 이것이 없다면, 여러분은 확률적 시스템(Probabilistic systems)이 조용한 실패(Silent failure)로 표류하지 않기를 바라며 눈을 가린 채 제품을 출시하는 것과 같습니다.
자주 묻는 질문 (Frequently Asked Questions)
Q: 평가 과정에서 LLM의 비결정론적(Non-deterministic) 출력을 어떻게 처리하나요?
A: 요약이나 창의적인 텍스트와 같은 비결정론적 출력의 경우, 정확한 문자열 매칭(String matching) 대신 인간이 정의한 기준 세트에 따라 _품질(Quality)_과 _정확도(Accuracy)_를 평가하기 위해 참조용 LLM (Claude 3.5 Sonnet)을 사용합니다. 평가용 LLM을 위한 프롬프트(Prompt)는 의미론적 정확성(Semantic correctness)과 완전성(Completeness)에 집중하도록 정교하게 설계되었습니다.
Q: 평가를 수행하는 LLM 자체가 실수를 하면 어떻게 하나요?
A: 이는 타당한 우려입니다. 저는 평가를 위해 매우 유능한 LLM (Claude 3.5 Sonnet)을 사용하고, 특히 새로운 테스트 케이스의 경우나 에이전트가 예상치 못하게 실패할 때 평가 LLM의 판단 샘플을 사람이 직접 검토함으로써 이 문제를 완화합니다. 또한 평가 프롬프트는 명시적으로 설계되어 모호성을 최소화합니다.
Q: 131개의 테스트를 위한 테스트 데이터를 어떻게 관리하나요? 모두 수동으로 진행하나요?
A: 초기에는 많은 테스트 케이스가 수동이었습니다. 하지만 변형(예: 패러프레이징 도구, 데이터 증강 (Data Augmentation))을 위한 테스트 케이스 생성을 자동화하기 시작했습니다. 핵심적인 비즈니스 로직의 경우, 사람이 큐레이션한 테스트 케이스가 여전히 필수적입니다. PostgreSQL 데이터베이스를 사용하면 테스트 케이스를 쉽게 쿼리하고 업데이트할 수 있습니다.
Q: 왜 기존의 평가 프레임워크 (Eval Framework)를 사용하지 않나요?
A: 기존 프레임워크들은 너무 범용적이어서 (멀티 에이전트, 도구 사용 시스템을 위해 상당한 커스터마이징이 필요함) 혹은 특정 LLM 제공업체나 사용 사례에 너무 편향되어 있다는 것을 알게 되었습니다. 직접 구축함으로써 저의 특정 에이전트 아키텍처, Oracle Cloud 인프라, 그리고 에이전트 고유의 실패 모드 (Failure Modes)에 맞춤화된 커스텀 평가 로직과 깊이 있게 통합할 수 있었습니다.
Q: 131개의 테스트를 수행하면서 어떻게 실행당 $0.03의 낮은 비용을 유지하나요?
A: 핵심은 지능적인 라우팅 (Intelligent Routing)입니다. 대부분의 초기 에이전트 상호작용과 단순 체크에는 매우 빠르고 비용 효율적인 Groq를 사용합니다. 오직 복잡한 의미론적 평가 (예: 레이어 2 콘텐츠 정확도)만이 Claude 3.5 Sonnet과 같이 더 비싸지만 더 유능한 모델로 라우팅됩니다. 또한 많은 테스트가 로컬 스키마 검증이나 모킹된 도구 호출 (Mocked Tool Calls)을 포함하므로, LLM 비용이 최소화됩니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기