거짓말하지 않는 평가(Evals) 구축하기: 프로덕션 환경에서 AI 평가를 신뢰할 수 있게 만드는 방법
요약
프로덕션 환경에서 LLM의 출력값을 신뢰할 수 있는 평가(Evals) 시스템을 구축하는 방법을 다룹니다. 구조화된 출력(Structured Output)과 엄격한 스키마 검증을 통해 환각을 방지하고 데이터의 정확성을 확보하는 엔지니어링 전략을 제시합니다.
핵심 포인트
- 자유 형식 텍스트 대신 함수 호출과 엄격한 스키마를 사용하여 구조화된 출력을 확보해야 함
- Zod와 같은 라이브러리를 활용해 LLM 응답을 검증함으로써 환각이 데이터베이스에 유입되는 것을 방지
- 형식적 유효성을 넘어 의미론적 드리프트를 탐지하기 위한 추가적인 평가 전략이 필요함
- 신뢰 수준(Confidence level) 수치에만 의존하지 말고 실제 출력값을 무작위로 검증하는 과정이 필수적임
제가 대규모 채용 공고 배치에 대해 LLM 스코어링 파이프라인 (LLM scoring pipeline)을 처음 실행했을 때, 결과는 서류상으로 매우 훌륭해 보였습니다. 모든 공고에는 점수가 있었고, 모든 점수에는 신뢰 수준 (confidence level)이 있었습니다. 수치들도 잘 분포되어 있었습니다. 한동안은 그것에 대해 만족했습니다.
그러다 무작위로 몇 개의 출력값을 확인해 보았습니다. 대부분이 틀렸습니다. LLM은 관련 없는 공고에 높은 점수를 주고, 완벽하게 일치하는 공고에는 낮은 점수를 주었으며, 데이터의 카테고리 전체를 날조했습니다. 신뢰 수준은 아무런 의미가 없었습니다. 시스템이 대규모로, 아주 자신 있게 틀리고 있었던 것입니다.
그것이 바로 평가의 함정 (eval trap)입니다. 파이프라인을 구축하고, 실행하고, 수치를 생성하면 작업이 끝났다고 생각합니다. 하지만 그렇지 않습니다. AI 출력값을 평가하는 것 자체가 하나의 엔지니어링 과제이며, 이를 사후 고려 사항으로 취급한다면 당신의 AI 기능은 고장 난 채로 출시될 것입니다.
저는 LLM 스코어링 파이프라인을 통해 매일 10,000개 이상의 공고를 처리하는 트래픽이 높은 채용 게시판을 포함하여, 한동안 프로덕션 AI 시스템을 구축해 왔습니다. 진실을 말해주는 평가를 만드는 것에 대해 제가 배운 점은 다음과 같습니다.
구조화된 출력 (Structured Output)은 당신의 첫 번째 방어선입니다
대부분의 팀이 저지르는 첫 번째 실수는 LLM에 자유 형식의 텍스트 (freeform text)를 요청한 다음, 정규 표현식 (regex)이나 문자열 매칭 (string matching)으로 이를 파싱 (parse)하려고 시도하는 것입니다. 이는 매우 취약합니다. 모델 업데이트 한 번으로 문구가 바뀌면, 당신의 파서 (parser)는 소리 없이 망가집니다.
해결책은 엄격한 스키마 (strict schema)를 사용한 함수 호출 (function calling)입니다. LLM에게 출력이 가져야 할 형태를 정확히 알려줍니다. 만약 스키마와 일치하는 유효한 JSON을 생성하지 못한다면, 응답을 거부하고 재시도합니다.
다음은 후보자 프로필에 대해 채용 공고를 점수 매길 때 제가 사용하는 실제 스키마입니다:
import { z } from 'zod';
const JobScoreSchema = z.object({
...
has* 불리언 가드(boolean guards)에 주목하세요. 이것들은 매우 중요합니다. 만약 채용 공고에 급여에 대한 언급이 없다면, hasSalaryData는 false가 되고 salaryRange 객체는 존재하지 않게 됩니다. 이 스키마는 LLM이 단순히 필드를 채우고 싶다는 이유만으로 급여 데이터를 허위로 만들어내는(fabricate) 것을 허용하지 않습니다. 이 패턴은 제가 이력서 맞춤화 도구에서 사용했던 환각 방지(anti-hallucination) 아키텍처에서 가져온 것이며, 다양한 도메인에서 효과적으로 작동합니다.
LLM이 이러한 구조화된 출력(structured output)을 반환하면, 단 하나의 필드라도 신뢰하기 전에 Zod 스키마를 통해 검증해야 합니다. 검증에 실패하면 원본 응답(raw response)을 로그로 남기고, 검토 대상으로 플래그를 지정하며, 선택적으로 더 강력한 프롬프트(prompt)를 사용하여 재시도합니다. 이 방식은 환각(hallucination)이 데이터베이스에 도달하기 전에 대부분을 잡아냅니다.
드리프트 탐지기로서의 임베딩 유사도 (Embedding Similarity)
구조화된 출력 검증은 형식 오류(format errors)는 잡아내지만, 의미론적 드리프트(semantic drift)는 잡아내지 못합니다. LLM은 형식적으로는 완벽하게 유효한 JSON 객체를 생성할 수 있지만, 의미론적으로는 쓰레기 같은 내용을 담을 수 있습니다. 관련성 점수(relevance score)는 높을 수 있지만, 실제 매칭 결과는 완전히 관련 없는 직무일 수 있다는 뜻입니다.
의미론적 드리프트를 잡아내기 위해, 저는 LLM 출력에 대한 임베딩(embeddings)을 계산하고 이를 검증된 양질의 출력(known-good outputs) 참조 세트와 비교합니다. 코사인 유사도(cosine similarity)가 임계값(threshold) 아래로 떨어지면 무언가 변했다는 신호입니다.
import { OpenAI } from 'openai';
const openai = new OpenAI();
...
저는 각 출력 유형에 대해 참조 임베딩(reference embedding)을 구축했습니다. 직무 점수 산정(job scoring)의 경우, 사람이 검증한 양질의 출력 세트를 가져와 그 임베딩들의 평균을 내고 이를 기준점(baseline)으로 사용했습니다. 파이프라인이 새로운 공고에 점수를 매기기 시작할 때, 임베딩이 임계값 미만으로 떨어지는 모든 출력은 수동 검토(manual review)로 라우팅되었습니다.
LLM 업데이트로 인해 출력의 어조나 구조가 미묘하게 변경된다고 가정해 봅시다. 임베딩 유사도는 출력이 구조적 검증을 통과하더라도 이를 감지해낼 것입니다. 이러한 의미론적 체크는 스키마가 놓치는 부분을 잡아냅니다.
인간의 임계값: 언제 라우팅할 것인가
모든 출력을 수동으로 검토할 수는 없습니다. 하루에 10,000개의 공고가 올라온다면 그것은 불가능한 일입니다. 그렇다고 모든 출력을 맹목적으로 신뢰할 수도 없습니다. 해결책은 계층화된 임계값 시스템(tiered threshold system)입니다.
저는 임베딩 유사도 점수(embedding similarity score)를 기반으로 세 가지 구역(zones)을 사용합니다:
- 높은 유사도 (High similarity): 자동 승인. 출력이 이미 검증된 양질의 패턴과 밀접하게 일치합니다.
- 중간 유사도 (Medium similarity): 자동 승인하되 검토를 위해 로그를 남깁니다. 수시 점검(spot checks)을 위해 대시보드에 플래그를 표시합니다.
- 낮은 유사도 (Low similarity): 사람의 검토(human review)로 라우팅합니다. 누군가가 승인할 때까지 해당 출력이 사용되는 것을 차단합니다.
임계값(thresholds)은 마법의 숫자가 아닙니다. 일련의 예시들을 실행하고, 사람이 각각의 점수를 매기게 한 뒤, 검토량을 관리 가능한 수준으로 유지하면서 정밀도(precision)를 극대화하는 유사도 차단 지점(similarity cutoff)을 찾아냄으로써 튜닝합니다. 저의 파이프라인의 경우, 엄격한 임계값을 적용하여 일일 리스팅의 관리 가능한 비율 미만으로 검토율을 유지하면서도 대부분의 잘못된 출력을 잡아낼 수 있었습니다.
이것은 한 번 설정하면 끝나는(set-it-and-forget-it) 숫자가 아닙니다. LLM 모델이 변경되거나 데이터 분포(data distribution)가 이동함에 따라 재교정(recalibrate)이 필요합니다. 저는 모델 업데이트가 있거나 출력 분포의 변화가 감지될 때마다 교정 배치(calibration batch)를 실행합니다.
시간이 지남에 따른 평가 드리프트(Eval Drift) 모니터링
제가 배운 가장 어려운 교훈은 평가 시스템 자체가 드리프트(drift)될 것이라는 점입니다. 임베딩 기준점(embedding baseline)은 노후화됩니다. 사람 검토자들은 피로를 느낍니다. LLM의 동작은 API 업데이트와 함께 변합니다.
모니터를 모니터링해야 합니다. 저는 세 가지 지표를 추적합니다:
- 정밀도 (Precision): 나쁘다고 플래그가 지정된 출력 중 실제로 나빴던 것은 얼마나 되는가?
- 재현율 (Recall): 실제로 나쁜 출력 중 평가 시스템이 잡아낸 것은 얼마나 되는가?
- 검토 일치도 (Review Agreement): 두 명의 사람 검토자가 동일한 출력을 평가했을 때, 얼마나 자주 일치하는가?
저는 모든 플래그 지정된 출력과 최종적인 사람의 결정(human decision)을 함께 저장합니다. 정기적으로 평가의 판결(verdict)과 사람의 판결을 비교하는 보고서를 실행합니다. 만약 정밀도가 크게 떨어지거나 재현율이 크게 떨어진다면, 무언가 잘못되었다는 것을 알 수 있습니다. 대개는 임베딩 기준점의 드리프트 때문이며, 이 경우 마지막으로 검증된 출력 배치로부터 기준점을 다시 생성합니다.
또한 저는 시간에 따른 유사도 점수(similarity scores)의 분포를 추적합니다. 만약 평균 유사도가 갑자기 변한다면, 이는 LLM의 출력 분포(output distribution)가 변경되었다는 신호입니다. 모델 제공업체가 기본 동작(default behavior)을 업데이트한다고 가정해 보십시오. 평균 유사도가 하룻밤 사이에 급격히 떨어질 수 있습니다. 만약 여러분이 이 추세를 모니터링하고 있다면, 몇 주가 걸리는 대신 몇 시간 내에 이를 포착할 수 있습니다.
만약 여러분의 팀이 신뢰할 수 없는 AI 출력값으로 고군분투하고 있으며, 그로 인해 배포 속도가 늦어지고 있다면, 그것이 바로 제가 도움을 드리는 부분입니다. 저는 여러분에게 거짓말을 하지 않는 프로덕션 AI 파이프라인(production AI pipelines)을 구축합니다. 함께 의견을 나누게 된다면 기쁘겠습니다.
작성자: Abdul Rehman, 프로덕션 SaaS, MVP 및 AI 자동화를 구축하는 풀스택 AI 엔지니어. 더 많은 내용은 PrimeStrides에서 확인하세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기