RAG 시스템이 엉터리 답변을 내놓을 때 대부분 이렇게 생각해요.
"임베딩 모델 바꿔볼까?"
"프롬프트 더 정교하게 써야겠다"
"LLM을 더 좋은 걸로 바꾸면 되겠지"
근데 실제로 RAG 실패의 80%는 청킹 문제예요.
잘못 자른 청크가 들어가면 아무리 좋은 LLM도 엉터리 답변을 낼 수밖에 없어요.
청킹이 뭔가
RAG는 문서를 통째로 넣을 수 없어요. 임베딩 모델의 입력 한계가 있고, 벡터 검색 정확도도 떨어져요.
원문 (10,000 토큰):
"회사 규정 전체 문서..."
청킹:
청크 1: "출근 시간은 오전 9시..." (512토큰)
청크 2: "연차 사용 규정은..." (512토큰)
청크 3: "복리후생 제도는..." (512토큰)
...
질문: "연차 며칠이야?"
→ 청크 2만 검색됨
→ 관련 내용만 LLM에 전달
→ 정확한 답변
청크가 너무 작으면 컨텍스트 부족, 너무 크면 관련 없는 내용이 섞여요.
2026년 벤치마크 결과
Vecta가 학술 논문 50개, 905,746 토큰으로 진행한 2026년 2월 벤치마크예요.
전략별 정답률:
Recursive 512토큰: 69% ← 1위
Page-level: 64%
Token-based 256토큰: 61%
Semantic chunking: 54%
Fixed 128토큰: 49%
충격적인 결과: 더 "스마트한" 시맨틱 청킹이 단순한 Recursive보다 낮아요.
이유: 시맨틱 청킹이 평균 43토큰 짜리 작은 조각들을 만들어서 LLM이 답하기에 컨텍스트가 너무 부족했어요.
전략 1 — Fixed-size Chunking (고정 크기)
# 가장 단순한 방법
def fixed_chunk(text: str, size: int = 512, overlap: int = 50):
tokens = text.split()
chunks = []
for i in range(0, len(tokens), size - overlap):
chunk = " ".join(tokens[i:i + size])
chunks.append(chunk)
return chunks
장점: 빠름, 예측 가능
단점: 문장 중간에 잘릴 수 있음
"오늘 회의에서 | 결정된 사항은 세 가지"
→ 앞뒤 청크 모두 의미 불완전
프로토타이핑할 때 쓰고, 바로 Recursive로 이동해요.
전략 2 — Recursive Character Splitting (권장 기본값)
계층적 구분자로 자연스러운 경계에서 먼저 분할해요.
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=512,
chunk_overlap=50, # 10% overlap
separators=[
"\n\n", # 단락 먼저
"\n", # 줄바꿈
". ", # 문장
" ", # 단어
"" # 문자 (최후 수단)
]
)
chunks = splitter.split_text(document)
우선순위:
1. 단락 경계에서 자름 (\n\n)
2. 그래도 크면 줄바꿈 (\n)
3. 그래도 크면 문장 (.)
4. 그래도 크면 단어
→ 가능한 한 의미 단위 유지
2026년 벤치마크 1위. 대부분 상황에서 이걸로 시작해요.
전략 3 — Document-aware Chunking (구조 인식)
Markdown, HTML 같은 구조화된 문서에 써요.
from langchain.text_splitter import MarkdownHeaderTextSplitter
headers_to_split = [
("#", "H1"),
("##", "H2"),
("###", "H3"),
]
splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=headers_to_split
)
chunks = splitter.split_text(markdown_doc)
# 결과 예시:
# {
# "content": "연차 사용 규정...",
# "metadata": {"H1": "복리후생", "H2": "휴가 제도", "H3": "연차"}
# }
메타데이터에 헤더 정보가 붙어서 검색 정확도가 올라가요.
활용 예시:
기술 문서 (Markdown)
API 문서
사내 위키
코드 문서
→ 구조 있는 문서면 무조건 이걸 써요
전략 4 — Semantic Chunking
의미적으로 유사한 문장들을 묶어요.
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai.embeddings import OpenAIEmbeddings
splitter = SemanticChunker(
OpenAIEmbeddings(),
breakpoint_threshold_type="percentile",
breakpoint_threshold_amount=95
)
chunks = splitter.split_text(text)
장점: 의미적으로 완결된 청크
단점:
- 임베딩 모델 호출 비용 발생
- 청크 크기가 들쭉날쭉 (평균 43토큰 문제)
- 벤치마크에서 오히려 낮은 성능
주의: 최소 청크 크기 설정 필수
min_chunk_size = 200 # 이 이하면 병합
주제 전환이 많은 비정형 문서(회의록, 인터뷰)에 유용해요. 일반 문서는 Recursive가 더 나아요.
전략 5 — Late Chunking (2024년 등장, 2026년 주목)
기존 방식과 순서를 바꿔요.
기존 방식 (Early Chunking):
문서 → 청크로 자름 → 각 청크 임베딩 → 벡터 DB
Late Chunking:
문서 → 전체 임베딩 생성 → 청크 단위로 풀링
# Late Chunking 개념 코드
# (Jina AI의 jina-embeddings-v3에서 지원)
from jina import Document
doc = Document(text=long_document)
# 문서 전체를 한번에 인코딩 (컨텍스트 유지)
# 청킹은 임베딩 후에 진행
chunks = doc.late_chunk(chunk_size=512)
장점: 청크가 전체 문서 컨텍스트를 유지해요. "그것", "이것" 같은 대명사 참조 문제가 줄어요.
단점: 아직 모든 프레임워크에서 지원 안 됨.
전략 6 — LLM 기반 청킹 (고비용, 고정밀)
LLM이 직접 의미 단위로 청크를 나눠요.
def llm_chunk(document: str, llm) -> list[str]:
prompt = f"""다음 문서를 의미적으로 완결된 단위로 나눠줘.
각 청크는 독립적으로 이해될 수 있어야 해.
JSON 배열로 반환해줘.
문서:
{document}"""
response = llm.invoke(prompt)
return json.loads(response)
장점: 가장 정확한 청크
단점:
- 문서당 LLM 호출 비용 발생
- 대용량 문서에 적용 불가
- 실시간 인제스션 불가
추천 용도:
- 법률 문서
- 의료 문서
- 금융 컴플라이언스 문서
→ 오답의 비용이 큰 고가치 도메인
청크 크기 선택 기준
쿼리 유형별 최적 크기:
사실 기반 질문 ("몇 명이야?", "언제야?"):
→ 256~512토큰 (작은 청크, 정확한 검색)
분석/설명 질문 ("왜 그런거야?", "어떻게 작동해?"):
→ 1,024토큰+ (큰 청크, 충분한 컨텍스트)
혼합 사용:
→ 512토큰 기본값으로 시작
오버랩:
→ 10~20% (512토큰이면 50~100토큰)
→ SPLADE 같은 Sparse Retrieval은 오버랩 효과 없음
Small-to-Large 패턴 — 검색과 생성 분리
작은 청크로 검색하고, 큰 청크를 LLM에 전달해요.
from llama_index.core.node_parser import HierarchicalNodeParser
parser = HierarchicalNodeParser.from_defaults(
chunk_sizes=[512, 1024, 2048]
)
nodes = parser.get_nodes_from_documents(documents)
# 검색: 512토큰 청크로 정밀 검색
# 컨텍스트 전달: 부모 1024토큰 청크를 LLM에 전달
# 결과:
# 검색 정확도 ↑ (작은 청크)
# 생성 품질 ↑ (충분한 컨텍스트)
전략 선택 가이드
문서 종류별 추천:
Markdown, HTML, 기술 문서:
→ MarkdownHeaderTextSplitter (구조 인식)
일반 텍스트, PDF:
→ RecursiveCharacterTextSplitter 512토큰
회의록, 인터뷰, 비정형 문서:
→ SemanticChunker (최소 크기 200토큰 설정)
법률/의료/금융 고정밀 문서:
→ LLM 기반 청킹
긴 문서 + 대명사 참조 많음:
→ Late Chunking (Jina embeddings)
모르겠으면:
→ RecursiveCharacterTextSplitter 512토큰, 오버랩 50토큰
청킹 평가 방법
어떤 전략이 맞는지 데이터로 확인해야 해요.
from ragas import evaluate
from ragas.metrics import context_precision, context_recall
# 대표 쿼리 50~100개로 테스트
test_cases = [
{"question": "연차는 며칠이야?", "answer": "15일"},
# ...
]
results = evaluate(
dataset=test_cases,
metrics=[context_precision, context_recall]
)
# 전략 A vs B 비교
print(f"Recursive 512: {results_a['context_recall']:.2f}")
print(f"Semantic: {results_b['context_recall']:.2f}")
청킹 전략 바꾸면 전체 재임베딩이 필요해요. 처음에 제대로 테스트하는 게 중요해요.
📌 관련 글
Agentic RAG 동작 원리
쿼리 재작성, 반복 검색, 멀티소스 라우팅 — Agentic RAG 동작 원리와 동적 검색 전략 완전 정리
RAG 시스템을 만들고 나면 이런 한계가 생겨요."단순한 질문은 잘 답하는데, '2024년 실적을 바탕으로 2025년 전략을 분석해줘' 같은 복잡한 질문은 엉뚱한 답이 나온다."이건 일반 RAG의 구조적 한계
cell-devlog.tistory.com
RAG 실패 이유 7가지
RAG 데모는 잘 되는데 배포하면 망하는 이유 7가지 — 원인별 해결법, 프로덕션 RAG 완전 가이드
RAG 데모는 항상 잘 돼요.PDF 몇 개 넣고, 벡터 DB 연결하고, LLM 붙이면 마법처럼 답이 나와요. 팀이 흥분하고, 경영진이 빠른 배포를 요구해요.그리고 3개월 뒤, 시스템이 무너지기 시작해요.데이터
cell-devlog.tistory.com
'RAG' 카테고리의 다른 글
| 컨텍스트 엔지니어링 — 프롬프트 엔지니어링의 다음 단계 (2) | 2026.04.09 |
|---|---|
| RAG 데모는 잘 되는데 배포하면 망하는 이유 7가지 — 원인별 해결법, 프로덕션 RAG 완전 가이드 (2) | 2026.04.09 |
| 쿼리 재작성, 반복 검색, 멀티소스 라우팅 — Agentic RAG 동작 원리와 동적 검색 전략 완전 정리 (0) | 2026.03.25 |
| 벡터 검색 정확도 올리는 법 — 임베딩 모델 선택부터 HNSW 튜닝, Reranking까지 (0) | 2026.03.24 |
| 기존 RAG의 한계를 그래프로 돌파한다 — Graph RAG 동작 원리 완전 정리 (0) | 2026.03.24 |