RAG 검색 품질을 좌우하는 청킹 전략의 종류, 벤치마크 결과, 그리고 최적의 청크 크기를 선택하는 실전 가이드입니다.
청킹(Chunking)은 원본 문서를 검색 단위인 청크(Chunk)로 분할하는 과정입니다. RAG 시스템에서 검색의 최소 단위가 청크이므로, 청킹 전략은 검색 품질에 직접적인 영향을 미칩니다. Vectara의 2025년 연구에 따르면, 청킹 설정이 임베딩 모델 선택만큼 또는 그 이상으로 검색 품질에 영향을 준다는 결과가 보고되었습니다.
잘못된 청킹은 두 가지 방향으로 문제를 일으킵니다. 청크가 너무 작으면 문맥이 손실되어 의미 있는 정보를 전달하지 못합니다. 반대로 청크가 너무 크면 노이즈가 포함되어 검색 정밀도가 떨어지고, 임베딩 벡터가 구체적인 의미를 담지 못하게 됩니다.
원본 문서 (10,000 토큰):
"리팩터링은 소프트웨어의 외부 동작을 변경하지 않으면서
내부 구조를 개선하는 것이다. 리팩터링의 목적은 코드를
이해하기 쉽고 수정하기 쉽게 만드는 것이다..."
너무 작은 청크 (50 토큰):
"리팩터링은 소프트웨어의 외부 동작을" --> 의미 불완전
너무 큰 청크 (2000 토큰):
"리팩터링은 ... (1장 전체 내용) ..." --> 벡터가 희석됨
적절한 청크 (512 토큰):
"리팩터링은 소프트웨어의 외부 동작을 변경하지 않으면서
내부 구조를 개선하는 것이다. 리팩터링의 목적은 코드를
이해하기 쉽고 수정하기 쉽게 만드는 것이다. 주요 기법으로는
메서드 추출, 변수 인라인, 함수 이동 등이 있다..." --> 문맥 보존가장 단순한 방식으로, 지정된 문자 수 또는 토큰 수 단위로 텍스트를 자릅니다. 구현이 쉽고 예측 가능하지만, 문장이나 단락의 중간에서 잘리는 문제가 있습니다.
def fixed_size_chunk(text, chunk_size=500, overlap=50):
chunks = []
start = 0
while start < len(text):
end = start + chunk_size
chunks.append(text[start:end])
start = end - overlap
return chunksLangChain에서 기본 제공하는 방식으로, 계층적인 구분자 목록을 순서대로 적용합니다. 먼저 단락 구분(\n\n)으로 나누고, 결과가 너무 크면 줄바꿈(\n), 마침표(. ), 공백( ) 순으로 더 작은 단위로 분할합니다.
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=512,
chunk_overlap=50,
separators=["\n\n", "\n", ". ", " ", ""],
length_function=len
)
chunks = splitter.split_text(document_text)2026년 초 발표된 대규모 벤치마크에서, 재귀적 문자 분할(512 토큰, 50~100 토큰 오버랩)이 69%의 정확도를 기록하며 가장 안정적인 기본 전략으로 확인되었습니다.
특별한 이유가 없다면 재귀적 문자 분할을 기본 전략으로 시작하세요. 512 토큰, 50 토큰 오버랩 설정이 벤치마크 검증된 합리적인 출발점입니다. 이후 자체 평가 결과에 따라 조정하시면 됩니다.
의미적으로 유사한 문장들을 하나의 청크로 묶는 방식입니다. 인접 문장 간의 임베딩 유사도를 계산하고, 유사도가 급격히 떨어지는 지점에서 청크를 분리합니다.
from langchain_experimental.text_splitter import SemanticChunker
from langchain.embeddings import OpenAIEmbeddings
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
semantic_splitter = SemanticChunker(
embeddings,
breakpoint_threshold_type="percentile",
breakpoint_threshold_amount=95
)
chunks = semantic_splitter.split_text(document_text)이론적으로 매력적이지만, 실제 벤치마크에서는 주의가 필요합니다. 시맨틱 청킹은 생성되는 청크 크기가 불균일하며, 평균 43 토큰 수준의 작은 조각이 많이 만들어져 54%의 정확도에 그치는 경우가 보고되었습니다. 또한 임베딩 모델 호출이 필요하므로 인덱싱 비용이 증가합니다.
마크다운, HTML 등 구조화된 문서의 경우, 헤딩(h1, h2, h3)이나 HTML 태그를 기준으로 분할하면 논리적 단위를 보존할 수 있습니다.
from langchain.text_splitter import MarkdownHeaderTextSplitter
headers_to_split_on = [
("#", "제목"),
("##", "섹션"),
("###", "하위섹션"),
]
md_splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=headers_to_split_on
)
chunks = md_splitter.split_text(markdown_text)
# 각 청크에 메타데이터로 헤딩 계층 구조가 포함됨구조 기반 분할은 기술 문서, API 문서, 위키 등 체계적으로 작성된 문서에 특히 효과적입니다. 단, 구조가 없는 평문이나 비정형 문서에는 적용할 수 없습니다.
LLM을 활용하여 문서의 내용을 이해하고 최적의 분할 지점을 판단하는 방식입니다. 가장 높은 품질의 청크를 생성할 수 있지만, LLM 호출 비용이 발생하므로 대규모 코퍼스에는 적합하지 않습니다.
from openai import OpenAI
client = OpenAI()
def agentic_chunk(text):
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "system",
"content": """주어진 텍스트를 의미적으로 독립적인 청크로 분할하세요.
각 청크는 하나의 주제나 개념을 다루어야 합니다.
청크 사이에 ---를 넣어 구분하세요."""
}, {
"role": "user",
"content": text
}]
)
chunks = response.choices[0].message.content.split("---")
return [chunk.strip() for chunk in chunks if chunk.strip()]
# 비용이 높으므로 소규모 고가치 문서에만 적용
chunks = agentic_chunk(important_document)청크 간 오버랩(Overlap)은 인접 청크의 경계에서 발생하는 문맥 손실을 완화하기 위한 기법입니다. 앞 청크의 마지막 일부와 다음 청크의 시작 일부가 겹치도록 합니다.
청크 1: [AAAAAAAAAA]
청크 2: [BBBBBBBBB] (오버랩 영역: AB)
청크 3: [CCCCCCCCC]그러나 2026년 1월 발표된 체계적 분석에서는, SPLADE(학습된 희소 검색) 기반 검색에서 오버랩이 재현율(Recall)에 측정 가능한 이점을 제공하지 않으며, 저장 공간과 임베딩 비용만 증가시킨다는 결과가 보고되었습니다.
오버랩은 벡터 검색에서는 여전히 도움이 될 수 있지만, 만능 해결책은 아닙니다. 50~100 토큰의 적절한 오버랩으로 시작하되, 자체 평가를 통해 실제 효과를 확인하세요. 오버랩을 늘리는 것보다 청크 크기와 분할 전략을 최적화하는 것이 더 큰 효과를 줄 수 있습니다.
Late Chunking은 기존 청킹의 근본적인 문제인 문맥 손실을 다른 방향에서 해결합니다. 기존 방식은 먼저 문서를 자른 뒤 각 조각을 독립적으로 임베딩합니다. Late Chunking은 순서를 뒤집어, 먼저 전체 문서를 긴 컨텍스트 임베딩 모델에 통째로 넣고, 모든 토큰의 컨텍스트 인식 표현을 얻은 다음, 그 표현을 청크 단위로 분할하여 각 청크의 벡터를 생성합니다.
기존 방식:
문서 --> 청킹 --> 각 청크 독립 임베딩
Late Chunking:
문서 --> 전체 문서 임베딩 --> 토큰별 표현 획득 --> 청크 경계로 분할 --> 풀링이 방식의 핵심 장점은, 각 청크의 벡터가 문서 전체의 맥락을 반영한다는 것입니다. "이 방법은"이라는 대명사가 포함된 청크도, 문서 앞부분에서 설명된 구체적인 방법의 맥락을 벡터에 담고 있습니다.
청크에 메타데이터를 추가하면 검색 품질을 크게 향상시킬 수 있습니다. 문서 제목, 출처, 날짜, 카테고리, 그리고 청크가 속한 문서의 계층 구조 정보 등을 함께 저장합니다.
def create_enriched_chunks(document, chunks):
enriched = []
for i, chunk in enumerate(chunks):
enriched.append({
"content": chunk,
"metadata": {
"source": document.metadata.get("source"),
"title": document.metadata.get("title"),
"section": document.metadata.get("section"),
"chunk_index": i,
"total_chunks": len(chunks),
"date": document.metadata.get("date"),
}
})
return enrichedAnthropic이 2024년에 제안한 Contextual Retrieval 기법은, 각 청크 앞에 문서 전체 맥락을 요약한 문장을 추가합니다. LLM을 사용하여 "이 청크가 문서 전체에서 어떤 위치와 역할을 하는지"를 설명하는 접두사를 생성합니다.
def add_context_prefix(chunk, full_document):
"""각 청크에 문서 맥락을 설명하는 접두사를 추가"""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "system",
"content": """다음 문서에서 추출된 청크에 대해,
이 청크가 문서 전체에서 어떤 맥락에 위치하는지
1-2문장으로 간결하게 설명하세요."""
}, {
"role": "user",
"content": "문서:\n" + full_document[:3000]
+ "\n\n청크:\n" + chunk
}]
)
context = response.choices[0].message.content
return context + "\n\n" + chunk문맥적 청킹은 검색 품질을 크게 향상시키지만, 모든 청크에 대해 LLM 호출이 필요합니다. 비용을 고려하여 GPT-4o-mini나 Claude 3.5 Haiku 같은 경량 모델을 사용하는 것이 좋습니다.
| 전략 | 벤치마크 정확도 | 구현 복잡도 | 비용 | 적합한 상황 |
|---|---|---|---|---|
| 고정 크기 | 60~65% | 매우 낮음 | 최소 | 빠른 프로토타이핑 |
| 재귀적 문자 분할 | ~69% | 낮음 | 최소 | 범용 기본 전략 |
| 시맨틱 | ~54% | 중간 | 임베딩 비용 | 주의 필요 |
| 구조 기반 | 65~70% | 낮음 | 최소 | 구조화된 문서 |
| 에이전트 기반 | 70~75% | 높음 | LLM 비용 | 소규모 고가치 문서 |
| 문맥적 청킹 | 75%+ | 중간 | LLM 비용 | 프로덕션 고품질 |
프로덕션 환경에서는 단일 전략보다 복합 전략이 효과적입니다.
from langchain.text_splitter import (
RecursiveCharacterTextSplitter,
MarkdownHeaderTextSplitter
)
def production_chunking_pipeline(document):
"""프로덕션용 복합 청킹 파이프라인"""
text = document.page_content
doc_type = document.metadata.get("type", "plain")
# 1단계: 문서 유형에 따른 1차 분할
if doc_type == "markdown":
primary_splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=[
("#", "h1"),
("##", "h2"),
("###", "h3"),
]
)
sections = primary_splitter.split_text(text)
else:
sections = [text]
# 2단계: 재귀적 분할로 크기 조정
secondary_splitter = RecursiveCharacterTextSplitter(
chunk_size=512,
chunk_overlap=50,
separators=["\n\n", "\n", ". ", " "]
)
final_chunks = []
for section in sections:
content = section if isinstance(section, str) else section.page_content
sub_chunks = secondary_splitter.split_text(content)
for chunk in sub_chunks:
final_chunks.append({
"content": chunk,
"metadata": {
"source": document.metadata.get("source"),
"doc_type": doc_type,
}
})
return final_chunks청킹은 RAG 시스템 성능의 핵심 변수입니다. 2026년 벤치마크 기준, 재귀적 문자 분할(512 토큰, 50 토큰 오버랩)이 가장 안정적인 기본 전략이며, 문맥적 청킹을 추가하면 품질을 더 높일 수 있습니다. 시맨틱 청킹은 이론적으로 매력적이지만 실제 벤치마크에서는 기대 이하의 결과를 보이는 경우가 많으므로 주의가 필요합니다.
다음 장에서는 생성된 임베딩 벡터를 저장하고 검색하는 벡터 데이터베이스에 대해 다룹니다. Pinecone, Weaviate, Qdrant, pgvector 등 주요 벡터 데이터베이스의 특성을 비교하고, 상황에 맞는 선택 기준을 안내합니다.
이 글이 도움이 되셨나요?
Pinecone, Weaviate, Qdrant, pgvector 등 주요 벡터 데이터베이스의 특성을 비교하고 상황에 맞는 선택 가이드를 제공합니다.
텍스트 임베딩의 원리부터 2026년 최신 모델 벤치마크, 프로덕션 환경에서의 선택 기준까지 체계적으로 안내합니다.
문서 로딩부터 임베딩 생성, 벡터 저장, 유사도 검색까지 RAG 파이프라인의 전체 흐름을 실제 코드로 구현합니다.