RSS

RAG 답변 품질을 메트릭 없이 지키는 법: 출처·임계값·방어 코드

RAG 답변 품질을 재는 정량 프레임워크가 있습니다. RAGAS, TruLens 같은 도구가 답변의 충실도(faithfulness), 관련성, 맥락 적합성을 점수로 냅니다. 처음엔 이런 걸 달아야 하나 고민했습니다. 그런데 막상 혼자 쓰는 RAG를 짜고 보니, 정량 점수보다 답이 거짓말을 안 하게 만드는 "코드"가 먼저였습니다. 점수는 그 뒤에 재는 건데, 점수를 안 재도 답이 믿을 만하면 그만이었습니다.

이 글은 정량 메트릭 없이 RAG 답변 품질을 코드로 지킨 결정들을 적은 겁니다.

답이 거짓말을 하는 세 자리

RAG 답변이 틀리는 자리는 대개 세 곳으로 좁혀집니다. 첫째, 검색이 엉뚱한 문서를 가져왔을 때. 둘째, 모델이 가져온 근거를 제대로 안 읽고 지어낼 때. 셋째, 작은 모델이 습관적으로 붙이는 쓸데없는 말(꼬리 출처, 앞답변 베끼기)이 답을 흐릴 때.

정량 메트릭은 이 셋을 점수로 알려줍니다. 하지만 점수를 받아도 결국 고쳐야 할 자리는 같습니다. 그래서 저는 점수를 재는 대신, 이 세 자리 각각에 코드를 하나씩 세웠습니다.

임계값에 상대 갭을 더한 이유

벡터 검색은 항상 가장 가까운 결과를 돌려줍니다. 질문과 전혀 상관없어도 뭔가는 나옵니다. 그래서 코사인 유사도 하한을 둬야 합니다. 처음엔 절대값 하나로 잡았습니다. 0.42. 이 값보다 낮으면 답에 안 넣었습니다.

그런데 절대 기준만 쓰면 문제가 생깁니다. 아주 강한 근거가 하나 있고 나머지가 다 애매한 경우, 애매한 것들도 0.42를 넘기면 답에 섞여 들어옵니다. 반대로 전반적으로 관련도가 낮은 질문에서는 0.42를 넘는 게 하나도 없어 답이 텅 빕니다.

그래서 절대 기준 위에 상대 갭을 하나 더했습니다. "최상위 근거보다 0.14 이상 유사도가 낮은 청크는 같은 질문에 대한 답이라고 보기 어렵다." 즉 강한 근거가 있을 때는 기준이 저절로 올라가고, 약한 질문에서는 절대 기준이 버티는 구조입니다. 코드 주석에 적은 대로, "절대 기준만 쓰면 강한 근거가 있을 때도 애매한 청크가 따라옵니다."

이건 정확한 메트릭이 아닙니다. 0.42와 0.14는 실제 답변을 보면서 잡은 값입니다. 하지만 점수를 자동으로 계산하는 것보다, 제가 실제로 겪은 두 사례(강한 근거 옆에 쓰레기가 섞이는 것, 약한 질문에 빈 답이 나오는 것)를 직접 막는 데는 이 값들이 더 들어맞았습니다.

리랭커가 코사인을 이기는 자리

코사인은 "비슷한가"를 재지만 "답이 되는가"는 모릅니다. 그래서 작은 모델로 검색 결과를 한 번 더 정렬합니다. 리랭커가 고른 청크는 유사도 하한을 면제합니다.

코드 주석에 이유를 적어뒀습니다. "리랭커는 '답이 되는가'를 본 것이라, 뒤엣것이 더 앞선 판단이다." 즉 코사인 0.38이더라도 리랭커가 "이게 답이다"라고 고르면, 0.42 하한을 무시하고 답에 넣습니다. 반대로 코사인이 높아도 리랭커가 밀면 밀립니다.

이건 두 평가 기준의 위계를 정하는 결정입니다. 유사도(기계적 수치)보다 언어 모델의 판단(의미)을 우선합니다. 물론 리랭커도 틀릴 수 있습니다. 그래서 리랭커가 실패하면 원래 코사인 순서로 돌아가게 했습니다. 어느 쪽이든 답은 나오게, 그리고 더 나은 판단이 있으면 그쪽을 따르게.

같은 문서의 인접 청크가 두 번 들어가는 일

어느 날 답변의 근거 여섯 개를 펼쳐봤더니 두 개가 거의 똑같았습니다. 길이 2,008자가 완전히 겹쳤습니다. 이유는 단순했습니다. 청크 검색은 좁게 하고, 답변을 만들 때 각 청크의 앞뒤를 넓힙니다. 그러다 보니 같은 문서의 붙어 있는 청크 두 개가 따로 검색되고, 각각 앞뒤로 넓히면 서로의 이웃을 끌어와 거의 같은 내용이 됩니다.

그만큼 다른 문서가 밀려났습니다. 근거 여섯 자리 중 두 자리가 사실상 하나였으니까요. 이걸 막으려고, 검색 결과에서 같은 문서의 붙어 있는 청크는 하나만 고르게 했습니다. 빈자리는 다른 문서의 근거가 채웁니다.

이건 답변의 "다양성"을 지키는 코드입니다. 정확도(relevance) 메트릭만 보면 놓치기 쉬운 부분인데, 실제 답변을 읽어보면 한 문서에 편중된 답이 빈약하다는 걸 알게 됩니다. 점수로 잡기 어려운 품질을 코드로 잡은 셈입니다.

출처 투명성이 곧 평가다

이 모든 코드가 있어도, 결국 답이 맞는지 최종 판단은 제 눈이 합니다. 그래서 답에는 항상 출처를 붙입니다. 답변의 문장 끝에 [1], [2]처럼 근거 번호를 달고, 화면에는 각 번호의 출처(문서 제목, 제목 경로, DB 수정일)를 카드로 따로 보여줍니다.

제가 답을 읽다가 의심이 가면, 번호를 눌러 원문을 바로 봅니다. 이게 혼자 쓰는 RAG에서 가장 강한 품질 신호입니다. 정량 메트릭이 "충실도 0.87"이라고 해도 제가 원문과 비교 안 하면 모릅니다. 반대로 출처가 투명하면, 틀린 답은 제가 읽는 즉시 드러납니다.

그래서 답변에 출처가 안 붙는 경우를 적극적으로 막습니다. 모델이 답을 지어낸 경우 출처 번호가 안 달리니, 번호 없는 주장은 의심부터 합니다. 그리고 모델에게 "근거에 없는 사실, 숫자, 날짜, 이름은 만들지 않는다", "정보가 부족하면 '문서에서 충분한 근거를 찾지 못했습니다'라고 답하라"고 시스템 프롬프트에 박아뒀습니다. 모름을 인정하는 답이, 틀리면서 아는 척하는 답보다 훨씬 낫습니다.

작은 모델의 버릇을 코드로 막는다

작은 모델은 프롬프트로 금지해도 자꾸 같은 버릇을 되풀이합니다. 두 가지가 눈에 띄었습니다.

첫째, 답변 끝에 "출처 목록: [1] 문서A, [2] 문서B"를 붙입니다. 화면에는 출처 카드가 따로 있어 중복입니다. 시스템 프롬프트로 "출처 목록을 만들지 마라"고 해도 소용이 없었습니다. 그래서 정규식으로 이 꼬리를 잘라내는 함수를 하나 뒀습니다. 다만 "출처: 사내 규정에 따르면…"처럼 본문으로 이어지는 문장은 자르지 않게, 구분선이나 인용 번호가 있을 때만 자릅니다.

둘째, 후속 질문을 검색어로 바꿀 때 앞 답변을 그대로 베낍니다. "그럼 그건 얼마야?"를 바꾸라고 했더니, 앞 답변의 설명 문장을 통째로 가져오는 겁니다. 그걸 검색에 쓰면 엉뚱한 곳이 검색됩니다. 프롬프트로 "설명 문장을 베끼지 마라" 해도 안 됐습니다. 그래서 결과값 자체를 검사합니다. 인용 번호([1])나 "DB 수정일" 같은 답변 전용 표시가 들어있으면, 그건 검색어가 아니라 답변 조각이니 버리고 원래 질문으로 검색합니다.

이 두 결정 모두 "프롬프트로 가르치다 포기하고 코드로 막은" 사례입니다. 작은 모델을 쓰면 이런 버릇이 잦고, 메트릭으로는 잡히지 않습니다. 답을 직접 읽어봐야 알 수 있는 품질 문제를, 정규식 몇 줄로 막은 셈입니다.

메트릭을 안 측정해도 괜찮은 이유

정리하면, 제가 세운 코드는 전부 "틀린 답이 나올 수 있는 자리를 하나씩 막는" 방어선입니다. 임계값과 상대 갭, 리랭커의 우선순위, 인접 청크 중복 제거, 출처 투명성, 작은 모델 버릇 차단. 각각은 점수가 아니라 구체적인 실패 사례에 대한 대응입니다.

정량 메트릭이 없어도 답을 믿을 수 있는 건, 이 방어선들이 "틀린 답이 나오기 어려운 구조"를 만들기 때문입니다. RAGAS 점수가 0.9라도 출처가 없으면 못 믿고, 점수가 0.6이어도 출처를 눌러보니 맞으면 믿습니다. 혼자 쓰는 시스템에서는 점수보다 그 신뢰의 흐름이 더 중요했습니다.

물론 이게 정답은 아닙니다. 데이터가 늘고 질문이 다양해지면, 결국 정량 평가가 필요해질 시점이 올 겁니다. 그때는 이 방어선들 위에 점수를 얹으면 됩니다. 하지만 지금, 사용자가 한 명인 단계에서는 코드로 품질을 지키는 쪽이 점수를 재는 쪽보다 훨씬 쐈습니다.