Chunking
Text Chunking / Document Splitting
긴 문서를 작은 조각(청크)으로 나누는 기법입니다. RAG(검색 증강 생성) 시스템에서 임베딩 생성 전 필수적으로 수행되며, 청크 크기와 오버랩 설정이 검색 품질에 결정적인 영향을 미칩니다.
Text Chunking / Document Splitting
긴 문서를 작은 조각(청크)으로 나누는 기법입니다. RAG(검색 증강 생성) 시스템에서 임베딩 생성 전 필수적으로 수행되며, 청크 크기와 오버랩 설정이 검색 품질에 결정적인 영향을 미칩니다.
Chunking(청킹)은 긴 텍스트 문서를 더 작은 단위인 "청크(chunk)"로 분할하는 전처리 기법입니다. LLM(대규모 언어 모델)과 임베딩 모델은 처리할 수 있는 토큰 수에 제한이 있고, 긴 문서를 통째로 임베딩하면 의미가 희석되기 때문에, 적절한 크기로 나누는 것이 필수입니다. RAG 시스템의 검색 품질은 청킹 전략에 크게 좌우됩니다.
Chunking의 필요성은 LLM의 컨텍스트 윈도우 제한과 임베딩의 특성에서 비롯됩니다. 예를 들어, OpenAI의 text-embedding-3-small 모델은 8191 토큰까지만 처리할 수 있고, 너무 긴 텍스트를 하나의 임베딩으로 압축하면 세부 정보가 손실됩니다. 적절한 크기의 청크로 나누면 각 청크가 특정 주제에 집중하게 되어 검색 정확도가 향상됩니다.
청킹 방법은 여러 가지가 있습니다. 가장 기본적인 것은 고정 크기 분할로, 일정 문자 수나 토큰 수로 자르는 방식입니다. 하지만 이는 문장 중간에서 잘릴 수 있어 의미가 깨질 수 있습니다. 이를 보완하기 위해 문장 단위, 문단 단위, 또는 마크다운 헤더 기준 분할 등 구조 기반 청킹이 사용됩니다. 최근에는 시맨틱 청킹(의미 단위 분할)도 주목받고 있으며, 문맥의 연속성을 위해 청크 간 오버랩(overlap)을 두는 것도 중요합니다.
실무에서 청킹 전략은 데이터 특성에 따라 달라집니다. 기술 문서나 법률 문서는 섹션/조항 단위로, 대화 로그는 발화 단위로, 코드는 함수/클래스 단위로 나누는 것이 효과적입니다. 일반적으로 청크 크기 500-1500 토큰, 오버랩 10-20% 정도가 권장되지만, 실제 검색 성능을 측정하며 최적값을 찾아야 합니다. LangChain, LlamaIndex 등의 프레임워크는 다양한 청킹 전략을 쉽게 적용할 수 있는 도구를 제공합니다.
다양한 청킹 방법을 구현하는 Python 코드입니다.
# pip install langchain langchain-text-splitters tiktoken
from langchain_text_splitters import (
RecursiveCharacterTextSplitter,
CharacterTextSplitter,
TokenTextSplitter,
MarkdownHeaderTextSplitter
)
import tiktoken
# 예시 문서
document = """
# 인공지능 개요
인공지능(AI)은 인간의 학습능력, 추론능력, 언어이해능력 등을
컴퓨터 프로그램으로 실현한 기술입니다.
## 머신러닝
머신러닝은 데이터로부터 패턴을 학습하여 예측하는 AI의 하위 분야입니다.
지도학습, 비지도학습, 강화학습으로 나뉩니다.
### 딥러닝
딥러닝은 인공신경망을 여러 층으로 쌓아 복잡한 패턴을 학습하는 기법입니다.
CNN, RNN, Transformer 등의 아키텍처가 있습니다.
## 자연어 처리
자연어 처리(NLP)는 컴퓨터가 인간의 언어를 이해하고 생성하는 기술입니다.
텍스트 분류, 번역, 요약, 질문 응답 등에 활용됩니다.
"""
# 1. 재귀적 문자 분할 (가장 추천되는 방법)
recursive_splitter = RecursiveCharacterTextSplitter(
chunk_size=200, # 청크 최대 크기 (문자 수)
chunk_overlap=50, # 청크 간 오버랩 (문맥 유지)
length_function=len,
separators=["\n\n", "\n", ".", " ", ""] # 분할 우선순위
)
chunks = recursive_splitter.split_text(document)
print("=== 재귀적 문자 분할 ===")
for i, chunk in enumerate(chunks):
print(f"Chunk {i+1} ({len(chunk)}자): {chunk[:50]}...")
# 2. 토큰 기반 분할 (토큰 제한 고려 시)
token_splitter = TokenTextSplitter(
encoding_name="cl100k_base", # GPT-4/3.5용 토크나이저
chunk_size=100, # 최대 토큰 수
chunk_overlap=20 # 오버랩 토큰 수
)
token_chunks = token_splitter.split_text(document)
print("\n=== 토큰 기반 분할 ===")
for i, chunk in enumerate(token_chunks):
# 토큰 수 확인
enc = tiktoken.get_encoding("cl100k_base")
tokens = len(enc.encode(chunk))
print(f"Chunk {i+1} ({tokens} tokens): {chunk[:40]}...")
# 3. 마크다운 헤더 기반 분할 (구조화된 문서에 적합)
headers_to_split_on = [
("#", "Header 1"),
("##", "Header 2"),
("###", "Header 3"),
]
markdown_splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=headers_to_split_on
)
md_chunks = markdown_splitter.split_text(document)
print("\n=== 마크다운 헤더 기반 분할 ===")
for i, chunk in enumerate(md_chunks):
print(f"Chunk {i+1}: {chunk.metadata} - {chunk.page_content[:40]}...")
# 4. 시맨틱 청킹 (의미 단위 분할) - 고급 방법
from langchain_openai import OpenAIEmbeddings
from langchain_experimental.text_splitter import SemanticChunker
# 의미적 유사도 기반 분할
semantic_splitter = SemanticChunker(
embeddings=OpenAIEmbeddings(),
breakpoint_threshold_type="percentile", # 분할 기준
breakpoint_threshold_amount=95 # 임계값
)
# 5. 커스텀 청킹 함수
def custom_chunker(
text: str,
chunk_size: int = 500,
chunk_overlap: int = 100,
min_chunk_size: int = 50
) -> list:
"""
문장 경계를 존중하는 커스텀 청커
Args:
text: 분할할 텍스트
chunk_size: 목표 청크 크기
chunk_overlap: 오버랩 크기
min_chunk_size: 최소 청크 크기
Returns:
청크 리스트
"""
import re
# 문장 단위로 분리
sentences = re.split(r'(?<=[.!?])\s+', text)
chunks = []
current_chunk = ""
for sentence in sentences:
if len(current_chunk) + len(sentence) <= chunk_size:
current_chunk += " " + sentence if current_chunk else sentence
else:
if len(current_chunk) >= min_chunk_size:
chunks.append(current_chunk.strip())
# 오버랩 적용
overlap_text = current_chunk[-chunk_overlap:] if len(current_chunk) > chunk_overlap else current_chunk
current_chunk = overlap_text + " " + sentence
if len(current_chunk) >= min_chunk_size:
chunks.append(current_chunk.strip())
return chunks
custom_chunks = custom_chunker(document, chunk_size=300, chunk_overlap=50)
print("\n=== 커스텀 문장 기반 분할 ===")
for i, chunk in enumerate(custom_chunks):
print(f"Chunk {i+1} ({len(chunk)}자): {chunk[:50]}...")
고정 크기로 무작정 자르면 문장 중간에서 끊기거나 중요한 문맥이 분리될 수 있습니다. RecursiveCharacterTextSplitter를 사용하여 문장/문단 경계를 존중하고, 적절한 오버랩(10-20%)을 설정하세요. 시맨틱 청킹도 고려해보세요.
청크가 너무 작으면 문맥 부족, 너무 크면 의미 희석과 토큰 제한 문제가 발생합니다. 정답은 없으며, 실제 검색 성능을 평가하며 최적값을 찾아야 합니다. 일반적으로 500-1500 토큰이 시작점입니다.
문자 수 기준 청킹은 한글의 경우 토큰 수와 차이가 큽니다. 한글 1글자는 보통 1-3토큰입니다. 임베딩 모델의 토큰 제한을 정확히 지키려면 tiktoken 등으로 실제 토큰 수를 계산하거나 TokenTextSplitter를 사용하세요.