시맨틱 청킹 (Semantic Chunking): RAG 검색 성능 개선을 위해 의미의 경계에서 문서 분할하기
요약
RAG 시스템의 검색 성능을 높이기 위해 고정 크기 방식 대신 의미의 경계를 기준으로 문서를 분할하는 시맨틱 청킹 기법을 소개합니다. 문장 간 임베딩 유사도를 측정하여 주제가 변하는 지점을 중단점으로 설정하는 원리와 구현 방법을 다룹니다.
핵심 포인트
- 고정 크기 청킹은 문맥을 끊어 검색 품질을 저하시킬 수 있음
- 시맨틱 청킹은 문장 간 코사인 유사도를 측정하여 의미적 경계를 탐색함
- 백분위수 임계값(percentile threshold)을 사용하여 유연한 분할 기준 설정 가능
- LangChain 등 실제 프레임워크에서 활용되는 구현 로직 설명
검색 증강 생성 (Retrieval-augmented generation, RAG)은 모델에 문서 전체를 전달하지 않습니다. 대신 _청크 (chunks)_를 전달합니다. 따라서 모델이 제공할 수 있는 답변은 단일 청크가 포함하는 내용에 의해 제한됩니다. 만약 청킹 (chunking) 과정에서 하나의 아이디어가 반으로 쪼개진다면, 가장 상위에 검색된 청크는 답변의 절반에 불과하며, 아무리 영리한 프롬프팅 (prompting)을 사용하더라도 누락된 나머지 절반을 복구할 수 없습니다. 청킹은 단 하나의 쿼리가 실행되기 전에 내려지는 상류 단계의 품질 결정이며, 여러분의 RAG 시스템이 얼마나 우수한지를 조용히 결정합니다.
단순한 방식은 생각의 중간을 끊어버립니다
고정 크기 청킹 (Fixed-size chunking)은 의미와 상관없이 매 N개의 문자(또는 문장)마다 자릅니다. 이는 매우 단순하며 임베딩 (embeddings)이 필요하지 않지만, 경계가 카운터가 끝나는 지점에서 결정되기 때문에 종종 주제의 한복판을 가로지르게 됩니다. 정의와 그에 대한 예시가 서로 다른 청크에 담기거나, 질문과 답변이 분리됩니다. 정보는 모두 거기 있지만, 검색 가능한 하나의 단위로 함께 묶이지 않을 뿐입니다.
의미가 변하는 지점에서 분할하기
시맨틱 청킹 (Semantic chunking)은 대신 _의미의 경계 (seams of meaning)_에서 분할합니다. 각 문장을 임베딩 (embed)하고, 각 문장이 인접한 문장과 얼마나 유사한지 측정하며, 인접한 유사도가 떨어지는 곳마다 중단점 (breakpoint)을 설정합니다. 동일한 주제가 계속되면 → 유사도가 높고, 주제가 바뀌면 → 벡터 (vectors)가 다른 곳을 가리키며 코사인 유사도 (cosine similarity)가 떨어집니다.
데모에서는 가벼운 TF-IDF bag-of-words (결정론적 방식, API 불필요)로 임베딩을 수행한 다음, 문장들을 따라가며 인접한 코사인 유사도를 측정합니다. 이 n-1개의 숫자 시퀀스가 청커 (chunker)가 분할 기준으로 삼는 전체 신호입니다:
def adjacent_sims(vecs):
return [cosine(vecs[i], vecs[i + 1]) for i in range(len(vecs) - 1)]
...
중단점 백분위수 임계값 (breakpoint-percentile threshold)
어느 정도의 하락이 자르기에 "충분히 큰" 것일까요? sim < 0.3과 같은 고정된 컷오프 (cutoff)는 유사도가 다른 범위에 있는 문서들에서는 제대로 작동하지 않습니다. 대신, **거리 (distance) = 1 − 유사도 (similarity)**로 작업하고, 간격의 _백분위수 (percentile)_를 임계값으로 설정하십시오. p = 85는 상위 15%의 가장 큰 도약 지점만 분할합니다. 더 낮은 백분위수는 기준을 낮추어 더 많은 간격이 조건에 부합하게 만들며, 결과적으로 더 많고 작은 청크를 얻게 됩니다. 이것이 LangChain의 breakpoint_threshold_type="percentile" 방식입니다.
def percentile(xs, p):
s = sorted(xs)
if not s: return 0.0
...
청크 조립하기 (Assemble the chunks)
문장들을 따라 이동합니다. 문장 i 뒤의 중단점(breakpoint)은 i+1에서 새로운 청크를 시작합니다. 두 중단점 사이의 모든 것은 상호 유사한 문장들의 연속이며, 하나의 일관되고 자기 완결적인 아이디어를 형성합니다:
def semantic_chunks(sents, p=85):
vecs = tfidf_vectors(sents)
sims = adjacent_sims(vecs)
...
광합성 → 로마 제국 → 에스프레소 순으로 이어지는 문서의 경우, 적절한 백분위수(percentile)를 설정하면 두 중단점이 실제 주제의 경계에 정확히 위치하게 됩니다. 따라서 각 청크는 하나의 깔끔한 주제가 되며, 검색기(retriever)는 이야기의 절반이 아닌 전체 답변을 반환하게 됩니다.
조절 요소와 도구 모음 (The dials and the toolkit)
청크가 너무 크면 임베딩 (embedding)이 희석되고 프롬프트 (prompt)에 무관한 내용이 가득 차게 됩니다. 반대로 너무 작으면 사실 관계가 답변 가능하게 만드는 맥락 (context)을 잃게 됩니다. 약간의 중첩 (overlap) — 마지막 한두 문장을 다음 청크에 반복하는 것 — 은 주장과 그 근거를 분리해버리는 중단점 발생에 대비할 수 있게 해줍니다. 시맨틱 청킹 (Semantic chunking)은 어디를 자를지를 설정하고, 크기와 중첩은 얼마나 많은 맥락이 함께 따라갈지를 조정합니다.
실제 운영 환경(production)에서는 TF-IDF 장난감 모델을 실제 임베더 (embedder)로 교체하십시오 (Anthropic은 임베딩 엔드포인트를 제공하지 않으므로 Voyage AI를 권장하거나, LangChain의 SemanticChunker를 사용하십시오). 그 후 청크를 인덱싱 (indexing)하고, 상위 몇 개를 검색 (retrieve)하여 모델에 전달합니다. 재귀적 청킹 (Recursive chunking)이 구조를 존중한다면, 시맨틱 청킹은 의미를 존중합니다. 문서가 명확한 구조 없이 여러 주제가 섞여 있고 검색 품질이 중요한 경우 시맨틱 청킹을 활용하십시오.
백분위수를 조절하며 문장 스트립이 시맨틱 청크로 다시 색칠되는 것을 관찰하고, 고정 크기 방식과 검색 결과를 나란히 비교해 보십시오: https://dev48v.infy.uk/ai/days/day55-semantic-chunking.html
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기