RAG 챗봇 만들기 4 - 텍스트 청킹하기
처음에 청크 사이즈를 500자로 했더니 검색 정확도가 영 별로였어요. 문장이 잘리면서 의미가 끊기더라고요. 1000자로 올리고 overlap 200 주니까 훨씬 나아졌어요. 청킹 설정이 생각보다 RAG 성능에 영향을 많이 줘요.
오늘은 문서를 적당한 크기로 쪼개는 청킹(Chunking)을 배울 거예요. 왜 필요한지부터 시작해볼게요.
왜 청킹이 필요해요?
이유 1: LLM 컨텍스트 제한
LLM은 한 번에 처리할 수 있는 텍스트 양이 정해져 있어요 (토큰 제한).
- GPT-3.5: 약 4,000 토큰 (~3,000자)
- GPT-4: 약 8,000~128,000 토큰
100페이지짜리 문서를 통째로 넣을 수가 없어요.
이유 2: 검색 정확도
작은 단위로 쪼개야 관련 있는 부분만 정확하게 찾을 수 있어요.
[전체 문서 100페이지]
→ "연차 규정" 검색
→ 전체 문서가 결과로 나옴 (너무 넓음)
[1000자씩 쪼갠 청크들]
→ "연차 규정" 검색
→ 연차 관련 청크만 정확하게 찾음
이유 3: 비용 절감
LLM API는 토큰 단위로 과금돼요. 필요한 부분만 보내면 비용이 줄어요.
Text Splitter 종류
LangChain에서 제공하는 스플리터들이에요:
1. CharacterTextSplitter
단순히 글자 수로 자르는 방식:
from langchain.text_splitter import CharacterTextSplitter
splitter = CharacterTextSplitter(
chunk_size=1000, # 청크 크기
chunk_overlap=200, # 겹치는 부분
separator="\n" # 자르는 기준
)
2. RecursiveCharacterTextSplitter (추천)
문단 → 문장 → 단어 순으로 자연스럽게 자르는 방식:
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
separators=["\n\n", "\n", ". ", " ", ""]
)
얘가 제일 많이 쓰여요. 문장이 중간에 잘리는 걸 최대한 방지해줘요.
실습: 문서 청킹하기
기본 청킹
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import PyPDFLoader
# 1. 문서 로드
loader = PyPDFLoader("docs/sample.pdf")
documents = loader.load()
print(f"원본 문서: {len(documents)}개 페이지")
# 2. 청킹
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200
)
chunks = splitter.split_documents(documents)
print(f"청킹 후: {len(chunks)}개 청크")
# 3. 결과 확인
print(f"\n첫 번째 청크:")
print(f"길이: {len(chunks[0].page_content)}자")
print(f"내용: {chunks[0].page_content[:200]}...")
print(f"메타데이터: {chunks[0].metadata}")
실행 결과:
chunk_overlap이 뭐예요?
청크가 겹치는 부분이에요. 왜 겹쳐야 할까요?
overlap 없이 자르면:
[청크1: ...중요한 내용의 앞부분] [청크2: 뒷부분은 여기에...]
→ 문맥이 끊겨서 의미 파악이 어려움
overlap 있으면:
[청크1: ...중요한 내용의 앞부분 | 중간 겹침]
[청크2: 중간 겹침 | 뒷부분은 여기에...]
→ 겹치는 부분이 있어서 문맥이 이어짐
보통 chunk_size의 10~20% 정도로 설정해요.
chunk_size 정하기
정답은 없지만, 경험상 이래요:
| 용도 | chunk_size | chunk_overlap |
|---|---|---|
| 짧은 Q&A | 500 | 50 |
| 일반 문서 | 1000 | 200 |
| 긴 맥락 필요 | 2000 | 400 |
저는 보통 1000/200으로 시작하고, 검색 품질 보면서 조절해요.
청킹 품질 확인하기
청크가 잘 나뉘었는지 확인하는 코드:
def analyze_chunks(chunks):
"""청크 분석"""
lengths = [len(c.page_content) for c in chunks]
print(f"총 청크 수: {len(chunks)}")
print(f"평균 길이: {sum(lengths) / len(lengths):.0f}자")
print(f"최소 길이: {min(lengths)}자")
print(f"최대 길이: {max(lengths)}자")
# 너무 짧은 청크 확인
short_chunks = [c for c in chunks if len(c.page_content) < 100]
if short_chunks:
print(f"\n⚠️ 100자 미만 청크 {len(short_chunks)}개:")
for c in short_chunks[:3]:
print(f" - {c.page_content[:50]}...")
analyze_chunks(chunks)
실행 결과:
메타데이터 활용하기
청크에 추가 정보를 붙이면 나중에 유용해요:
from langchain.text_splitter import RecursiveCharacterTextSplitter
def chunk_with_metadata(documents, source_name):
"""메타데이터 추가하며 청킹"""
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200
)
chunks = splitter.split_documents(documents)
# 각 청크에 추가 메타데이터
for i, chunk in enumerate(chunks):
chunk.metadata["chunk_id"] = i
chunk.metadata["source_name"] = source_name
chunk.metadata["chunk_size"] = len(chunk.page_content)
return chunks
# 사용
chunks = chunk_with_metadata(documents, "회사규정집")
print(chunks[0].metadata)
실행 결과:
전체 파이프라인 코드
지금까지 배운 걸 합치면:
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
def load_and_chunk(file_path: str, chunk_size: int = 1000) -> list:
"""문서 로드 + 청킹 파이프라인"""
# 1. 로드
loader = PyPDFLoader(file_path)
documents = loader.load()
print(f"✓ {len(documents)}개 페이지 로드")
# 2. 청킹
splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=int(chunk_size * 0.2) # 20% overlap
)
chunks = splitter.split_documents(documents)
print(f"✓ {len(chunks)}개 청크 생성")
# 3. 메타데이터 추가
for i, chunk in enumerate(chunks):
chunk.metadata["chunk_id"] = i
return chunks
# 실행
chunks = load_and_chunk("docs/sample.pdf")
운영자 실전 노트
실제 프로젝트 진행하며 겪은 문제
- chunk_size 500으로 시작 → 문맥 잘려서 검색 품질 저하. 1000으로 올리니 개선
- overlap 0으로 설정 → 문장이 청크 경계에서 잘림. 200(20%)으로 설정하니 문맥 연결 개선
- 표와 코드 블록이 청크 중간에 잘림 → separators에
\n\n\n추가해서 구조 보존 - 너무 짧은 청크(<100자) 다수 발생 → 품질 저하. 후처리로 짧은 청크 합치기 필요
이 경험을 통해 알게 된 점
- chunk_size는 문서 특성에 따라 다름. 여러 값(500/1000/1500)으로 테스트 필수
- overlap은 10~20%가 적정선. 너무 크면 중복 데이터로 비용 증가
- RecursiveCharacterTextSplitter가 CharacterTextSplitter보다 문맥 보존 우수
마무리
청킹 설정은 RAG 성능에 직접적인 영향을 준다. 기본값으로 시작하되, 실제 검색 결과를 보며 튜닝하는 게 중요하다.
다음 편에서는 텍스트를 숫자 벡터로 바꾸는 “임베딩”을 배운다. 왜 텍스트를 숫자로 바꿔야 하는지, 어떻게 작동하는지 알아본다.
RAG 챗봇 만들기 시리즈:
← 블로그 목록으로