청킹(chunking) — RAG에서 제일 과소평가되는 단계. 대부분 RecursiveCharacterTextSplitter에 chunk_size=1000 박고 끝인데, 한글 문서에선 이게 엄청난 차이를 만든다.
한글 특유의 문제
영어는 공백 단위 토큰 비율이 안정적인데 한글은 한 글자당 bpe 토큰이 2~3개. 그래서 character 기준 1000자는 실제로 ada-002 토큰 700~900. 이건 그나마 다행. 문제는 문장 경계다. "...다." "...요." "...음." 같은 종결어미가 혼재하면 naive sentence splitter가 다 틀린다.
시도한 전략들
- Fixed size 500자 + overlap 100 — 기준선. 문장 끊김 많음.
- Recursive with ["\n\n","\n","다. ","요. "] — 문단 우선, 그다음 문장. 한결 자연스러워짐.
- kss 라이브러리 기반 sentence split — 한국어 특화. 정확한데 느림(문서당 수 초).
- Semantic chunking (LangChain 신기능) — embedding 거리가 threshold 이상 벌어지면 분리. 품질 좋은데 색인 비용 2배.
평가
200개 질문-정답 셋 만들어서 recall@5 측정:
- Fixed 500: 0.71
- Recursive with hangul terminators: 0.79
- kss + hierarchical (문단 → 문장): 0.84
- Semantic: 0.86
운영 비용까지 감안하면 kss + hierarchical이 우리 현실적 선택. semantic은 문서 업데이트 빈번한 환경엔 재색인 비용이 부담.
또 배운 것
표(table)나 코드블록은 무조건 하나의 chunk로 보존해야 한다. 가운데 잘리면 retrieval이 완전히 망가진다. 그래서 실제론 원문을 markdown으로 변환한 뒤 "table block은 indivisible" 규칙을 사전 처리로 걸었음.
그리고 chunk마다 문서 메타데이터(제목, 섹션 경로)를 앞에 붙이는 것. 이게 작은 트릭 같지만 retrieval quality에 꽤 영향. "이 조각이 어디서 왔는지"를 embedding 공간에서도 문맥화함.