Spring AI Evals: 에이전트 동작을 테스트하는 방법
요약
Spring AI를 사용하여 AI 에이전트의 동작을 검증하는 평가(Evals) 방법론을 소개합니다. 에이전트가 도구를 올바르게 호출하고 의도한 대로 동작하는지 확인하기 위한 성공 기준 설정과 테스트 전략을 다룹니다.
핵심 포인트
- 에이전트 동작을 회귀 테스트(Regression Tests) 관점에서 관리해야 함
- 명확한 성공 기준(Success Criteria) 정의가 평가의 핵심임
- 도구 정의와 시스템 프롬프트를 통해 모델과의 계약(Contract)을 명확히 함
- 도구 호출 여부를 검사하는 결정론적 검사(Deterministic Checks) 레이어 활용
AI 에이전트를 구축할 때, 프롬프트가 제대로 작동하는 것처럼 보이는 순간이 보통 찾아옵니다. 하지만 개발이 계속됨에 따라 이는 빠르게 통제 불능 상태가 될 수 있습니다. 오늘은 에이전트가 올바른 도구(tool)를 호출할 수 있지만, 내일은 작은 시스템 프롬프트(system prompt) 변경 후에 호출을 중단할 수도 있습니다. 나중에는 호출해서는 안 될 때 호출하기 시작할 수도 있습니다.
그래서 저는 평가(evals)를 AI 동작에 대한 일반적인 회귀 테스트(regression tests)로 취급합니다.
제 프로젝트의 예시는 작습니다: Spring AI 에이전트는 create_note 도구를 통해 Markdown 노트를 생성해야 합니다. 작업은 단순해 보이지만, 에이전트가 실패할 수 있는 지점은 이미 여러 군데가 있습니다:
- 사용자가 노트를 생성해 달라고 요청했을 때 도구를 호출하지 않음;
- 사용자가 단순히 무언가를 보여달라거나 설명해 달라고 요청했을 때 도구를 호출함;
- 추가적인 파일을 생성함;
- Markdown 헤딩(heading) 없이 노트를 작성함;
- 추가적인 조언이나 관련 없는 내용을 덧붙임.
그래서 저는 체크 항목을 몇 가지 계층으로 나눕니다.
성공 기준 (Success Criteria)
먼저 올바른 동작이 무엇을 의미하는지 정의합니다.
에이전트는 다음과 같이 동작해야 합니다:
- 사용자가 새 노트를 생성해 달라고 요청할 때
create_note를 호출해야 함; - 사용자가 읽기/삭제/이름 변경/설명을 요청할 때는
create_note를 호출하지 않아야 함; - 오직
note.txt만 생성해야 함; - 첫 번째 줄에 Markdown 헤딩을 넣어야 함;
- 짧고, 관련성이 있으며, 읽기 쉬운 내용을 작성해야 함.
이것이 중요합니다: 명확한 성공 기준이 없다면, 평가는 다시 주관적인 확인 작업으로 변질됩니다.
도구 정의 (Tool Definition)
도구는 의도적으로 좁게 정의되었습니다. 그것은 한 가지 일, 즉 note.txt를 생성하는 일만 합니다.
@Tool(
name = "create_note",
description = "Create a new Markdown note in note.txt. Use this only when the user asks to create a new note."
...
여기서 name과 description은 중요합니다. 이것들은 단순한 문서화가 아닙니다. 모델에게 있어 이것들은 도구가 적합한 시점과 그렇지 않은 시점을 알려주는 신호입니다.
에이전트 지침 (Agent Instructions)
다음으로, 시스템 프롬프트(system prompt)에 프로세스 목표를 명확하게 정의합니다.
return this.chatClient.prompt()
.system("""
When the user asks to create a new note, use the create_note tool.
...
저는 여기서 "아름다운 프롬프트"를 작성하려는 것이 아닙니다. 저는 계약(contract)을 수정하고 있는 것입니다:
- 언제 도구(tool)를 사용할 것인가;
- 어떤 파일이 나타나야 하는가;
- 결과에 무엇이 나타나지 않아야 하는가.
결정론적 검사 (Deterministic Checks)
첫 번째 평가(eval) 레이어는 도구 호출(tool invocation)을 검사합니다. 이는 빠르고 명확한 검사입니다: 도구가 호출되었는가, 아니면 호출되지 않았는가?
@ParameterizedTest
@CsvSource(value = {
"note-create-01|true|Create a new note about my plans for the week",
...
이 테스트는 텍스트 품질에 대해 모델과 논쟁하지 않습니다. 오직 하나의 구체적인 동작을 검사합니다.
테스트가 실패한다면, 그 이유는 명확합니다:
- 거짓 음성 (false negative): 사용자가 노트를 생성해달라고 요청했지만, 도구가 호출되지 않음;
- 거짓 양성 (false positive): 사용자가 파일을 생성해달라고 요청하지 않았는데, 도구가 호출됨.
실패 원인을 이해하기 쉽기 때문에 저는 여기서부터 시작하는 것을 선호합니다.
도구 단위 테스트 (Tool Unit Test)
저는 또한 모델 없이 도구 자체를 테스트합니다. 이것은 일반적인 단위 테스트 (unit test)이며, 안정적이어야 합니다.
@Test
void createNoteWritesOnlyNoteFileWithMarkdownHeading() throws IOException {
var noteTool = new NoteTool(this.tempDir);
...
이를 통해 두 가지 문제를 분리할 수 있습니다:
- 단위 테스트가 깨진다면, 문제는 도구 구현 (tool implementation)에 있습니다;
- 평가 테스트 (eval test)가 깨진다면, 문제는 에이전트 동작 (agent behavior), 프롬프트 (prompt), 또는 모델 출력 (model output)에 있습니다.
루브릭 기반 채점 (Rubric-Based Grading)
결정론적 검사는 사실 관계를 포착하는 데 유용하지만, 모든 것을 단순한 단언 (assert)으로 확인할 수는 없습니다.
예를 들어, "노트가 관련성이 있는가", "추가 정보가 없는가", "텍스트를 읽기 쉬운가" 등은 정성적인 요구사항입니다. 이를 위해 저는 루브릭 기반 채점 (rubric-based grading)을 사용합니다.
Spring AI에서 이는 Evaluator와 잘 맞습니다. Evaluator는 EvaluationRequest를 받고, 내부에서 채점자 프롬프트 (grader prompt)를 실행하며, pass, score, feedback, metadata를 포함한 EvaluationResponse를 반환합니다.
채점자 (grader)의 경우, 모델을 고정된 상태로 유지하며, 가급적 유동적인 별칭 (floating alias) 대신 버전이 명시된 모델 이름을 사용하는 것이 좋습니다. 예를 들어, 저는 gpt-5.5보다 gpt-5.5-2026-04-23을 선호합니다. 만약 제공업체가 모델 별칭을 업데이트하면, 코드 변경 없이도 채점자가 더 엄격해지거나 더 관대해질 수 있습니다. 이 경우 점수의 변화는 에이전트 (agent)의 동작 때문이 아니라 채점자 드리프트 (grader drift)로 인해 발생할 수 있습니다.
또한 채점 시에는 낮은 온도 (low temperature) 설정을 선호합니다. 채점자는 창의적일 필요가 없으며, 동일한 루브릭 (rubric)을 동일한 방식으로 적용해야 합니다.
중요한 루브릭 평가 (rubric evals)의 경우, 채점자를 한 번 이상 실행할 수도 있습니다. 단일 실행 (single pass)도 유용하지만, 채점자 역시 모델인 경우에는 통과율 (pass rate)을 확인하는 것이 더 정직한 지표가 됩니다.
class NoteStyleEvaluator implements Evaluator {
private static final List<String> REQUIRED_CHECK_IDS =
...
여기서 모델은 채점자로 사용되지만, 자유 형식 (free-form) 모드로 사용되지는 않습니다. 결과가 자동으로 확인될 수 있도록 구조화된 출력 (structured output)을 요청합니다.
Spring AI 평가 추상화 (eval abstractions)의 주요 이점은 채점이 일반적인 Java 계약 (contract)이 된다는 점입니다.
EvaluationRequest는 원래의 사용자 프롬프트 (prompt)와 생성된 출력을 유지합니다.Evaluator는 채점 로직을 포함합니다.EvaluationResponse는 기계가 확인할 수 있는 결과를 반환합니다.metadata는 각 루브릭 체크에 대한 세부 정보를 유지합니다.
이를 통해 채점은 CI (지속적 통합)에 유용해집니다. pass, score, feedback, 그리고 체크 리스트가 존재하기 때문입니다.
루브릭 평가 테스트 (Rubric Eval Test)
@ParameterizedTest
@CsvSource(value = {
"weekly-plan-01|Create a new note about my plans for the week. Include a clear title and only a concise list of concrete weekly goals.|80",
...
이 테스트는 단순히 "도구가 호출되었는가" 이상을 확인합니다. 전체 결과물을 확인합니다.
여기서 저는 여전히 에이전트를 한 번만 실행합니다. 오직 채점자만이 동일한 note.txt 콘텐츠에 대해 여러 번 실행됩니다.
이는 채점자의 안정성 (grader stability)만을 측정합니다. 에이전트의 안정성 (agent stability)은 별도의 지표입니다. 이를 위해서는 전체 에이전트 흐름을 여러 번 실행하고 에이전트 통과율을 별도로 추적해야 합니다.
prompt -> agent run -> note.txt -> rubric result -> score
버전 간 결과 저장하기
저는 실행(run) 간의 평가(eval) 결과도 저장합니다. 이는 프롬프트(prompt), 모델(model), 또는 루브릭(rubric) 변경 후의 동작을 비교하는 데 도움이 됩니다.
합격/불합격(Pass/fail)만으로는 항상 충분하지 않습니다. 점수가 서서히 나빠지더라도 테스트는 여전히 합격할 수 있기 때문입니다. 이것이 제가 점수(score), 피드백(feedback), 그리고 루브릭 메타데이터(rubric metadata)를 아티팩트(artifacts)로 유지하는 이유입니다.
var resultPath = Path.of("build", "eval-results", id + ".json");
var resultJson = objectMapper.writeValueAsString(Map.of(
...
이를 통해 합격/불합격 결과, 점수, 피드백, 그리고 현재 실행을 이전 버전과 비교하는 데 필요한 메타데이터를 보존할 수 있습니다.
평가 태스크 (Eval Task)
저는 AI 평가(evals)를 일반적인 테스트와 분리하여 관리합니다. AI 평가는 모델 API를 호출하기 때문에 더 느리고, 비용이 많이 들며, 결정론적(deterministic)이지 않기 때문입니다.
tasks.named('test') {
useJUnitPlatform {
excludeTags 'eval'
...
다음과 같이 별도로 실행하세요:
./gradlew test
./gradlew evalTest
이를 통해 얻는 이점
이 방식은 에이전트(agent) 개발을 더 예측 가능하게 만들고 제어하기 쉽게 해줍니다.
시스템 프롬프트(system prompt), @Tool 설명, 모델 옵션(model options), 또는 도구(tool) 자체를 변경한 후, "더 나아진 것 같은가?"라고 묻는 대신 다음과 같은 구체적인 신호들을 확인할 수 있습니다:
- 오탐(false positives)이 증가했는가;
- 미탐(false negatives)이 나타났는가;
- 예상된 아티팩트(artifact)가 생성되었는가;
- 출력이 루브릭(rubric)을 통과하는가;
- 품질이 최소 점수 미만으로 떨어졌는가.
가장 유용한 점은 평가 스위트(eval suite)를 점진적으로 확장할 수 있다는 것입니다. 새로운 실수(miss)를 발견하면 새로운 평가 케이스(eval case)를 추가합니다. 에이전트가 실제 사용자 프롬프트에서 실패했다면, 그 프롬프트는 회귀 테스트(regression test)가 됩니다. 이런 방식으로 동작 드리프트(behavior drift)를 수동 테스트 속에 숨겨두지 않고 가시화할 수 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기