프롬프트 성능이 저하되면 빌드를 실패시키세요: 프롬프트 회귀 CI를 위한 evalgate
요약
프롬프트 품질 저하를 감지하기 위해 CI 환경에서 프롬프트 회귀 테스트를 수행하는 TypeScript 도구인 evalgate를 소개합니다. 베이스라인 대비 품질 차이를 계산하여 점수가 하락할 경우 빌드를 실패시키는 방식으로 에이전트의 안정성을 보장합니다.
핵심 포인트
- 프롬프트 품질 저하를 빌드 아티팩트처럼 관리하여 CI 단계에서 검증
- 베이스라인 대비 품질 차이(delta)를 기준으로 객관적인 통과 여부 결정
- API 키 없이 오프라인 테스트가 가능한 결정론적 모의 프로바이더 제공
- JSON 스키마, 유사도, LLM Judge 등 10가지 다양한 스코어러 지원
프롬프트는 소리 없이 부패합니다. 모델을 교체하고, 시스템 프롬프트 (system prompt)를 수정하고, 도구 (tool)를 추가해도 모든 것이 여전히 작동합니다. 예외가 발생하지 않고, 테스트가 실패(red)하지 않으며, JSON은 여전히 파싱됩니다. 단지 출력이 조용히 나빠질 뿐이며, 저는 보통 CI가 아닌 사용자로부터 그 사실을 알게 됩니다. 유닛 테스트 (Unit tests)는 여기서 잘못된 도구입니다. 왜냐하면 포착할 수 있는 것이 없기 때문입니다. 실패 모드는 충돌 (crash)이 아니라 품질의 저하입니다.
그래서 저는 프롬프트와 에이전트 (agent)의 품질을 빌드 아티팩트 (build artifact)처럼 취급하는 작은 TypeScript 도구인 evalgate를 만들었습니다. 선언적인 평가 스위트 (eval suite)를 작성하면, evalgate가 이를 실행하고, 점수를 매기고, 베이스라인 (baseline)을 저장합니다. 그리고 모든 풀 리퀘스트 (pull request)마다 스위트를 재실행하여 베이스 브랜치 (base branch) 대비 품질 차이 (delta)를 계산하고, 점수가 회귀 (regress)하면 빌드를 실패시킵니다. 그런 다음 차이 테이블을 PR 코멘트로 게시합니다.
핵심 아이디어
중요한 설계 결정은 CI가 어떤 질문을 던질 수 있게 할 것인가입니다. "이 프롬프트가 좋은가?"라는 질문은 주관적이며 자동화된 게이트 (gate)에서는 승리할 수 없습니다. "이것이 main 브랜치에 있던 것보다 나쁜가?"라는 질문은 객관적이며 답할 수 있습니다. evalgate는 두 번째 질문을 중심으로 구축되었습니다. 베이스라인을 한 번 캡처하면, 그 이후의 모든 변경 사항은 절대적인 선함의 개념이 아니라 베이스라인 대비 차이로 판단됩니다.
두 번째 결정은 전체 과정이 API 키 없이 실행되어야 한다는 것이었습니다. evalgate는 결정론적인 모의 프로바이더 (deterministic mock provider)를 제공하므로, 스위트를 실행하고, 베이스라인을 저장하고, 실행 결과를 비교하며, 전체 테스트 스위트를 완전히 오프라인에서 실행할 수 있습니다. 프로젝트 자체에는 67개의 테스트가 있으며 그 중 어느 것도 네트워크에 접속하지 않습니다. 모든 기능은 완료된 것으로 간주되기 전에 모의 모드 (mock mode)에서 작동해야 합니다.
작동 방식
스위트는 검사할 코드 옆의 버전 관리 시스템에 저장되는 YAML (또는 JSON) 파일입니다. 각 케이스는 입력 (input), 예상 참조 값 (expected reference value), 그리고 하나 이상의 스코어러 (scorer)를 가집니다. 다음은 최소한의 예시입니다:
name: my-agent
provider: mock # API 키 없이 작동
threshold: 0.9 # 통과를 위해 필요한 평균 점수
...
모든 스코어러 (scorer)가 통과하고, 수치 점수가 개별 스코어러 점수의 가중 평균 (weighted mean)일 때 케이스가 통과됩니다. 카탈로그에는 모델 출력에 대해 실제로 검증하고자 하는 다양한 범위를 다루는 10개의 스코어러가 있습니다:
- 문자열 수준의 체크를 위한
exact-match,regex,contains,not-contains(contains는 여러 부분 문자열에 대해 부분 점수를 부여합니다). - 구조화된 출력 (structured output)을 위한
json-schema. 이를 통해 모델이 스키마와 일치하는 유효한 JSON을 반환하는지 검증할 수 있습니다. - 코사인 유사도 (cosine similarity)를 통해 "의미상 충분히 유사함"을 확인하는
embedding-similarity. - 기준 기반의 유연한 판단을 위한
llm-judge및rubric. - 예산 게이트 (budget gates)를 위한
latency및cost. 이를 통해 에이전트를 느리게 만들거나 비용을 발생시키는 변경 사항도 게이트를 통과하지 못하게 할 수 있습니다.
이 중 두 가지 스코어러는 플러그인 방식으로 교체 가능하며 결정론적인 오프라인 폴백 (offline fallback) 기능을 제공하는데, 이것이 mock-first 규칙을 정직하게 유지해 주는 핵심입니다. embedding-similarity는 제공자(provider)의 embed() 기능이 있으면 이를 사용하고, 그렇지 않으면 안정적인 로컬 bag-of-hashed-words 임베딩으로 폴백합니다. llm-judge는 실제 제공자를 호출하여 {score, reason} 형태의 JSON 응답을 파싱하지만, mock 제공자에서는 대신 재현 가능한 단어 중첩 (word-overlap) 점수를 계산합니다. 따라서 judge와 임베딩을 사용하는 스위트 (suite)라도 키(key) 없이 모든 머신에서 동일하게 실행됩니다.
워크플로우는 세 개의 명령어로 구성됩니다. 스위트를 실행하고, 베이스라인 (baseline)을 저장한 다음, 이후의 실행 결과들을 베이스라인과 비교합니다:
npx @royalpinto007/evalgate run suite.eval.yaml
npx @royalpinto007/evalgate baseline suite.eval.yaml --out baseline.json
npx @royalpinto007/evalgate compare suite.eval.yaml --base baseline.json --tolerance 0.01
compare는 어떤 케이스라도 허용 오차 (tolerance) 범위를 벗어나 회귀 (regress)하면 0이 아닌 종료 코드 (non-zero exit)를 반환하며, 이 0이 아닌 종료 코드가 CI 작업을 실패하게 만듭니다. 모델 출력은 완벽하게 안정적이지 않기 때문에 허용 오차가 중요합니다. 일반적으로 무언가를 실제 회귀로 간주하기 전에 약간의 허용 가능한 편차 (drift)를 두고 싶어 하기 때문입니다.
단순한 스크립트가 아닌 CI(지속적 통합)처럼 느껴지게 만드는 핵심 요소는 GitHub Action입니다. 각 Pull Request(PR)마다 테스트 스위트(suite)를 다시 실행하고 단일 댓글을 Upsert(업서트)합니다. 즉, 매 푸시(push)마다 새로운 댓글을 쌓는 대신 기존 댓글을 제자리에서 수정합니다. 회귀(regression) 결과는 다음과 같이 렌더링됩니다.
### evalgate: support-agent
FAIL - Quality regressed. 5 case(s) got worse.
...
머지(merge)하기 전, 리뷰 단계에서 바로 퍼센트 포인트 단위의 전체적인 변화량과 정확히 어떤 케이스가 악화되었는지에 대한 케이스별 상세 내역을 확인할 수 있습니다. 내부적으로는 동일한 로직이 라이브러리로 노출되어 있어, loadSuite, runSuite, compareRuns, renderCompareMarkdown 등을 모두 임포트(import)할 수 있습니다. 따라서 evalgate를 Action 이외의 다른 곳에 연결하고 싶다면 그렇게 할 수 있으며, 스코어러(scorer)와 프로바이더(provider) 모두 등록 가능하므로 사용자 정의 기능을 추가할 수도 있습니다.
솔직한 한계점
evalgate는 점수가 변했다는 사실을 알려줄 뿐, 새로운 점수가 올바른 것인지는 알려주지 않습니다. 만약 프롬프트가 실제로 개선되었는데 참조 기대치(reference expectations)가 이제는 낡은 것이 된 경우라면, evalgate는 여전히 차이(delta)를 표시할 것이며, 베이스라인(baseline)을 업데이트하는 것은 사용자의 몫입니다. 이 게이트(gate)는 변화 감지기(change detector)이지, 신탁(oracle)이 아닙니다. 이는 비교적 유연한 스코어러인 llm-judge와 embedding-similarity에도 해당되는 이야기입니다. 이들은 판사 모델(judge model)과 사용자가 작성한 기준(criteria)만큼만 신뢰할 수 있으므로, 취약한 기준을 바탕으로 통과된(green) 스위트는 잘못된 안전감을 줄 수 있습니다. 저는 엄격하게 다루고 싶은 모든 부분에 대해서는 정확한(exact), 정규 표현식(regex), 스키마(schema) 스코어러에 의존하며, 모델 기반 점수는 정답(ground truth)이라기보다는 방향성을 나타내는 신호로 취급합니다.
마치며
제가 원했던 것은 간단했습니다. 깨진 타입(type)이나 실패하는 테스트와 마찬가지로, 프롬프트 품질 때문에 Pull Request가 실패할 수 있도록 만드는 것이었습니다. evalgate는 선언적 스위트(declarative suite), 베이스라인 및 차이(delta) 엔진, 10개의 스코어러, 그리고 회귀 내용을 인라인으로 댓글 달아주는 GitHub Action을 통해 이를 구현하며, 이 모든 것은 결정론적 모의 프로바이더(deterministic mock provider)를 통해 오프라인에서도 실행 가능합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기