Skip to content
Park Hyoin PARKHYO.IN
Go back

Eval 공부 #2 — 유사도 기반 평가의 함정 · 테스트셋 5원칙 · '회귀테스트' 라는 용어 오해

Edit page

Eval 공부 #1 (정확도 + LLM-as-Judge) 에 이어서 오늘은 유사도 기반 평가 + 테스트셋 설계 원칙.

Table of contents

Open Table of contents

세 가지 평가의 자리

이전에 정리한 두 가지 축을 다시 놓으면:

방식언제 쓰나
정확도 기반정답이 명확 할 때 (도구 선택, 분류)
LLM-as-Judge정답이 딱 정해지지 않은 주관적 평가
유사도 기반정답은 있지만 표현이 다양 할 때

오늘 다루는 건 세 번째 — 예상 정답과 실제 출력을 임베딩 유사도로 비교 하는 방법.

실험 시작 — 그리고 임베딩 모델 선택의 감

예제 코드를 따라치다가 임베딩 모델을 text-embedding-3-small 로 쓰는 걸 발견했다. 이전 RAG 글에서 확인했던 문제 가 떠올랐다 — 이 모델은 영어에선 top-K 를 잘 뽑지만 한국어는 유사도 절대값이 낮고 분별력도 낮다.

이런 경험이 쌓이면 코드를 그대로 따라치기 전에 “여기서 뭘 바꿔야 할지” 가 보인다. 최근 AI 로 코드를 받아 쓰는 흐름에서 시니어 개발자의 값어치가 오히려 커지는 건, AI 가 확률적으로 정답에 가까운 답을 뱉을 때 본인 상황에 맞춰 어떤 지점을 조정해야 할지를 아는 감각 이 중요해지기 때문일 것 같다. “막연히 더 공부해야지” 가 아니라 여기서 처음 구체적인 방향이 하나 보인다.

다만 — 교체 전에 수치화

“내 기억에 더 안 좋았던 것 같은데” 가 아니라 실제로 두 모델이 어떤 차이를 보이는지 숫자로 확인 부터 한다. Eval 배우면서 가장 먼저 익힌 원칙 그대로.

두 임베딩 모델 비교 — 예상 못 한 발견

text-embedding-3-small (OpenAI) vs bge-m3 (Ollama) 로 같은 질문-답변 쌍을 임베딩해서 유사도 계산.

OpenAI vs BGE-M3 유사도 비교 표 — 같은 의미 통과 케이스 0.4911 / 0.5599, 틀린 답 탈락 케이스 0.5597 / 0.8182, 같은 의미 통과 케이스 0.2125 / 0.5286

두 모델 다 “틀린 답” 에 가장 높은 점수를 매겼다. OpenAI 0.5597 / BGE-M3 0.8182. 같은 의미의 정답 은 오히려 더 낮게 (0.4911 / 0.5599).

유사도 기반 평가의 근본적 한계

이유를 뜯어보면 명확하다:

유사도는 “답변의 내용” 이 아니라 “주제·표현의 근접성” 을 잡는다.

같은 질문에 대해 “틀렸지만 관련 주제 어휘를 많이 담은 답변” 이 “맞았지만 다른 표현으로 축약한 답변” 보다 높게 나올 수 있다. 임베딩은 어휘·구조의 의미 공간 거리이지 논리적 참·거짓이 아니라서 그렇다.

재현 + 분리 실험

방금 결과가 우연인지 확인하려고 몇 가지 케이스를 더:

재현·분리 실험 — 자기 자신 1.0/1.0, 완전 무관 0.0862/0.2724, 재현 (틀린 답) 0.5597/0.8182, 분리 (틀린 내용+단어 안 겹침) 0.1753/0.5102, 분리 (맞는 내용+단어 겹침) 0.8182/0.9557

관찰:

  • 자기 자신: 1.0 (당연)
  • 완전 무관: 0.086 / 0.272 (기대: 낮음 ✅)
  • 재현 (아까 틀린 답): 0.560 / 0.818 (재현됨)
  • 분리 (틀린 내용 + 단어 안 겹침): 0.175 / 0.510 (낮음 ✅ 잘 잡음)
  • 분리 (맞는 내용 + 단어 겹침): 0.818 / 0.956 (높음 ✅ 잘 잡음)

단어 자체가 겹치는지 여부가 유사도를 압도적으로 결정한다. “틀렸지만 관련 어휘 나열” 이 왜 높은 점수 받았는지 여기서 명확해진다.

두 모델 우열은 지금 데이터로 판단 불가

OpenAI 쪽 점수 range 가 더 넓어서 (0.09 ~ 0.82) 분별력 있어 보이지만, bge-m3 는 영점 자체가 높아서 (0.27 ~ 0.96) 점수대가 전반적으로 위 로 붙는 것뿐이다. 어느 쪽이 더 나은지는 이 표만으로는 못 정한다 — 정확한 평가는 별도 벤치마크가 필요하다.

실전 예제 — 4개 전부 통과 실패 (그러나 진짜 문제는 형식)

다시 예제로 돌아가서 4개 질문에 대한 답변을 유사도 기반으로 평가:

평가 결과 — Prompt Caching / MCP 관련 4개 질문, 유사도 0.44 / 0.60 / 0.72 등. 통과율 0/4 = 0.0%, 평균 유사도 0.5937. 마크다운 제목·볼드가 유사도를 떨어뜨림

통과율 0/4 라 대참사인 줄 알았는데 자세히 보니:

  • 기준 점수 (threshold) 를 못 넘었을 뿐, 실제로는 1순위 답변이 다 맞는 방향
  • LLM 응답이 마크다운 제목 형식, 볼드, 줄바꿈 을 포함해서 임베딩 벡터 방향이 기준 정답과 어긋남
  • 내용이 맞아도 형식만 달라도 유사도가 떨어진다

그래서 실전 표준 — 유사도 + LLM-as-Judge 병행

유사도만 쓰면:

  • 잘 맞아도 형식 차이로 실패 처리
  • 관련 어휘만 많은 오답을 통과시킴

유사도 + LLM-as-Judge 병행이 실전 표준. 유사도로 1차 필터 + LLM 이 내용 논리성 최종 판단.

테스트셋 설계 — 5원칙

Eval 이 아무리 잘 짜여도 테스트셋 자체가 편향 이면 아무 소용 없다. 5원칙:

1. 실제 사용 분포 반영

실제 사용 환경에서 나올 법한 질문들을 다양하게 갖춘다. 개발 편의로 만든 인위적 케이스만 쌓이면 production 성능을 예측할 수 없다.

2. Edge Case 포함

정상 케이스만 있으면 안 된다. 반드시 포함:

  • 존재하지 않는 데이터 요청
  • 모호한 질문
  • 범위 밖 질문
  • 프롬프트 인젝션 시도

3. 난이도 계층화

Easy / Medium / Hard 로 나눠서 관리한다. 계층별로 봐야 개선할 부분이 명확히 보인다 — “easy 100%, medium 60%, hard 20%” 같은 층별 점수가 나오면 어디를 파야 할지 명확해진다.

4. 크기는 작게 시작

처음부터 10,000개 같이 완벽하게 만들지 않는다. 먼저 어느 정도 테스트 가능한 규모 로 확보한 후, 틀린 케이스를 발견할 때마다 추가한다. 이게 살아있는 평가셋이다.

5. 정답 기준을 명확하게

  • 꼭 사용해야 할 도구
  • 꼭 포함해야 할 단어
  • 포함하면 안 되는 단어

명시적으로 문서화한다. “이 정도면 맞다” 같은 모호한 기준은 평가자가 매번 흔들린다.

용어 정정 — ‘회귀 테스트’ 가 아니었다

이후에 description 을 대충 쓴 V1명확하게 쓴 V2 를 비교하는 예제를 진행했다. 처음엔 이걸 “회귀 테스트” 라고 불렀는데 — 내가 아는 회귀 테스트는 다른 개념이라 이상해서 다시 물어봤다.

LLM 답변 — regression_eval.py 에서 한 것은 V1(70%) vs V2(90%) A/B 비교. 이건 엄밀히 말하면 회귀 테스트가 아니라 A/B 비교 또는 개선 검증(improvement validation)

답변:

회귀 테스트 (Regression Test) — 기존 코드에 수정이 생겼을 때, 수정 전보다 성능이 나빠지지 않았는지 확인

개선 검증 / A/B 비교 (Improvement Validation) — 두 버전을 같은 테스트셋으로 재서 어느 쪽이 더 나은지 판정

내가 이해하던 회귀 테스트가 맞고, 예제 코드는 이름을 잘못 붙인 케이스였다. 용어 하나 헷갈리면 실제 운영에서 소통 오류가 난다.

V1 vs V2 결과

V1 vs V2 비교 — V1 전체 10/10 100%, V2 도 10/10 100%. easy/medium/hard 전부 100%. 개선폭 +0.0%p. regression_results.json 저장

이번 예제는 description 이 대충이든 명확하든 도구 선택 정확도가 100% / 100% 다. 예제 자체가 쉬웠다. 실전에서 description 이 구체적일수록 어려운 문제에 강해진다 는 원리를 확인하기엔 부족한 데이터다.

다만 “측정 → 개선 → 재측정” 사이클이 어떻게 굴러가는지 흐름은 손에 잡혔다.

회고

오늘 학습으로 얻은 것 세 가지:

  1. 유사도 기반 평가의 함정 — 내용이 아니라 어휘·표현의 근접성. 마크다운 형식 하나로 통과율 0/4 나오는 게 실제로 관찰됐다. 실전은 유사도 + LLM-as-Judge 병행이 답이다.
  2. 테스트셋 5원칙 — 사용 분포 / Edge Case / 난이도 계층 / 작게 시작 / 정답 기준 명확. 살아있는 평가셋 이 핵심이다.
  3. 용어 확인의 중요성 — “회귀 테스트” 처럼 자주 쓰는 용어일수록 예제 코드 따라칠 때 검증이 필요하다. 이걸 그대로 넘어갔으면 나중에 팀에서 소통 오류로 이어진다.

더 공부해볼 것

1. 임베딩 모델 벤치마킹 — 정량 비교

  • 오늘은 케이스 5~7개로 감만. 정식 벤치마크는 KLUE-STS, MTEB (Massive Text Embedding Benchmark) 같은 데이터셋으로
  • OpenAI text-embedding-3-small/large vs bge-m3 vs KoSimCSE vs multilingual-e5 를 한국어 실무 태스크에서 비교
  • 영점 (baseline similarity) 을 정규화한 후 비교하는 방법 (z-score normalization)

2. 유사도 + LLM-as-Judge 병행 파이프라인

  • 1차 유사도 필터 임계값 결정 (너무 관대하면 오답 통과 / 너무 엄격하면 정답 형식 차이로 탈락)
  • 2차 LLM-as-Judge 에 유사도 점수 자체를 컨텍스트로 넘기는 게 유용한지
  • RAGAS / DeepEval 등이 이 조합을 어떻게 구현하는지

3. 형식 정규화 (Format Normalization)

  • 마크다운 → plain text 변환 후 임베딩
  • 볼드/줄바꿈/제목 문법 제거
  • 이걸 하면 유사도 점수가 얼마나 안정화되는지 실측

4. Edge Case 카탈로그

  • “존재하지 않는 데이터 요청” — LLM 이 환각으로 답하는지 아니면 “자료 없음” 응답하는지
  • “프롬프트 인젝션” — 시스템 프롬프트 무력화 시도
  • “다국어 혼입” — 한국어 질문에 영어로만 답하는지
  • 도메인별로 자주 나오는 edge case 카탈로그화

5. RAG 시스템에 실제 Eval 적용

  • 이번에 만든 FEMS RAG 에 오늘 배운 유사도 + LLM-as-Judge 병행 파이프라인 얹어보기
  • 검색 (Hit Rate @ K, MRR) 과 생성 (Faithfulness, Answer Relevance) 을 분리 측정
  • 측정 → 개선 (청킹 / 임베딩 / 프롬프트) → 재측정 사이클 실제로 굴려보기

6. 백엔드 / WAS 이해

  • Eval 시스템이 CI 에 들어가려면 결국 서버 형태로 굴러가야 한다
  • Django / FastAPI / Node 기반 WAS 구축 감각
  • Eval 결과를 대시보드로 보는 방식 (Grafana / 자체 UI)
  • 임베디드 시절 익힌 시스템 감각 위에 서버 사이드가 얹히는 그림

Edit page