Code › tail-villain

RAG 검색과 답변 품질을 따로 검증하기

사용자의 목표와 배경을 검색하는 RAG 파이프라인을 만들고 실제 학습 답변까지 단계별로 검증한 과정

tail-villain의 Study 답변에 사용자의 목표와 배경을 반영하려면 RAG가 필요했다. 사용자가 JLPT N2를 준비하는지, IELTS Writing Task 2에서 관사 사용을 어려워하는지, 실무에서 어떤 장애를 겪었는지 알고 있어야 같은 주제라도 다른 설명과 연습 문제를 줄 수 있기 때문이다.

그런데 관련 문서를 검색해 프롬프트에 넣었다는 사실만으로 개인화가 완성되지는 않는다. 검색 결과가 맞는지, 다른 사용자의 데이터가 섞이지 않는지, 검색된 정보가 실제 답변에 쓰였는지, 그 답변이 현재 학습 주제를 제대로 이어 가는지를 각각 확인해야 했다.

그래서 RAG 구현과 검증을 한 번에 묶지 않고 단계별로 나눴다.


검색 결과를 먼저 검증했다

저장 대상은 사용자가 Roadmap을 만들 때 입력한 Goal과 Background였다. 원문을 KnowledgeDocument로 관리하고, 검색할 수 있는 크기로 나눈 조각을 KnowledgeChunk에 저장했다. 각 조각은 Vertex AI의 임베딩 모델로 768차원 벡터를 만든 뒤 PostgreSQL의 pgvector 컬럼에 넣었다.

전체 경로는 다음과 같다.

Goal / Background
  → 문서 저장
  → 문장과 문단 기준으로 분할
  → 문서 임베딩 생성
  → pgvector 저장
  → 질문 임베딩 생성
  → 코사인 거리로 검색
  → Study 프롬프트에 검색 결과 추가

문서를 저장하는 작업과 임베딩 생성은 실패 기준을 다르게 잡았다. Goal이나 Background 저장은 제품의 기본 기능이므로 반드시 성공해야 하지만, 외부 임베딩 제공자가 잠시 실패했다고 사용자 입력까지 되돌릴 필요는 없다. 원문과 조각은 남겨 두고 임베딩만 나중에 다시 만들 수 있도록 처리했다.

검색에서는 소유권 조건을 두 번 확인했다. KnowledgeChunk의 ownerId뿐 아니라 연결된 KnowledgeDocument의 ownerId도 같은 사용자여야 결과에 포함된다. RAG 문서는 이력서나 목표처럼 개인 정보가 들어갈 수 있어서, 소유권 조건은 검색 정확도가 아니라 데이터 격리 문제다.

첫 번째 실험에서는 IELTS 목표와 영어 말하기 배경을 가진 사용자 A, AWS 목표를 가진 사용자 B를 따로 만들었다. 사용자 A의 질문에는 관련 Goal과 Background가 올바른 순서로 나왔고, 사용자 B의 문서는 결과에 나타나지 않았다. 검색 후에는 실험용 사용자와 파생 문서까지 삭제되는지도 확인했다.


점수 하나를 정답처럼 쓰지 않았다

검색 결과에는 코사인 거리로 계산한 유사도 점수가 붙는다. 구현에서는 1에서 코사인 거리를 뺀 값을 사용했지만, 이 수치는 정답일 확률이 아니다. 같은 0.65라도 언어와 문서 구성, 질문의 구체성에 따라 의미가 달라질 수 있다.

처음 만든 열 개의 라벨 데이터에서는 0.65를 기준으로 삼았을 때 정밀도와 재현율이 모두 90%였다. 하지만 이 값을 고정한 채 새로운 데이터 열 개에 적용하자 정밀도가 81.8%로 떨어졌다.

검색 순위 자체는 여전히 정확했다. 관련 문서가 첫 번째에 나왔지만, 의미가 비슷한 불필요한 문서도 0.65를 넘었고 실제로 필요한 다국어 배경 문서는 0.62에 머물기도 했다. 기준을 높이면 잡음과 함께 필요한 문서까지 사라진다.

여기서 검색 범위를 사용자 전체 문서에서 현재 Roadmap에 연결된 Goal과 Background로 좁혔다. 임베딩 모델이나 점수 계산을 바꾸지 않고 후보 문서부터 줄인 것이다. 새로운 독립 데이터로 이 경로를 다시 평가했을 때 정밀도는 90.9%, 재현율은 100%였고, 관련 없는 다른 사용자의 문서는 계속 차단됐다.

이 결과로 0.65가 보편적인 기준이라는 결론을 내리지는 않았다. 확인된 것은 현재 Roadmap의 출처로 검색 범위를 제한하면 사용자 전체 문서를 뒤지는 것보다 잡음을 줄일 수 있다는 점이었다.


검색 성공과 답변 성공은 달랐다

검색 경로가 동작한 뒤에는 실제 Study API를 통해 RAG를 켠 답변과 끈 답변을 비교했다. 주제와 페르소나, 사용자 메시지는 같게 두고 Roadmap에 Goal과 Background가 연결됐는지만 바꿨다.

다섯 개 주제로 열 번의 요청을 보낸 결과는 분명했다.

확인 항목RAG 사용RAG 미사용
개인 정보 신호가 한 개 이상 포함된 답변5/50/5
개인 정보 신호가 나타난 총횟수120
검색 내부 용어가 노출된 답변00

RAG를 사용한 답변에는 Kafka 비동기 추적 문제, JLPT N3 합격 경험, IELTS 관사 오류, PMP의 CPI와 SPI, Airflow와 dbt에서 겪은 중복 레코드처럼 사용자가 입력한 정보가 실제로 등장했다. 반면 RAG를 끈 답변은 현재 수준과 경험을 다시 묻는 일반적인 질문으로 시작했다.

비용도 같이 확인했다. 이 실험에서 RAG를 사용한 요청은 평균적으로 약 1.1초 더 걸렸고, 답변 하나당 입력 토큰이 약 133개 늘었다. 한 번씩 생성한 결과라서 안정적인 성능 수치로 볼 수는 없지만, 개인화에 검색과 추가 문맥 비용이 붙는다는 사실은 확인할 수 있었다.

문제는 답변의 다음 행동이었다. IELTS 답변은 Writing Task 2와 관사 오류를 정확히 언급하고도 마지막에 말하기 실력을 물었다. 검색된 사실은 맞게 사용했지만 현재 학습 목표에서는 벗어난 것이다.

그래서 평가 항목도 분리했다. 개인 정보가 근거로 쓰였는지, 현재 주제의 첫 단계에 맞는지, 바로 실행할 수 있는 연습을 제시했는지를 각각 2점으로 두었다. RAG 답변은 평균 3.4점, RAG를 끈 답변은 1.0점이었지만, 엄격한 통과 기준을 만족한 RAG 답변은 다섯 개 중 하나뿐이었다.

검색은 개인화를 만들었지만, 좋은 수업까지 자동으로 만들지는 못했다.


실패를 구분할 수 있는 구조

이번 작업에서 가장 오래 걸린 부분은 pgvector 연결보다 평가 경계를 정하는 일이었다. 검색 결과가 틀리면 임베딩과 후보 범위, 순위를 확인해야 하고, 검색 결과가 맞는데 답변에 나타나지 않으면 프롬프트 주입 경로를 확인해야 한다. 정보는 들어갔지만 엉뚱한 질문으로 끝난다면 생성 정책과 수업 진행 규칙을 고쳐야 한다.

이 세 문제를 하나의 RAG 품질로 묶으면 어디를 수정해야 할지 알 수 없다. 그래서 검색 평가, 근거 사용 평가, 학습 답변 평가를 별도로 남겼고, 이전 답변보다 나아 보인다는 판단 대신 고정된 사례와 점수 기준으로 비교했다.

tail-villain에서 RAG의 역할은 사용자의 정보를 많이 넣는 데 있지 않다. 현재 학습에 필요한 Goal과 Background를 안전하게 찾고, 그 정보가 다음 설명이나 연습 문제로 정확히 이어지게 만드는 데 있다. 7월 10일 작업으로 검색과 개인화의 연결은 확인했지만, 검색된 근거를 어떤 수업 행동으로 바꿀지는 다음 단계의 문제로 남았다.