내가 만든 RAG의 성능 측정하기
- #AI
동작은 하는데 잘 동작하나요?
이전 글에서 블로그의 게시글을 바탕으로 원하는 답변을 생성하는 간단한 RAG를 직접 만들어봤습니다. 일단 질문을 던지면 답은 나옵니다. 동작은 하는거죠. 하지만 몇 번 써보니 곧 의문이 들었습니다. 이 RAG가 정말 "잘" 동작하고 있는걸까요?
답이 그럴듯하게 보이는 것과 실제로 성능이 좋은 것은 다른 문제입니다. 직접 몇 번 질문해보는 것만으로는 검색이 잘 된 것인지, 아니면 모델이 그럴듯하게 답한 것인지 구분하기 어렵습니다.
그래서 이번 글에서는 직접 만든 RAG의 성능을 어떤 기준으로 측정할 수 있는지, 그리고 청킹 단위를 더 잘게 나눴을 때 그 지표가 어떻게 달라지는지 실험한 결과를 공유해보려고 합니다.
평가 기준 세우기
RAG를 사용하면서 쉽게 측정 가능한 값들을 한 번 생각해봅시다. 토큰 사용량, 레이턴시, 그리고 조회된 청크의 평균 유사도입니다. 이는 수치적인 값이라 쉽게 측정이 가능한데요. 하지만 답변은 어떤가요? 답변은 생성된 글입니다. 명확한 채점 기준을 잡기 애매하죠.
다행히도 RAG에서 이를 비교적 편하게 평가할 수 있는 라이브러리가 있으니, 바로 Ragas입니다. Ragas는 질문, 답변, 그리고 검색으로 가져온 문맥을 함께 보고 몇 가지 지표를 계산해 줍니다. 물론 이 점수만으로 성능을 단정할 수는 없습니다. 하지만 적어도 기준은 세울 수 있겠죠. 저처럼 RAG가 잘 동작하는지 판단할 기준이 없는 상태였다면 꽤 유용하게 써볼 만한 도구입니다.
Ragas는 여러 가지 평가 지표를 제공하지만, 이번 글에서는 그중에서도 제가 이번 실험에 사용한 4가지 지표만 보겠습니다.
Faithfulness(근거 충실성)
Faithfulness는 답변이 검색된 문맥에 실제로 근거하고 있는가를 봅니다.
RAG를 쓰는 이유는 결국 내가 가진 문서를 바탕으로 답하게 만들기 위해서입니다. 그런데 검색된 문맥에는 없는 내용을 LLM이 그럴듯하게 덧붙여버리면, 답변이 자연스러워 보여도 좋은 답변이라고 보기 어렵습니다. 그래서 답변의 주장을 분석해, 각 주장이 검색된 문맥으로 실제로 뒷받침되는지를 측정하는 용도로 사용합니다.
Context Precision(문맥 정밀도)
Context Precision은 검색된 청크들이 관련 있는 순서대로 잘 정렬되어 있는가를 봅니다. 조금 더 정확히 말하면, 검색된 여러 청크 중에서 관련 있는 것들이 얼마나 위쪽에 배치되어 있는지를 보는 지표입니다.
RAG에서는 검색된 청크 여러 개를 LLM에게 한꺼번에 넘겨줍니다. 그런데 LLM은 주어진 문맥 전체를 균등하게 활용하지 않고, 앞에 놓인 내용을 더 많이 참고하는 경향이 있습니다. 때문에 관련 내용을 얼마나 앞에 배치했는지에 따라 점수가 달라집니다.
Context Recall(문맥 재현율)
Context Recall은 질문에 답하는 데 필요한 문맥을 빠뜨리지 않고 잘 가져왔는가를 봅니다. 다만 이 지표는 정답이 미리 주어져 있어야 측정할 수 있습니다. 정답 속 주장들을 기준으로, 각각이 검색된 문맥에서 확인되는지를 역으로 체크하는 방식이기 때문입니다.
Answer Correctness(답변 정확도)
마지막으로, Answer Correctness는 답변이 정답과 사실적으로나 의미적으로 얼마나 일치하는가를 보는 최종 품질 지표입니다. LLM이 판단한 사실 일치 여부와 임베딩 기반 의미 유사도를 함께 써서, 답변이 기준 정답과 얼마나 일치하는지를 측정합니다.
ㅤ정리하면, Faithfulness는 답변의 근거성을, Context Precision과 Context Recall은 검색 품질을, Answer Correctness는 최종 답변의 품질을 보는 지표라고 정리해볼 수 있을 것 같네요.
눈치채셨는지 모르겠지만 결국 이 채점 역시 LLM이 하게 됩니다. 즉, 같은 데이터라도 모델에 따라 답변이 차이가 나게 된다는 한계가 있습니다. 참고로 저는 google/gemini-2.0-flash 모델을 사용해보기로 했습니다.
테스트 세트 만들기
이제 평가 지표를 정했으니 테스트 세트를 만들어야 했습니다. 처음에는 Ragas에 테스트 세트를 자동으로 만들어주는 기능이 있어 이걸 쓰려고 했는데요. 막상 생성된 질문들을 보니 만족스럽지 않았습니다. 질문과 답변들이 일반적인 내용들이라 블로그 글의 구체적인 내용을 제대로 테스트하기엔 부족하다는 느낌이 들었거든요. 좋은 테스트 세트라면 '정말 제시된 문서를 읽어야만 알 수 있는' 질문들로 구성되어야 하는데, 자동 생성으로는 그 수준이 나오지 않았습니다.
질문 세트를 직접 만들어야 고민하고 있을 때, 제가 AI를 구독 중이라는 사실이 떠올랐습니다. 블로그 글이 5개밖에 없으니 차라리 Claude와 Codex에게 파일을 직접 읽혀서 질문을 만들게 하면 되지 않을까? 라는 생각이 들더군요. 그래서 시켜보니까 오, 잘하더라구요.
그렇게 최종적으로 40개 질문을 생성할 수 있었습니다. 5개 포스트를 고르게 커버했고, 단일 포스트에서 답을 찾는 질문 31개와 여러 포스트를 넘나드는 질문 9개로 구성됐습니다. 예를 들어 이런 질문입니다.
블로그 메인 화면에 개발 글과 단순 문제 풀이 기록이 한꺼번에 섞여 보였고, 처음 방문자에게 보여주고 싶은 인상과 달랐기 때문이다. 문제 풀이 글 자체가 싫었던 것은 아니지만, 플랫폼이 그 둘을 원하는 방식으로 구분해서 보여주지 못한다는 한계를 크게 느꼈다.
실험 1: 문서 단위 청킹
이제 테스트 세트까지 완성되었으니 실제 실험을 해볼 차례였습니다. 먼저 가장 기본이었던 문서 단위 청킹에서 성능을 측정해보기로 했습니다. 결과는 아래와 같습니다.
| 지표 | 전체 40개 | 검색 성공 31개만 |
|---|---|---|
| Faithfulness | 0.937 | 0.951 |
| Context Precision | 0.733 | 0.946 |
| Context Recall | 0.742 | 0.957 |
| Answer Correctness | 0.431 | 0.517 |
먼저 성능 지표입니다. 검색 성공 시 Faithfulness, Context Precision, Context Recall은 모두 0.9 이상으로 나쁘지 않습니다. 다만 Answer Correctness는 0.517로, 검색이 돼도 정답과 완전히 일치하는 답변을 만들어내지는 못한다는 것을 알 수 있습니다. 즉, 검색만 되면 문맥 활용은 잘 하는데, 최종 답변의 정확도는 아직 개선이 필요하다는 것이겠죠.
| 지표 | 값 |
|---|---|
| 검색 실패 | 9 / 40개 |
| 평균 유사도 | 0.601 |
| 평균 레이턴시 | 15,982ms |
| 평균 입력 토큰 | 6,962 |
그렇다면 다른 지표도 봐볼까요? 무엇보다 눈에 띄는 건 검색 실패 사례인데요. 전체 40개 중 9개는 검색 단계에서 컨텍스트를 전혀 가져오지 못했습니다. 정확히는 현재 검색 설정상, 질문과 충분히 유사하다고 판단된 문서 청크가 반환되지 않은 경우입니다.
왜 이런 일이 발생했을까요? 실패한 9개 질문을 살펴봤는데, 포스트별로도 고르게 분포되어 있었고 질문 유형도 다양해서 하나의 공통 패턴을 단정적으로 찾기는 어려웠습니다.
다만 문서 단위 청킹에서는 포스트 전체가 하나의 임베딩으로 묶이기 때문에, 특정 섹션의 세부 내용을 묻는 질문일수록 해당 신호가 문서 전체 의미에 묻혀 버렸을 가능성이 있습니다. 즉, 문서 전체 주제는 비슷하더라도 질문이 찾는 구체적인 단락 수준 정보까지는 충분히 포착하지 못했을 수 있다는 뜻입니다.
그 밖에도 답변이 생성되기 까지 평균 16초 가량이 소요됐고, 답변을 생성하기 위해 약 7,000 토큰 정도 소모하는 것을 알 수 있었습니다. 문서 전체를 하나의 청크로 쓰다 보니 컨텍스트가 길어진 탓이라고 추측해볼 수 있겠네요.
그렇다면 간단하게나마 결론을 내려볼 수 있겠죠. 문서 단위로 청크를 나누는 경우, 청킹 단위가 커서 특정 섹션과의 유사도 검색에 실패할 확률이 있다. 그렇다면 청킹 단위를 더 작게 세분화하면 어떨까요? 더 많이 성공하지 않을까요?
실험 2: 섹션 단위 청킹
그래서 이번에는 정말 청킹 단위를 더 잘게 나누면 검색이 더 잘 되는지 확인해보기로 했습니다. 문서 전체를 하나로 보던 방식 대신, 섹션 기준으로 나눠서 다시 측정한 것이죠.
실험은 h2로만 나눴을 때와 h2/h3를 모두 나눴을 때 차이가 있을 것 같아서 두 단계로 나눠 실험했습니다. v2는 ## 기준으로만 분할한 버전이고, v3는 ###까지 포함해 더 잘게 나눈 버전입니다. 그리고 이 두 결과를 앞서 측정한 v1과 함께 비교해보았습니다.
| 지표 | v1 (문서) | v2 (h2) | v3 (h2, h3) |
|---|---|---|---|
| 검색 실패 | 9개 | 4개 | 3개 |
| 평균 유사도 | 0.601 | 0.611 | 0.621 |
| 평균 검색 청크 수 (최대 5개) | 1.48개 | 3.48개 | 3.77개 |
| 평균 레이턴시 | 15,982ms | 11,684ms | 11,062ms |
| 평균 입력 토큰 | 6,962 | 4,082 | 2,344 |
| Faithfulness | 0.937 | 0.867 | 0.935 |
| Context Precision | 0.733 | 0.827 | 0.869 |
| Context Recall | 0.742 | 0.852 | 0.873 |
| Answer Correctness | 0.431 | 0.478 | 0.473 |
가장 눈에 띄는 변화는 검색 실패가 9개에서 3개로 줄고, 입력 토큰도 약 66% 감소했다는 점입니다. 즉, 청크를 잘게 나누는 것만으로 검색 안정성과 비용 효율은 확실히 좋아졌고, 주요 품질 지표도 전반적으로 개선되거나 비슷한 수준을 유지했습니다.
흥미로운 점은 Faithfulness가 v2에서 한 번 떨어졌다가 v3에서 다시 회복되었다는 것인데요. h2 단위로 문서를 나누자 검색 대상은 더 세분화되었지만, 아직 청크 하나에 담긴 범위가 넓어서 답변 생성에 실제로 필요하지 않은 내용까지 함께 들어왔고, 그 영향으로 Faithfulness가 낮아진 것으로 보입니다. 반면 h3 단위로 나눈 v3에서는 청크가 더 잘게 쪼개지면서 필요한 문맥만 더 정확하게 가져올 수 있었고, 그 결과 Faithfulness도 다시 높은 수준으로 회복되었습니다.
반면 Answer Correctness는 v1부터 v2와 v3가 큰 차이를 보이지 않았습니다. 다시 말해, 검색 자체의 성능, 안정성, 효율은 증가했지만 이를 바탕으로 적절한 답안을 생성하는 측면은 아직 여전히 부족하다고 볼 수 있습니다.
다만 이 지표는 조금 보수적으로 해석할 필요가 있습니다. Answer Correctness는 채점 AI의 영향도 크게 받고, 기준 답안과 표현 방식이 다르면 실제로는 맞는 답변이어도 점수가 낮게 나올 수 있기 때문입니다. 특히 블로그처럼 하나의 주제를 여러 방식으로 설명할 수 있는 경우라면 이런 한계가 더 명확합니다.
그래도 이번 결과에서 한 가지는 확인할 수 있습니다. 적어도 답변 품질이 크게 무너진 수준은 아니라는 점입니다. 정말 엉뚱한 답변이 많았다면 0.1대에 가까운 점수가 나왔을텐데 0.4~0.5 정도의 점수를 꾸준히 유지하고 있습니다. 따라서 채점 AI가 보기에 답변이 아주 뛰어나다고 보기는 어렵더라도 최소한 일정 수준의 답변은 꾸준히 만들어내고 있다고 해석할 수 있겠습니다.
마무리 - 동작은 하는데 잘 동작하나요?
처음 질문으로 다시 돌아가보겠습니다. "잘 동작하는 것 같다"는 느낌과 "잘 동작한다고 말할 수 있다"는 것은 전혀 다른 이야기입니다. 느낌으로는 "잘 되는 것 같아요"라고밖에 못 하지만, 근거가 있어야 "잘 됩니다"라고 말할 수 있으니까요.
이번 실험을 통해 적어도 지금 이 RAG에 대해서는, 검색 안정성은 개선되고 있고, 문맥 활용은 나쁘지 않으며, 최종 답변 품질은 아직 개선 여지가 있다고 말할 수 있게 됐습니다. 막연한 느낌이 아니라, 수치라는 데이터와 함께 말이죠.
물론 아직도 남은 일들은 있습니다. 아직 검색되지 않는 3개의 질문, 0.5 정도를 유지하는 Answer Correctness, 그리고 11초 정도가 소요되는 레이턴시까지 여전히 부족한 부분들은 존재합니다.
하지만 적어도 이제는 막막하게 "왜 이상하지?"라고 느끼는 대신, "어디가 문제인지"를 짚어볼 수 있게 되었습니다. 측정할 수 있으면 개선할 수 있으니까요.