한국어 RAG에서 임베딩 모델 4개 갈아끼우면서 삽질한 기록
정유나
@yuna_ai
결론부터: 모델 문제인 줄 알았는데 청킹이었다.
사내 문서 검색 챗봇 만드는데, 연차 이월 규정 쿼리를 넣으면 1등으로 연차 휴가 신청 절차 문서가 올라왔다. '이월'이 통째로 무시되는 느낌. 임베딩은 text-embedding-3-small(1536) 썼고 문서는 한국어 사내 위키 + PDF 한 200개쯤.
코사인 한번 찍어봤다.
q = embed("연차 이월 규정")
print(cos(q, chunk_이월)) # 0.41
print(cos(q, chunk_신청절차)) # 0.39
0.41 vs 0.39. 둘이 사실상 구분이 안 된다. 정답 청크가 1등이긴 한데 마진이 0.02면 그냥 운임. 쿼리 어미만 살짝 바꿔도 순위 뒤집힘.
그래서 모델부터 의심했다. 한국어가 약한 게 원인이겠거니 하고 갈아끼우기 시작.
text-embedding-3-large(3072): small보단 마진 벌어짐(0.44 vs 0.38쯤?). 근데 비용 2배에 조사/동의어 변형엔 여전히 둔함bge-m3: self-host. 한국어 멀티링구얼 평 좋대서 깔아봄. 품질은 확실히 올라왔는데 GPU 메모리랑 배치 직접 관리하는 게 일이었다KURE-v1: 한국어 특화라 기대했는데 우리 도메인(사내 줄임말이 많음) 타는 게 좀 있었다
여기까지 하고도 마진이 드라마틱하게 안 벌어져서 좀 멘붕이 왔다. 모델을 4개나 돌렸는데 왜 안 되지. 임베딩 차원이 문제인가, 정규화를 빼먹었나, 별 걸 다 의심함.
그러다 답답해서 청크를 직접 까봤다. 1000자 고정으로 잘랐더니 표가 중간에서 동강나 있고, '이월'이 들어있는 문단이 헤딩이랑 분리돼서 맥락이 날아가 있었다. 즉 임베딩에 넣는 입력 자체가 쓰레기였던 거. 모델 탓할 게 아니었음.
청킹을 헤딩 단위로 바꾸고, 청크 앞에 섹션 제목을 prepend 했다.
chunk = f"# {section_title}\n{body}" # 제목을 본문에 붙여서 임베딩
그랬더니 small 그대로 두고도 0.41 vs 0.39가 0.52 vs 0.34로 벌어짐. 모델 안 바꾸고 청킹만 손봤는데. 좀 허무했다. 표는 표대로 따로 떼서 마크다운으로 직렬화한 것도 효과 컸던 듯(정확힌 ablation 안 해봐서 모름).
평가도 눈으로 하다가 한번 데였다. 쿼리 몇 개 던져보고 "오 되네" 했는데, 실사용 질문 50개쯤 모아 돌리니 Recall@5가 60%대로 처참. 골든셋 만드는 거 귀찮아도 결국 그게 제일 빨랐다. 아마 표본 더 모으면 또 깨질 것 같긴 함.
암튼 2주 날린 것 치고 얻은 건 "모델 갈아끼우기 전에 청크부터 까봐라" 하나. 지금은 bge-m3 + 섹션 청킹 + 리랭커 한 단 얹어서 굴리는 중. 리랭커가 마진을 얼마나 더 벌어주는지는 아직 측정 중인데, 그건 다음에 따로 쓸까 함(쓸지는 모름).