hmy751.dev

검색이 잘못된 줄 알았는데, 평가자가 흔들리고 있었다

두 번째 스프린트에서 RAG를 만들며 프롬프트만으로는 답변 품질을 설명할 수 없다는 것을 배웠습니다. 답변이 이상하면 LLM의 말투를 고치기 전에, 어떤 Context가 모델 앞에 놓였는지 봐야 했습니다.

그때는 검색 결과를 프롬프트 안에서 무엇이라고 부르는지만 바꿔도 결과가 움직였습니다. 같은 검색 결과를 두고도 "문서"라고 부를 때와 "Context"라고 부를 때 정확도가 67%에서 83%로 달라졌습니다. 입력의 이름은 단순한 말투가 아니라, 모델이 근거의 역할을 읽는 인터페이스였습니다.

세 번째 스프린트는 그 질문을 더 복잡하게 만들었습니다. 이번에는 텍스트 문서만 다루지 않았습니다. 영상에서 음성을 뽑아 STT로 텍스트를 만들고, 화면 프레임을 추출해 설명을 붙이고, 그 자료를 다시 검색해서 질문에 답하는 멀티모달 RAG를 만들었습니다.

처음에는 검색을 개선하는 문제처럼 보였습니다. 답변이 틀리면 필요한 장면이나 발화를 못 찾은 것 같았습니다. 실제로 retrieval 지표가 낮게 나왔고, embedding 모델이나 top-k, chunk 크기를 바꾸면 결과도 움직였습니다.

그런데 실험을 계속하다 보니 이상한 점이 보였습니다. 검색 파이프라인의 큰 방향이 비슷해도, Judge 프롬프트나 평가 모델을 바꾸면 지표가 크게 달라졌습니다. 결과를 어떤 기준으로 읽느냐에 따라 성공과 실패의 경계가 움직였습니다.

이때부터 질문이 바뀌었습니다.

검색을 고치기 전에, 먼저 검색이 정말 문제인지 믿을 수 있게 측정하고 있는지 봐야 했습니다.

이 글은 그 기준을 세워가는 과정입니다. 검색을 개선하지 말자는 이야기가 아니라, 검색을 비교하기 전에 평가자가 같은 자로 재고 있는지 확인해야 한다는 이야기입니다.

실패 지점이 많아진 멀티모달 RAG

텍스트 RAG에서는 실패 지점을 비교적 단순하게 나눌 수 있었습니다. 문서를 어떻게 자를지, 어떤 embedding을 쓸지, 검색 결과를 얼마나 넣을지, 프롬프트에서 Context를 어떻게 다룰지 보면 됐습니다.

멀티모달 RAG에서는 그 앞에 단계가 더 붙었습니다.

영상에서 음성을 추출하고, STT로 자막을 만들고, 일정 시간 단위로 chunk를 나누고, 화면 프레임을 추출하고, 이미지 설명을 생성하고, 텍스트와 이미지 설명을 함께 저장한 뒤, 질문이 들어오면 관련 구간을 찾아 답해야 했습니다.

어디에서 실패가 생길 수 있는지 보려면 흐름을 먼저 펼쳐야 했습니다.

text
영상
  |-- 음성 -> STT -> 자막 chunk
  |-- 화면 -> 프레임 추출 -> 이미지 설명
  -> 같은 시간 구간으로 결합
  -> 검색: 질문과 가까운 구간을 찾는다
  -> 답변: 검색된 Context로 LLM이 답한다
  -> Judge: 답변과 근거를 평가한다

이 도식에서 중요한 점은 Judge도 파이프라인 바깥의 중립적인 관찰자가 아니라 마지막 판단 단계로 들어온다는 점이었습니다. 답변 하나가 나오기 전에도 전사, 화면 설명, 검색, 평가가 각각 실패할 수 있었습니다.

이 구조에서는 답변이 틀렸을 때 원인을 바로 특정하기 어렵습니다.

  • STT가 발화를 잘못 옮겼을 수 있습니다.
  • chunk 경계가 질문에 필요한 구간을 잘랐을 수 있습니다.
  • frame description이 생성됐지만 실제 답변 Context에 들어가지 않았을 수 있습니다.
  • embedding 모델이 질문과 구간의 의미를 잘 맞추지 못했을 수 있습니다.
  • rerank가 관련 구간을 밀어냈을 수 있습니다.
  • LLM이 Context를 보고도 답을 잘못 만들었을 수 있습니다.
  • Judge가 맞는 답을 틀렸다고 보거나, 틀린 답을 맞았다고 볼 수 있습니다.

처음에는 이 중에서 retrieval이 가장 눈에 띄었습니다. 필요한 구간을 못 찾으면 뒤에서 아무리 잘해도 답변은 좋아질 수 없습니다. 그래서 검색 품질을 높이는 쪽으로 실험이 시작됐습니다.

하지만 멀티모달 RAG에서 어려운 점은, 검색 품질을 평가하는 일 자체도 또 하나의 시스템이라는 데 있었습니다.

낮은 RP가 가리킨 곳

실험에서는 여러 지표를 함께 봤습니다. STT 품질은 WER과 CER로 봤고, 답변은 AR, GR, RP 같은 지표로 평가했습니다.

WER은 단어 단위 오류율이고, CER은 글자 단위 오류율입니다. 영어처럼 단어 경계가 비교적 뚜렷한 언어에서는 WER이 직관적일 수 있지만, 한국어에서는 띄어쓰기나 형태소 처리 때문에 WER이 더 크게 흔들릴 수 있습니다. 그래서 STT 품질을 볼 때는 CER도 함께 봐야 했습니다.

답변 쪽에서는 RP가 특히 눈에 들어왔습니다. 검색된 근거가 정답을 뒷받침하는지를 보려는 지표였기 때문입니다. RP가 낮으면 검색이 필요한 근거를 제대로 찾지 못한 것처럼 보입니다.

실제로 초기 실험에서는 RP가 낮게 나왔습니다. 예를 들어 bge-m3를 쓴 한 기준 실험에서는 WER이 0.1463, CER이 0.0790으로 STT 품질은 아주 나쁘지 않았지만, RP는 0.2571에 머물렀습니다. AR은 0.5286, GR은 0.8857로 나와서 생성 답변과 검색 근거 사이에 간극이 있어 보였습니다.

이 숫자만 보면 다음 행동은 분명해 보입니다. 검색을 고쳐야 합니다.

그래서 같은 기준선에서 top-k와 chunk 크기를 하나씩 바꿔 봤습니다. 숫자를 비교하면 아래처럼 보였습니다.

조건 AR RP 처음 읽은 방식
bge-m3 기준 0.5286 0.2571 RP가 낮아 검색 근거가 부족해 보인다
top-k 3 0.7143 0.3333 검색 설정 조정으로 AR과 RP가 같이 오른다
chunk 20초 0.8571 0.2000 AR은 오르지만 RP는 내려간다

이 표에서 먼저 보이는 것은 검색 설정이 실제로 결과를 바꾼다는 점입니다. top-k를 5에서 3으로 줄였을 때는 AR과 RP가 함께 올랐습니다. 답변에 들어가는 Context가 줄어들면서 노이즈가 줄어든 것으로 읽을 수 있었습니다.

하지만 chunk를 20초로 줄였을 때는 다른 그림이 나왔습니다. AR은 더 크게 올랐지만 RP는 내려갔습니다. 청크가 짧아지면서 답변 생성에는 더 집중된 Context가 들어갔을 수 있지만, Judge가 보기에 검색된 근거의 비율은 낮아진 셈입니다.

이 엇갈림은 중요했습니다. 모델이 답은 맞혔지만, 평가 기준에서 보기에는 근거가 충분하지 않았다는 뜻일 수 있습니다. 또는 Judge가 근거와 답변의 관계를 너무 좁게 보고 있었을 수도 있습니다. 어느 쪽이든 "RP가 낮다"는 말만으로는 다음 행동을 바로 정할 수 없었습니다.

여기서부터 RP를 조심해서 읽어야 했습니다. RP는 검색기만의 순수한 점수가 아니었습니다. 검색 결과, chunk 구성, rerank, 질문-정답 세트, Judge의 판정 기준이 함께 만든 값이었습니다.

검색은 여전히 중요한 레버

그렇다고 검색 문제가 없었다는 뜻은 아닙니다. embedding 모델과 검색 설정은 결과를 분명히 바꿨습니다.

같은 멀티모달 자료라도 어떤 embedding을 쓰느냐에 따라 질문과 구간이 가까워지는 방식이 달라졌습니다. bge-m3는 초기 비교에서 꽤 좋은 후보로 보였고, 다른 embedding 조합과 비교하면서 검색 품질을 끌어올리는 기준이 됐습니다.

이후 교차 실험에서도 이 방향은 어느 정도 확인됐습니다. Judge를 gpt-4o-mini로 맞춰놓고 비교했을 때, text-embedding-3-small에 threshold 0.3을 둔 조합의 RP는 0.238이었고 bge-m3 조합의 RP는 0.571이었습니다. 이 비교에는 실행 환경과 전사/비전 경로 차이도 섞여 있어서 "embedding만의 효과"라고 단정할 수는 없습니다. 그래도 낮은 RP를 Judge의 엄격함만으로 설명하기에는 부족했습니다. 검색 스택 자체가 RP에 영향을 주고 있었습니다.

rerank도 중요한 선택지였습니다. 1차 검색에서 후보를 넓게 가져오고, 그 후보를 다시 질문과 더 잘 맞는 순서로 재정렬하면 답변에 들어가는 Context의 품질을 개선할 수 있습니다. 실제로 text-embedding-3 계열 embedding과 rerank를 결합한 실험에서는 AR과 GR이 높게 나오는 경우가 있었습니다. 한 실험에서는 WER 0.2806, CER 0.1514 조건에서 AR 0.8571, GR 0.9143, RP 0.3095가 나왔습니다.

하지만 이 결과도 단순히 "rerank가 좋다"로 끝낼 수 없었습니다. 같은 계열에서도 설정에 따라 RP가 다르게 움직였고, AR과 RP가 항상 같은 방향으로 움직이지 않았습니다.

검색을 개선하는 일은 분명 필요했습니다. 다만 검색 실험을 제대로 읽으려면 평가자가 안정적이어야 했습니다. 평가 기준이 흔들리면, 검색을 바꿔서 좋아진 것인지 평가자가 다르게 판정한 것인지 구분하기 어려웠습니다.

Judge를 바꾸자 달라진 해석

가장 큰 전환점은 Judge였습니다.

초기에는 답변을 평가하는 Judge 프롬프트도 실험의 일부처럼 다뤘습니다. 답변이 맞는지, 근거가 충분한지, 검색 결과가 정답을 포함하는지 판정하게 했습니다. 문제는 그 프롬프트가 너무 짧았다는 점이었습니다. "관련성을 0.0에서 1.0 사이로 평가하라"는 식의 지시만으로는, Judge가 무엇을 관련 근거로 볼지 안정적으로 맞추기 어려웠습니다.

그래서 Judge 프롬프트를 다시 썼습니다. 역할을 명시하고, 점수 구간별 기준을 나누고, 타임스탬프 질문처럼 형식이 다르더라도 의미상 같은 근거를 인정하는 예외 규칙을 넣었습니다. 검색 조건은 그대로 둔 채 QA와 평가만 다시 실행했습니다.

지표 구 Judge judge-v2 변화
AR 0.55 0.80 +45%
GR 0.84 0.90 +7%
RP 0.33 0.48 +44%

이 숫자는 검색기가 갑자기 좋아졌다는 뜻이 아니었습니다. 같은 검색 결과를 두고, 평가자가 근거를 읽는 방식이 달라졌다는 뜻이었습니다. 이전까지 낮은 RP를 보고 검색을 고쳐야 한다고 판단했던 것의 일부는, 실제로는 Judge가 간접 근거와 동의어를 놓친 결과였습니다.

수동 검증을 해보면 이 차이는 더 분명했습니다. LLM Judge의 RP 평균은 0.48이었지만, 사람이 검색된 청크를 다시 보면 평균은 0.67 정도로 올라갔고, 답이 없는 환각 테스트 질문을 제외하면 0.71까지 올라갔습니다. 검색 개선이 필요한 질문도 있었지만, 여러 질문에서는 검색보다 판정 기준이 더 큰 문제였습니다.

더 분명했던 것은 같은 bge-m3 검색 스택에서 Judge 모델을 바꿔 본 비교였습니다. llama3.1 Judge에서는 RP가 0.381이었고, gpt-4o-mini Judge에서는 0.571로 올라갔습니다. 같은 retrieved chunk를 두고도 Judge가 무엇을 관련 근거로 인정하느냐에 따라 RP가 달라졌습니다.

여기서 조심해야 할 점은, 이런 숫자를 곧바로 "검색이 좋아졌다" 또는 "검색이 나빠졌다"로 읽을 수 없다는 것입니다. RP는 검색된 구간을 Judge가 관련 있다고 판정하는지까지 포함한 값이었습니다. 검색 설정도 영향을 주지만, Judge가 무엇을 근거로 인정하는지도 함께 영향을 줍니다.

이때 평가 시스템을 모델 실험의 부속물이 아니라 1급 시민으로 봐야 한다는 생각이 들었습니다.

검색을 바꾸는 실험을 할 때는 Judge를 고정해야 합니다. Judge를 바꾸는 실험을 할 때는 검색 조건을 고정해야 합니다. 둘을 동시에 바꾸면 숫자는 움직이지만, 왜 움직였는지 알기 어렵습니다.

간단히 쓰면 실험 분리는 이렇게 잡아야 했습니다.

text
검색을 비교할 때: retrieval 설정만 바꾸고 Judge는 고정한다.
Judge를 비교할 때: Judge만 바꾸고 retrieval 설정은 고정한다.
둘을 함께 바꾸면: 숫자는 움직여도 원인을 읽기 어렵다.

이때부터 검색 결과를 읽는 순서가 바뀌었습니다. 먼저 평가자를 고정하고, 그 다음 검색 설정을 바꿔야 했습니다.

첫 스프린트에서 한 번에 하나의 변수만 바꾸자는 원칙을 세웠는데, 세 번째 스프린트에서 그 원칙이 다시 돌아왔습니다. 이번에는 하이퍼파라미터가 아니라 평가자까지 실험 변수였습니다.

데이터가 실제 Context에 들어가는가

멀티모달 RAG에서 또 하나 중요했던 것은 데이터 흐름이었습니다.

영상에서 frame description을 만드는 것은 그 자체로 기능처럼 보입니다. 화면을 추출하고, 이미지 설명을 만들고, 저장까지 되면 멀티모달 정보가 시스템에 들어온 것처럼 느껴집니다.

하지만 저장됐다는 것과 답변에 쓰였다는 것은 다릅니다.

실제로 점검 과정에서 frame description이 생성되어 저장되는 흐름과, 최종 QA Context에 포함되는 흐름을 분리해서 봐야 했습니다. 설명 데이터가 있어도 답변 생성 단계에서 Context로 조립되지 않으면, LLM은 그 정보를 볼 수 없습니다.

이 문제는 기술적으로는 단순한 연결 문제처럼 보일 수 있습니다. 하지만 실험 해석에서는 큽니다. frame description을 만들었는데도 성능이 오르지 않았다면 두 가지 가능성이 있습니다.

하나는 이미지 설명 자체가 답변에 도움이 되지 않았다는 것입니다. 다른 하나는 이미지 설명이 애초에 답변에 들어가지 않았다는 것입니다.

두 경우의 다음 행동은 완전히 다릅니다. 전자라면 vision 설명 품질이나 질문 세트를 봐야 합니다. 후자라면 pipeline 연결을 먼저 고쳐야 합니다. 따라서 실험 전에 데이터가 실제 평가 경로를 통과하는지 확인해야 했습니다.

이 장면에서 "구현했다"와 "실험에 반영됐다"를 구분하는 감각이 생겼습니다. 기능이 코드에 존재하는 것과, 그 기능이 평가되는 경로 위에 놓인 것은 다릅니다.

답하지 않는 것도 평가 대상

RAG 평가에서 또 하나 까다로웠던 부분은 답이 없는 질문이었습니다.

문서나 영상 안에 근거가 없으면 시스템은 답을 만들지 않아야 합니다. 이때 "답하지 않음"은 실패가 아니라 올바른 행동일 수 있습니다. 하지만 평가 지표가 이것을 제대로 다루지 않으면, 시스템은 억지로 답을 만드는 쪽으로 유도될 수 있습니다.

두 번째 스프린트에서도 비슷한 문제가 있었습니다. Context가 비어 있거나 관련 근거가 부족할 때, 모델이 답변을 멈추는 것이 맞는 경우가 있었습니다. 세 번째 스프린트에서는 이 문제가 더 중요해졌습니다. 영상 기반 질문에서는 특정 장면이나 발화가 실제로 없을 수 있고, 검색 결과가 없을 때의 올바른 거절을 평가해야 했습니다.

만약 Judge가 "정답 문장을 말했는가"만 본다면, 올바른 거절은 낮은 점수를 받을 수 있습니다. 반대로 근거 없는 답변이 운 좋게 정답과 비슷하면 높은 점수를 받을 수도 있습니다.

실험에서도 이런 질문이 있었습니다. 영상에 없는 정보를 일부러 묻는 환각 테스트 질문은 모든 실험에서 RP가 0에 가까웠습니다. 이것은 검색기가 실패했다는 뜻이 아니라, 애초에 관련 근거가 없는 질문이기 때문에 자연스러운 결과였습니다. 그런데 이 값을 그대로 RP 평균에 넣으면 검색 품질이 실제보다 낮아 보입니다. 이런 질문은 retrieval precision보다 groundedness나 refusal accuracy로 읽어야 했습니다.

그래서 평가 시스템에는 답변의 정확도뿐 아니라 근거의 충분성, 그리고 근거가 없을 때의 중단 기준이 함께 들어가야 했습니다. RAG에서 환각을 줄인다는 것은 틀린 답을 줄이는 것만이 아니라, 답하지 않아야 할 때 답하지 않는 능력을 포함했습니다.

검색을 보기 전에 평가자 고정

세 번째 스프린트의 결론은 "검색보다 평가가 중요하다"가 아니었습니다. 검색은 여전히 중요했습니다. embedding 모델, top-k, chunk 크기, rerank는 모두 답변 품질을 바꿨습니다.

하지만 검색을 개선하려면 먼저 평가 시스템을 믿을 수 있어야 했습니다.

평가자가 흔들리면 실험 결과는 흐려집니다. RP가 낮을 때 그것이 검색 실패인지, 근거 판정 기준의 문제인지, QA Context와 평가 Context의 불일치인지 알 수 없습니다. AR이 높아졌을 때도 답변이 정말 좋아진 것인지, Judge가 더 관대해진 것인지 확인해야 합니다.

그래서 이번 스프린트에서 남은 규칙은 이렇습니다.

  • 검색 실험을 할 때는 Judge와 평가 기준을 고정합니다.
  • Judge를 개선할 때는 검색 조건을 고정합니다.
  • STT 품질은 WER만 보지 않고 CER도 함께 봅니다.
  • 생성된 데이터가 실제 QA Context에 들어가는지 확인합니다.
  • 답이 없는 질문은 retrieval 실패가 아니라 올바른 거절 여부로 평가할 수 있어야 합니다.
  • RP는 검색기만의 점수가 아니라 검색과 평가자가 함께 만든 결과로 읽습니다.

두 번째 스프린트에서 "프롬프트로 환각을 잡기 전에 Context를 봐야 한다"는 것을 배웠습니다. 세 번째 스프린트에서는 그 다음 질문을 만났습니다. Context를 잘 가져왔는지 알려면, 그 Context를 평가하는 기준도 먼저 안정되어 있어야 했습니다.

AI/ML 실험에서 모델을 바꾸는 일은 눈에 잘 보입니다. embedding을 바꾸고, top-k를 바꾸고, Judge 모델을 바꾸면 숫자가 움직입니다. 하지만 숫자가 움직였다는 사실보다 더 중요한 것은 그 숫자를 믿을 수 있는 조건입니다.

이번 스프린트 이후 RAG를 볼 때 질문이 하나 더 늘었습니다.

답변이 틀렸을 때 검색을 의심합니다. 하지만 검색을 의심하기 전에, 그 검색을 틀렸다고 말하는 평가자가 흔들리고 있지는 않은지도 같이 봐야 합니다.