AI 기능에는 '테스트 통과'의 순간이 없다. 그래서 나는 평가(Eval)를 먼저 작성한다.
요약
AI 기능 개발 시 프롬프트 엔지니어링에 의존하기보다 평가(Eval)를 먼저 설계하여 성공의 정의를 명확히 해야 합니다. 평가는 단순한 QA를 넘어, 확률적인 모델의 한계를 극복하고 검색(Retrieval) 아키텍처를 개선하도록 유도하는 설계 도구로 활용되어야 합니다.
핵심 포인트
- AI 기능 개발 시 '완료'에 대한 명확한 평가(Eval) 정의가 필수적임
- 평가는 단순 품질 보증이 아닌 시스템의 명세(Specification) 역할을 수행함
- 측정 가능한 실패 조건은 프롬프트 개선을 넘어 아키텍처 변화를 강제함
- 확률적인 LLM의 한계를 극복하기 위해 검색(Retrieval) 단계에서 제어 필요
나는 "이 책에 대해 질문하기(Ask This Book)" 기능을 만들고 있었다. 독자들이 책을 읽는 동안 책에 대해 질문할 수 있는 기능이다.
한 가지 요구사항은 단순해 보였다:
3장을 읽고 있는 독자가 30장의 스포일러를 받아서는 안 된다.
나의 첫 번째 본능은 다른 모든 사람들과 같았다. 모델에게 미래의 장을 스포일러하지 말라고 말하는 것이었다. 다음과 같이 말이다:
"독자가 아직 도달하지 않은 장의 정보를 공개하지 마세요."
그리고 솔직히 말해서, 그것은 대부분 잘 작동했다.
문제는 "대부분"이라는 말은 쓸모가 없다는 것이다. 사용자는 단 한 번의 스포일러만으로도 충분하기 때문이다.
그 순간 나는 이 기능에 '완료(done)'에 대한 정의가 없다는 것을 깨달았다.
일반적인 소프트웨어에서는 무언가가 저항한다. 컴파일러가 불평하고, 테스트가 실패하며, 타입(types)이 일치하지 않는다.
LLM 기능에서는 그 중 어느 것도 일어나지 않는다. 출력값은 기본적으로 그럴듯해 보인다. 틀렸을 때조차 유창하고, 자신감 있으며, 형식이 잘 갖춰져 있다.
그래서 "데모에서 괜찮아 보였다"라는 말이 조용히 결승선이 되어버린다.
그것이 바로 내가 기능을 작성하기 전에 평가(eval)를 먼저 작성하는 이유다.
평가는 명세(Specification)이다
대부분의 팀은 평가(eval)를 QA(품질 보증)로 취급한다. 기능을 만들고, 작동하는 것을 출시한 뒤, 나중에 평가를 추가한다.
나는 점점 그것이 거꾸로 되어 있다고 생각한다. AI 시스템의 경우, 평가는 종종 성공에 대한 유일하고 구체적인 정의가 된다.
스포일러 평가(eval)를 작성하는 순간, 나는 실패를 정의해야 했다. 스포일러 유출은 반드시 '0'이어야 한다. 낮아서도 안 되고, 허용 가능한 수준이어도 안 된다. 반드시 0이어야 한다.
그리고 그 요구사항은 즉시 문제를 드러냈다. 어떤 프롬프트(prompt)도 0을 보장할 수 없다.
프롬프트는 확률적이다. 사용자는 질문을 다르게 표현할 수 있다. 모델은 지침을 다르게 해석할 수 있다. 향후 모델 업데이트는 다르게 동작할 수 있다. 부드러운 지침(soft instruction)으로부터 확고한 보장(hard guarantee)을 얻을 수는 없다.
평가가 아키텍처를 바꾸었다
평가(eval)가 스포일러 0을 요구하게 되자, 해결책은 더 이상 프롬프트의 문제가 아니게 되었다. 그것은 검색(retrieval)의 문제가 되었다.
모델에게 미래의 장을 공개하지 말라고 말하는 대신, 미래의 장이 컨텍스트(context)에 아예 들어오지 못하도록 차단했다:
WHERE chapter_ord <= @maxChapterOrd```
독자의 진행 상황(progress)을 벗어난 그 어떤 것도 검색 세트(retrieval set)에 들어오지 않습니다. 모델은 본 적이 없는 정보를 유출할 수 없습니다.
그리고 이를 확인하는 평가(eval) 또한 매우 단순합니다. 독자의 진행 상황을 넘어선 검색된 청크(chunk)는 곧 유출(leak)입니다:
```csharp
// 독자의 진행 상황을 넘어선 단 하나의 검색된 청크 = 하나의 스포일러 유출.
public static int LeakCount(IEnumerable<RetrievedChunk> retrieved, int gateChapterOrd) =>
retrieved.Count(c => c.ChapterOrd > gateChapterOrd);
```
적대적 테스트 케이스(adversarial test cases) 전반에 걸쳐 이 수치는 반드시 0이어야 합니다. 그 순간 아이디어가 제게 완전히 와닿았습니다. 평가는 설계를 테스트하는 것이 아니라, 설계를 만들어내고 있었습니다.
측정 가능한 실패 조건이, 만약 제가 프롬프트 엔지니어링 (prompt engineering)부터 시작했더라면 구축했을 것보다 더 나은 아키텍처 (architecture)를 강제했습니다.
## 검색 품질 (Retrieval Quality)에서도 동일한 일이 일어났다
스포일러 방지 요구사항이 유일한 평가(eval)는 아니었습니다. 저는 기능을 구축하기 전에 두 가지 다른 목표를 정의했습니다:
- 검색(Retrieval)은 결과의 상단 근처에 정확한 구절을 노출해야 한다.
- 답변은 인용한 구절에 근거(grounded)해야 한다.
이러한 요구사항들은 측정 가능했기 때문에, 모든 변경 사항은 의견(opinion)이 아닌 판결(verdict)을 받았습니다.
단일 시맨틱 검색 (semantic search)만으로는 기준을 통과하지 못했습니다. 그래서 결국 두 가지 검색 접근 방식을 결합했습니다:
- 시맨틱 유사성 (semantic similarity)을 위한 벡터 검색 (vector search)
- 정확한 이름, 문구 및 인용구를 위한 전체 텍스트 검색 (full-text search)
결과는 상호 순위 결합 (Reciprocal Rank Fusion, RRF)을 사용하여 융합됩니다. 들리는 것보다 그리 신비롭지는 않습니다. 각 청크는 자신이 나타나는 리스트들 전체에 대해 1/(k+rank)의 합계 점수를 받으므로, 두 검색기 모두에서 높은 순위를 받은 항목은 상단으로 떠오르게 됩니다:
```
// 벡터와 어휘(lexical) 검색 모두에서 높은 순위를 받으면 -> 상단으로 떠오름.
scores[item] += 1.0 / (k + i + 1); // i는 0부터 시작하며, RRF 순위는 1부터 시작함
```
제가 하이브리드 검색 (hybrid retrieval)을 선택한 것은 그것이 유행이기 때문이 아닙니다. 그것이 수치(number)를 움직였기 때문에 선택했습니다. 평가는 시스템이 충분히 좋지 않다고 말했습니다. 시스템이 충분해질 때까지 아키텍처를 변경했습니다.
## 스택 (Stack)에 관한 참고 사항
이 모든 것이 의존성 없는 기술력을 과시하기 위한 것은 아닙니다. 근거 제시(Grounding)를 점수화하는 판정관(Judge)은 Microsoft.Extensions.AI.Evaluation의 커스텀 평가기(Custom Evaluator)입니다.
```
public sealed class RubricEvaluator(string id, Rubric rubric) : IEvaluator
```
저는 의도적으로 Microsoft 스택에 의존합니다. 제가 직접 구현(Hand-rolled)하는 부분은 품질을 결정하는 부분, 즉 검색(Retrieval), 융합(Fusion), 스포일러 게이트(Spoiler gate)입니다. 제가 긋는 선은 "라이브러리를 쓰지 않겠다"가 아닙니다. 그것은 시스템이 실제로 작동하는지 여부를 결정하는 핵심 요소들을 숨겨버리는 에이전트 프레임워크(Agent framework)를 쓰지 않겠다는 것입니다.
## 평가 우선 개발 (Eval-First Development)
전통적인 소프트웨어 개발은 컴파일러, 타입 시스템(Type systems), 단위 테스트(Unit tests), 통합 테스트(Integration tests) 덕분에 거의 공짜로 확신을 얻을 수 있습니다.
AI 시스템은 그렇지 않습니다. 어려운 점은 기능을 구현하는 것이 아닙니다. 어려운 점은 무엇이 "정답"인지를 정의하는 것입니다.
그렇기 때문에 저는 평가 우선 개발(Eval-first development)을 AI 버전의 TDD(테스트 주도 개발)라고 점점 더 생각하게 됩니다. 전통적인 소프트웨어에서 테스트는 구현을 검증합니다. AI 시스템에서 평가는 종종 구현을 정의합니다.
기능을 먼저 만들고 나중에 평가를 만들면, 평가는 이미 만들어진 것에 대해서만 점수를 매길 수 있을 뿐입니다. 평가를 먼저 만들면 평가가 시스템 자체를 형성하기 시작합니다.
평가는 '완료(Done)'의 기준을 정의합니다. 성능이 퇴보(Regression)했을 때를 알려줍니다. 그리고 때로는 원래 생각했던 것보다 더 나은 아키텍처를 강제하기도 합니다.
그렇지 않으면 당신은 기능을 출시하는 것이 아니라, 단지 데모가 잘 되었을 뿐인 추측을 출시하는 것이 됩니다.
_평가(Evals)에 대해 더 깊이 알고 싶으신가요? .NET에서 프로덕션 AI를 구축하는 방법에 대해 더 실무적인 별도의 시리즈를 작성했습니다: [평가가 실제로 무엇인지](https://vasyl.blog/2026/06/10/what-are-ai-evals/), [오류 분석(Error analysis)](https://vasyl.blog/2026/06/10/error-analysis-for-evals/), [골든 데이터셋(Golden datasets)](https://vasyl.blog/2026/06/10/golden-datasets-that-dont-lie/), [판정관으로서의 LLM(LLM-as-judge)](https://vasyl.blog/2026/06/10/llm-as-judge-done-right/), 그리고 [CI 및 프로덕션에서의 평가(Evals in CI and production)](https://vasyl.blog/2026/06/10/evals-in-ci-and-production/). 이 포스트는 원래 [제 블로그](https://vasyl.blog/2026/06/17/evals-before-rag/)에 게시되었습니다._
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기