LLM 평가 프레임워크를 처음부터 직접 구축하며 느낀 점: 차라리 구매했어야 했던 것들
요약
LLM 평가 프레임워크를 직접 구축할 때 겪는 시행착오와 효율적인 개발 전략을 다룹니다. 도메인 특화 요소인 루브릭과 데이터셋은 직접 구축하되, 모델 호출이나 캐싱 같은 범용적인 인프라는 기존 도구를 활용할 것을 권장합니다.
핵심 포인트
- 도메인 특화 루브릭과 데이터셋은 직접 구축하여 자산화해야 함
- 모델 호출, 파싱, 캐싱 등 범용적인 인프라는 직접 만들지 말고 구매/재사용할 것
- 복리로 쌓이는 자산(데이터셋)과 부패하는 자산(수동 트레이싱)을 구분할 것
- 평가 기준(Rubric)은 구체적이고 행동 가능한 형태로 작성해야 함
어느 주말, 저는 약 200줄의 Python 코드로 LLM 평가 (eval) 프레임워크를 작성했습니다. 데모는 아주 멋지게 작동했습니다. 저는 제가 똑똑하다고 느꼈습니다.
6개월 후, 그 프레임워크는 엉망진창이 되었습니다. 세 가지 서로 다른 파싱 해킹 (parsing hacks)이 적용된 세 가지 서로 다른 판사 모델 (judge models). 11월 이후로 아무도 손대지 않은 테스트 데이터셋. 누군가 프롬프트 (prompt)를 망가뜨려서가 아니라, 벤더 (vendor)가 모델을 살짝 수정하는 바람에 계속 실패하는 CI 게이트 (CI gate). 그리고 교대 근무 중인 두 번째 엔지니어가 저에게 아주 정당하게 물었습니다. "이게 대체 어떻게 작동하는 거죠?"
프레임워크가 실패한 것이 아닙니다. 주말 튜토리얼에서 생략되었던 80%의 작업이 실패한 것입니다. 그 격차가 이야기의 핵심이며, 이것이 제가 다시 시작하기 전에 스스로에게 해주고 싶은 말입니다.
누군가 내게 말해줬으면 했던 단 한 문장: 루브릭 (rubric)을 만들고, 러너 (runner)를 구매하라
제가 깨닫기까지 6개월이 걸린 구분은 다음과 같습니다.
평가 (eval) 설정의 일부는 당신만의 것이며 오직 당신만의 것입니다. 당신의 제품에 무엇이 "좋은" 것인지를 결정하는 루브릭 (rubric). 실제 실패 사례로부터 구축된 데이터셋. 변경 사항이 릴리스 (release)를 차단할 만큼 나쁜지 판단하는 규칙. 다른 누구도 이것들을 작성할 수 없습니다. 왜냐하면 이것들은 당신의 도메인 (domain)을 인코딩하기 때문입니다.
나머지는 모든 회사에서 동일합니다. 판사 모델 (judge model)을 호출하고, 답변을 파싱 (parse)하며, 재시도하고, 캐싱 (cache)하는 것. 수천 개의 체크를 병렬로 실행하는 기계 장치. 실시간 트래픽을 점수화하는 배관 (plumbing). 실패하는 호출들을 함께 그룹화하는 시스템. 모든 팀이 이것들을 재구축하고, 똑같은 버그를 겪으며, 두 번 작성함으로써 얻는 이득은 아무것도 없습니다.
그러니 첫 번째 목록을 만드세요. 두 번째 목록은 직접 만들지 마세요. 저는 두 번째 목록을 재구축했고, 그 대가로 1년의 대부분을 소비했습니다.
이제 모든 구성 요소에 대해 던지는 두 가지 질문
이것의 어떤 부분을 작성하기 전에, 저는 두 가지를 묻습니다:
- 나에게만 특수한 것인가, 아니면 일반적인 것인가? (Is it specific to me, or generic?) 내 도메인에 특화된 루브릭 (Rubric)은 특수하며 소유할 가치가 있습니다. 모델 호출 주변의 재시도 및 캐싱 루프 (retry-and-cache loop)는 일반적입니다. 누구나 똑같은 것을 작성합니다.
- 복리로 쌓이는가, 아니면 부패하는가? (Does it compound, or does it rot?) 실제 운영 환경의 실패 사례로부터 성장하는 데이터셋은 복리로 쌓입니다. 1년이 지나면, 이는 어떤 경쟁자도 복제할 수 없는 회귀 테스트 스위트 (regression suite)가 됩니다. 수동으로 구축한 트레이싱 레이어 (tracing layer)는 부패합니다. 벤더가 무언가를 변경하는 순간, 당신은 뒤처지고 패치 작업을 해야 합니다.
특수하고 복리로 쌓이는 부분들을 구축하세요. 그 외의 모든 것은 재사용하세요. 그 단 하나의 필터만 있었어도 이 모든 난장판을 피할 수 있었을 것입니다.
실제로 직접 구축할 가치가 있는 것들
네 가지 요소가 자격을 얻었으며, 이 네 가지 모두 에이전트 코드와 동일한 git 저장소에 존재하며, 점수를 매기는 프롬프트와 함께 동일한 풀 리퀘스트 (pull request)에서 리뷰됩니다:
- 루브릭 (The rubric). 변경 사항이 diff에 나타나도록 데이터베이스가 아닌 git에 보관하세요. 그리고 구체적으로 만드세요. "품질을 평가하라"는 노이즈입니다. "모든 주장이 출처에 의해 뒷받침되면 5점, 대부분 지어낸 것이라면 1점을 부여하라"는 행동으로 옮길 수 있는 시그널입니다. 이와 같이 세분화된 루브릭은 인간 리뷰어와 눈에 띄게 더 자주 일치합니다.
- 데이터셋 (The dataset). 당신이 만들어낸 200개보다 실제 운영 환경에서 가져온 100개의 사례가 더 낫습니다. 가장 어려운 실패 사례들을 가져오세요. 대부분의 시그널은 최악의 10% 호출에 들어있기 때문입니다. 하나의 기능 변경이 모든 것을 다시 실행하지 않도록 각 사례를 영역별로 태깅하세요.
- 게이팅 규칙 (The gating rules). 릴리스를 차단하기 위한 정확한 임계값 (thresholds)과 정책은 당신의 것입니다. 그 뒤에 숨은 수학은 당신의 것이 아니지만, 기준점이 어디에 위치할지에 대한 결정은 당신의 것입니다.
- 당신의 라벨 (Your labels). 무엇이 올바른 점수인지에 대한 인간의 판단입니다. 오직 당신의 팀만이 이를 만들어낼 수 있습니다.
내가 구축하지 말았어야 했던 것들
판단 엔진 (judge engine)이 가장 큰 고통을 주었으므로, 거기서부터 시작하세요.
모든 판단 모델 (judge model)은 점수를 조금씩 다른 형태로 반환합니다. 어떤 모델은 점수를 태그 (tags)로 감싸고, 다른 모델은 "Score: 4"라고 쓰며, 또 다른 모델은 횡설수설하는 문단 뒤에 숫자를 묻어버립니다. 제 파서 (parser)는 첫날에는 거의 모든 것을 잡아냈습니다. 하지만 90일째 되는 날, 한 벤더 (vendor)가 숫자를 다르게 포맷팅하는 업데이트를 배포했고, 제 파서는 놓친 사례들에 대해 조용히 중간 점수로 대체(fallback)해 버렸으며, 제 대시보드에는 평가 (evals) 결과가 완만하게 상승하는 것으로 표시되었습니다. 실제로는 그렇지 않았습니다. 중간 점수는 모든 것을 평균으로 끌어내릴 뿐입니다.
거기에 드리프트 (drift) 문제까지 더해집니다. 모델 업데이트 이후, 판단 모델이 지난달과 이번 달에 동일한 답변에 대해 서로 다른 점수를 매기게 되어, 아무도 건드리지 않은 프롬프트 (prompts)에서 게이트 (gate)가 실패하기 시작합니다. 그리고 비용 문제도 있습니다. 대규모 야간 데이터 세트에 최고 수준의 판단 모델을 실행하는 것은 실제 예산 항목이 됩니다. 이 모든 것을 올바르게 구축하는 것은 모든 회사에서 동일하게 보이는 결과물을 얻기 위해 몇 달간의 노력을 들여야 하는 일입니다.
대규모로 체크를 실행하고, 실시간 트래픽 (live traffic)을 점수화하며, 실패한 호출 (calls)을 테마별로 그룹화하는 것도 마찬가지입니다. 모두 일반적인 작업이며, 모두 긴 고난의 과정이고, 그 중 어느 것도 귀사의 경쟁 우위 (edge)가 아닙니다. 이제 저는 이 모든 작업에 대해 기존의 러너 (runner)를 재사용할 것입니다.
평균이 아니라 변화에 게이트를 설정하라
이것은 제가 저지른 가장 비싼 작은 실수였습니다. 저는 게이트를 "평균 점수가 특정 선 아래로 떨어지면 실패"하도록 설정했습니다.
문제는 작은 데이터 세트는 노이즈 (noise)가 심하다는 것입니다. 테스트 케이스가 30개뿐이라면, 어떤 케이스를 선택했느냐에 따라 평균은 자체적으로 몇 점씩 흔들립니다. 그래서 제 게이트는 무작위 노이즈 때문에 실패하고, 실제 퇴보 (regressions) 상황에서는 통과해 버렸습니다. 한 달도 채 되지 않아 모두가 게이트를 무시하는 법을 배웠고, 이는 게이트가 아예 없는 것보다 더 나쁜 상황이었습니다.
실제로 효과가 있는 것은 두 가지 체크를 병행하는 것입니다:
- 치명적인 하락을 잡아내는 하드 플로어 (hard floor).
- 하락 폭이 일반적인 흔들림보다 클 때만 작동하는, 최근 이동 평균 기준선 (rolling recent baseline)과의 비교.
두 번째 방식이 사람들이 신뢰하는 게이트와 사람들이 음소거 (mute)해 버리는 게이트의 차이를 만듭니다.
CI와 프로덕션에서 동일한 체크를 실행하라
저는 체크를 CI에만 유지했고, 프로덕션 (production)은 한 분기 만에 제 데이터 세트를 벗어나 드리프트되었습니다.
해결책은 두 곳 모두에서 정확히 동일한 루브릭 (rubric)을 실행하는 것입니다. 즉, 출시 전에는 테스트 세트 (test set)에 대해, 출시 후에는 라이브 트래픽 (live traffic)에 대해 실행하는 것입니다. 두 결과가 일치하지 않을 때, 그 불일치는 유용한 정보가 됩니다. 만약 CI는 통과(green)되었는데 프로덕션 (production)에서 실패하고 있다면, 당신의 데이터 세트 (dataset)가 더 이상 현실과 닮아 있지 않다는 뜻입니다. 만약 프로덕션에서는 전혀 나타나지 않는 문제로 CI가 실패한다면, 당신의 데이터 세트가 불필요한 짐 (dead weight)을 지고 있다는 뜻입니다. 어느 쪽이든 무언가를 배우게 되지만, 이는 양쪽 모두에서 동일한 체크를 수행할 때만 가능합니다.
아무도 경고해주지 않은 비용
주말 버전은 말 그대로 주말에만 하는 작업입니다. 하지만 실제 버전은 그렇지 않습니다. 일단 몇 개의 제품을 출시하고 나면, 이 모든 것을 유지하는 데에는 풀타임 엔지니어 1명에서 1.5명에 달하는 인력이 거의 1:1 비율로 필요합니다.
그리고 그 비용은 예상치 못한 시점에 나타납니다. 판사 모델 (judge model)이 업데이트되어 파서 (parser)가 고장 나는 주에 나타납니다. 벤더 (vendor)가 트레이싱 (tracing) 형식을 변경하는 주에 나타납니다. 혹은 당신의 데이터 세트가 제품의 주요 실패 사례를 조용히 커버하지 못하게 되는 달에 나타납니다. 이 중 어느 것도 주말 로드맵 (roadmap)에는 포함되어 있지 않지만, 이 모든 것이 바로 본업입니다.
전체를 직접 구축하는 것이 실제로 옳은 경우
공정하게 말하자면, 때로는 직접 구축하는 것이 올바른 결정일 때도 있습니다:
- 당신이 연구소 (research lab)이고, 평가 방법 (eval method) 자체가 제품인 경우.
- 모든 종속성 (dependency)에 대해 긴 보안 검토가 필요한 엄격한 격리 요구 사항이 있는 경우.
- 당신의 도메인이 너무 좁아서 일반적인 설정이 그저 불필요한 짐 (dead weight)이 되는 경우.
그리고 당신이 선을 넘었으며 직접 구축하는 것을 멈춰야 한다는 몇 가지 신호들:
- 두 번째 엔지니어가 합류했는데, 프레임워크 (framework)가 여전히 한 사람의 머릿속에만 존재하는 경우.
- 하나 이상의 판사 (judge)를 위해 하나 이상의 파서 (parser)를 유지 관리하고 있는 경우.
- 평가 비용 (eval bill)이 분기별 논의 주제가 된 경우.
친구에게 경고해주고 싶은 실수들
- 평가 설정 (eval setup)을 프롬프트와 함께 배포되는 코드가 아닌 사이드 프로젝트 (side project)로 취급하는 것.
- 루브릭 프롬프트 (rubric prompts)를 변경 사항이 diff에 나타나지 않는 데이터베이스에 보관하는 것.
- 판사 모델 (judge model)의 버전을 고정 (pinning)하지 않고 유동적으로 두는 것.
- 아주 작은 데이터셋의 평균값으로 게이팅 (gating)하는 것.
- 모든 풀 리퀘스트 (pull request)마다 비용이 많이 드는 전체 판사 (full expensive judge)를 실행하는 것.
- 출시 시점에 데이터셋을 동결하여, 2년 된 테스트 세트가 완전히 새로운 제품을 평가하게 만드는 것.
제가 계속해서 되새기는 교훈은, 어렵고 가치 있는 부분은 결코 코드가 아니었다는 점입니다. 그것은 루브릭 (rubric)과 데이터셋 (dataset)이었으며, 이 두 가지는 오직 저희 팀만이 작성할 수 있는 것이었습니다. 하지만 저는 처음 6개월 동안 이 두 가지를 제외한 모든 것을 구축하는 데 시간을 보냈습니다.
만약 여러분이 이런 시스템 중 하나를 구축해 보셨다면, 어떤 부분이 조용히 여러분의 시간을 가장 많이 잡아먹었는지 듣고 싶습니다. 저에게는 언제나 그 파서 (parser)가 그랬을 것입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기