Spring AI 평가(Evals)를 위한 베이스라인: 조용한 성능 저하 포착하기
요약
Spring AI를 사용하여 에이전트의 성능 저하를 감지하기 위한 베이스라인 평가 전략을 소개합니다. 단순 통과 여부를 넘어, 승인된 양호한 결과(Baseline)와 현재 실행 결과를 비교함으로써 미세한 품질 하락을 포착하는 방법을 다룹니다.
핵심 포인트
- 단순 임계값 통과 방식은 미세한 성능 저하를 놓칠 수 있음
- 베이스라인은 명시적으로 승인된 양호한 결과값으로 정의됨
- CI 환경을 위해 베이스라인과 실행 결과를 분리하여 관리 권장
- 점수, 루브릭 통과 여부, 피드백 등 다양한 신호를 비교 대상으로 활용
Part 1: https://dev.to/lbobylev/spring-ai-evals-how-i-test-agent-behavior-2pgj
첫 번째 파트에서 평가(eval) 스위트는 두 가지 질문을 확인했습니다:
- 에이전트가
note.txt를 생성했는가; - 결과가 루브릭(rubric) 기반 채점을 통과했는가.
이는 이미 수동 확인보다 나은 방식이지만, 이 접근 방식에는 사각지대가 있습니다. 테스트는 통과하더라도 품질은 서서히 나빠질 수 있기 때문입니다.
예를 들어, 이 프로젝트에는 다음과 같은 평가(eval) 케이스가 있습니다:
"weekly-plan-01|이번 주 계획에 대한 새로운 노트를 작성하세요. 명확한 제목을 포함하고 구체적인 주간 목표를 간결한 목록으로만 작성하세요.|80"
만약 채점기(grader)가 score = 91을 반환하면 테스트는 통과합니다. 시스템 프롬프트(system prompt) 변경 후 점수가 82가 되어도, 최소 점수가 80이기 때문에 테스트는 여전히 통과합니다. 하지만 에이전트의 동작은 악화된 것입니다.
이것이 바로 베이스라인(baseline)이 도움이 되는 지점입니다.
베이스라인이란 무엇인가
베이스라인(baseline)은 평가(eval) 케이스에 대해 마지막으로 승인된 양호한 결과입니다.
중요: 이는 단순히 이전 실행(run) 결과가 아닙니다. 이전 실행은 나빴을 수도 있고, 무작위였을 수도 있으며, 실험 중에 생성되었을 수도 있습니다. 베이스라인은 제가 명시적으로 수용 가능하다고 간주하며 비교 지점으로 사용하고자 하는 결과입니다.
이 프로젝트에서는 평가(eval) 케이스 id별로 베이스라인을 저장하는 것이 편리합니다:
src/test/resources/eval-baselines/weekly-plan-01.json
src/test/resources/eval-baselines/weekly-plan-02.json
src/test/resources/eval-baselines/weekly-plan-03.json
그리고 각 실행(run)의 현재 결과는 별도로 작성합니다:
build/eval-results/weekly-plan-01.json
build/eval-results/weekly-plan-02.json
build/eval-results/weekly-plan-03.json
이를 통해 두 가지를 분리합니다:
src/test/resources/eval-baselines- git에 유지할 수 있는 승인된 비교 지점;build/eval-results- CI 작업에 첨부하기 유용한 실제 실행(run)에 대한 아티팩트(artifacts).
무엇을 비교할 것인가
에이전트 평가(eval)를 위해, 저는 신호(signals)를 비교합니다:
pass- 루브릭 (rubric) 통과 여부;score- 수치화된 채점 점수 (grader score);checks- 어떤 루브릭 체크 (rubric checks) 항목이 통과하거나 실패했는지;feedback- 채점자(grader)의 설명;noteContent- 실제 생성된note.txt내용으로, 실패 원인을 빠르게 이해할 수 있게 함.
아래의 간단한 버전에서, 회귀 게이트 (regression gate)는 score와 pass만을 사용합니다. 개별 체크 결과, feedback, 그리고 noteContent는 진단을 위해 유지됩니다.
유용한 다음 확장 단계는 개별 루브릭 체크에 대해서도 게이트를 설정하는 것입니다. 예를 들어, 베이스라인 (baseline)에서 simplicity가 통과했다면, 현재 실행(run)은 평균 점수가 여전히 충분히 높다는 이유만으로 simplicity에서 실패하는 것이 허용되어서는 안 됩니다.
최소 결과 모델 (Minimal result model)
현재 프로젝트에서 NoteStyleRubricTests는 단일 EvaluationResponse를 받습니다. 베이스라인 메커니즘을 위해서는, 먼저 결과를 JSON으로 저장하기 쉬운 커스텀 레코드 (custom record)로 감싸야 합니다.
record NoteEvalResult(
String id,
String prompt,
...
채점자(grader)가 한 번만 실행된다면 이것으로 충분합니다. 나중에 여러 번의 채점자 실행을 추가하게 된다면, 해당 레코드는 averageScore, passingRuns, graderRuns, 그리고 모든 응답 리스트를 포함하도록 확장될 수 있습니다.
현재 결과 저장하기
먼저, 현재 결과는 항상 build/eval-results에 저장되어야 합니다. 베이스라인 비교가 실패하더라도, 아티팩트 (artifact)는 디스크에 남아 CI에서 검사할 수 있습니다.
private final ObjectMapper objectMapper = new ObjectMapper()
.enable(SerializationFeature.INDENT_OUTPUT);
...
이렇게 하면 현재 실행에 대한 스냅샷 (snapshot)이 저장됩니다.
베이스라인과 비교하기
이제 동일한 id에 대한 베이스라인을 로드하여 품질이 허용된 임계값 (threshold) 이상으로 떨어지지 않았는지 확인할 수 있습니다.
private void assertNoRegressionAgainstBaseline(NoteEvalResult current) throws IOException {
var baselinePath = Path.of(
"src",
...
여기에는 중요한 세부 사항이 있습니다. 베이스라인 (baseline) 파일이 존재하지 않더라도 테스트는 실패하지 않습니다. 이는 새로운 평가 (eval) 케이스를 추가할 때 유용합니다. 먼저 케이스를 추가한 다음, 여러 번 실행한 후 결과를 수락하고 베이스라인 JSON을 추가합니다.
테스트에서의 모습
현재의 gradesCreatedNoteWithStyleRubric는 테스트의 핵심 아이디어를 변경하지 않고도 확장될 수 있습니다.
@ParameterizedTest(name = "{0}")
@CsvSource(value = {
"weekly-plan-01|이번 주 계획에 대한 새로운 노트를 작성하세요. 명확한 제목을 포함하고 구체적인 주간 목표 목록만 간결하게 작성하세요.|80",
...
순서는 의도된 것입니다:
- 먼저 에이전트 (agent) 실행;
- 그다음 채점 (grading);
- 그다음 현재 결과 아티팩트 (artifact) 작성;
- 그다음 베이스라인 (baseline)과의 비교;
- 그다음 최소 점수에 대한 통상적인 단언 (assertions).
베이스라인 비교가 실패하더라도, build/eval-results/<id>.json은 이미 작성된 상태입니다.
베이스라인 JSON 예시
src/test/resources/eval-baselines/weekly-plan-01.json 파일은 다음과 같이 보일 수 있습니다:
{
"id" : "weekly-plan-01",
"prompt" : "이번 주 계획에 대한 새로운 노트를 작성하세요. 명확한 제목을 포함하고 구체적인 주간 목표 목록만 간결하게 작성하세요.",
...
저는 정확한 noteContent를 단언 (assert)하지 않습니다. 이는 진단 정보로서 베이스라인에 저장됩니다. 만약 점수가 떨어진다면, 에이전트가 무엇을 쓰기 시작했는지 빠르게 확인하고 싶기 때문입니다.
베이스라인을 업데이트해야 하는 시점
베이스라인은 매 실행 후 자동으로 업데이트되어서는 안 됩니다.
만약 자동으로 업데이트된다면, 그것은 더 이상 "마지막으로 좋았던 결과"가 아니라 단지 "최신 결과"가 되어버립니다. 그러면 나쁜 결과가 새로운 정상(normal)이 되어버리기 때문에, 다음 실행 시 회귀 (regression) 현상이 사라지게 됩니다.
올바른 워크플로우는 다음과 같습니다:
1. 프롬프트 (prompt), 도구 (tool) 설명, 모델 옵션, 또는 루브릭 (rubric)을 변경합니다.
2. ./gradlew evalTest를 실행합니다.
3. build/eval-results에 있는 실패 사례와 JSON 아티팩트를 검사합니다.
...
베이스라인 업데이트는 명시적인 엔지니어링 결정이어야 합니다.
비결정론 (nondeterminism) 처리
단일 채점자(grader) 실행은 노이즈가 발생할 수 있습니다. 간단한 데모라면 괜찮지만, 더 진지한 평가 스위트(eval suite)를 위해서는 하나의 점수 대신 여러 번의 채점자 실행을 사용하는 것이 좋습니다.
그다음 베이스라인은 집계된 결과(aggregate result)를 저장합니다:
record NoteEvalAggregateResult(
String id,
String prompt,
...
그러면 비교 방식이 더 유연해집니다:
private void assertNoRegressionAgainstBaseline(NoteEvalAggregateResult current) throws IOException {
var baselinePath = Path.of(
"src",
...
이는 AI 평가(evals)의 본질을 더 잘 반영합니다. 한 번의 실행은 우연히 더 약하게 나타날 수 있지만, 평균 점수나 통과율(pass rate)이 지속적으로 떨어진다면 그것은 성능 저하(regression)입니다.
이 프로젝트에서 베이스라인이 제공하는 것
현재 Spring AI 에이전트의 경우, 베이스라인은 일반적인 minimumScore가 포착하지 못하는 질문들에 답해줍니다:
- 노트가 여전히 루브릭(rubric)을 통과하긴 하지만, 이전보다 나빠졌는가?
- 에이전트가 추가적인 조언을 덧붙이기 시작했는가?
- 시스템 프롬프트(system prompt) 변경 후 가독성(readability)이 나빠졌는가?
- 채점자가
simplicity(단순성)에 대해 더 자주 불만을 제기하기 시작했는가? - 다른 케이스를 수정한 후, 특정 평가 케이스가 악화되었는가?
결국, 평가 스위트는 현재의 품질을 점검하고, 베이스라인은 변화의 방향을 보여줍니다. 이들은 서로 다른 신호이며, 에이전트의 동작을 파악하기 위해서는 두 가지 모두가 필요합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기