RAG를 위한 청킹 전략(Chunking Strategies for RAG)
청킹(chunking) 설정은 임베딩 모델(embedding model) 선택만큼이나 검색 품질(retrieval quality)에 큰 영향을 줍니다(Vectara NAACL 2025). 청킹을 잘못 잡으면 어떤 리랭킹(reranking)으로도 만회할 수 없습니다.
유형: Build 언어: Python 선수 강의: Phase 5 · 14 (정보 검색, Information Retrieval), Phase 5 · 22 (임베딩 모델, Embedding Models) 예상 시간: 약 60분
문제(The Problem)
50페이지 분량의 계약서(contract)를 RAG 시스템에 넣었다고 가정해 봅시다. 사용자가 "What is the termination clause?"(해지 조항이 무엇인가요?)라고 묻습니다. 그런데 검색기(retriever)는 표지 페이지를 반환합니다. 왜 그럴까요? 모델은 512토큰(token) 단위의 청크(chunk) 위에서 학습되어 있고, 정작 해지 조항은 20페이지쯤 안쪽에서 페이지 경계(page break)를 가로질러 잘려 있으며, 질의(query)와 이어 줄 만한 지역적 키워드(local keyword)가 주변에 없기 때문입니다.
해법은 "더 좋은 임베딩 모델을 사 오자"가 아닙니다. 해법은 청킹입니다. 얼마나 크게 자를지, 겹침(overlap)을 둘지, 어디서 자를지, 주변 맥락(surrounding context)을 함께 붙일지 결정해야 합니다.
2026년 2월 벤치마크(benchmark)는 다음과 같이 의외의 결과를 보여 줍니다.
- Vectara의 2026 연구: 재귀(recursive) 방식의 512토큰 청킹이 의미 기반(semantic) 청킹을 정확도 69% → 54%로 이겼습니다.
- Natural Questions 데이터셋에서 SPLADE + Mistral-8B 조합: 겹침은 측정 가능한 이득(benefit)을 전혀 만들어 내지 못했습니다.
- 컨텍스트 절벽(context cliff): 응답 품질은 약 2,500토큰 분량의 컨텍스트 부근에서 급격히 떨어집니다.
"당연해 보이는" 답, 예를 들어 의미 기반 청킹에 20% 겹침, 1,000토큰 같은 조합은 자주 틀린 선택입니다. 이번 강의는 여섯 가지 전략(strategy)에 대한 직관을 만들고, 언제 어떤 전략을 골라야 하는지 알려 드리는 것을 목표로 합니다.
개념(The Concept)
고정 청킹(Fixed chunking). N개의 문자(character) 또는 N개의 토큰마다 자릅니다. 가장 단순한 기준선(baseline)입니다. 문장 중간에서 끊기기 쉽습니다. 압축률은 좋지만 일관성(coherence)은 떨어집니다.
재귀 청킹(Recursive). LangChain의 RecursiveCharacterTextSplitter가 대표적입니다. 먼저 \n\n을 기준으로 자르고, 안 되면 \n, 다음에는 ., 마지막으로 공백 순서로 내려갑니다. 단계별로 깔끔하게 떨어집니다(falls back cleanly). 2026년 기준의 기본값(default)입니다.
의미 기반 청킹(Semantic). 각 문장을 임베딩한 뒤, 인접한 문장들 사이의 코사인 유사도(cosine similarity)를 계산합니다. 유사도가 임계값(threshold) 아래로 떨어지는 지점에서 자릅니다. 주제 일관성을 잘 보존합니다. 다만 더 느리고, 검색을 해치는 40토큰짜리 단편(fragment)을 만들기도 합니다.
문장 청킹(Sentence). 문장 경계에서 자릅니다. 청크당 한 문장을 두거나, N개의 문장을 한 묶음(window)으로 둡니다. 약 5,000토큰 규모까지는 의미 기반 청킹과 비슷한 품질을 훨씬 적은 비용으로 낼 수 있습니다.
부모-자식 청킹(Parent-document). 검색용으로는 작은 자식(child) 청크를 저장하고, 컨텍스트용으로는 더 큰 부모(parent) 청크도 함께 보관합니다. 자식으로 검색하고 부모를 반환합니다. 우아한 성능 저하(graceful degradation)가 가능합니다. 자식 청크가 좋지 않아도 부모는 그럭저럭 쓸 만한 결과를 돌려줍니다.
레이트 청킹(Late chunking, 2024). 전체 문서를 먼저 토큰 단위로 임베딩한 뒤, 그 토큰 임베딩을 풀링(pool)해 청크 임베딩을 만듭니다. 청크 간(cross-chunk) 컨텍스트를 보존합니다. 긴 컨텍스트(long-context) 임베더(BGE-M3, Jina v3 등)와 함께 동작합니다. 계산 비용은 더 큽니다.
컨텍스트 검색(Contextual retrieval, Anthropic, 2024). 각 청크 앞에, 그 청크가 문서 안에서 어디에 위치하는지를 설명하는 LLM 생성 요약을 덧붙입니다. 예: "This chunk is section 3.2 of the termination clauses...". Anthropic 자체 벤치마크에서 검색 성능이 35~50% 향상되었습니다. 인덱싱 비용이 큽니다.
기본값을 이기는 규칙
청크 크기를 질의 유형(query type)에 맞춥니다.
| 질의 유형(query type) | 청크 크기(chunk size) |
|---|---|
| 사실 조회(factoid, "what is the CEO's name?") | 256~512 토큰 |
| 분석/다중 추론(analytical / multi-hop) | 512~1024 토큰 |
| 섹션 전체 이해(whole-section comprehension) | 1024~2048 토큰 |
NVIDIA의 2026 벤치마크 기준입니다. 청크는 답과 그 주변의 지역 맥락까지 담을 수 있을 만큼 충분히 크되, 검색기의 상위 K(top-K) 결과가 컨텍스트 잡음(context noise)이 아니라 정답에 집중할 수 있을 만큼은 작아야 합니다.
직접 만들기(Build It)
Step 1: 고정·재귀 청킹(fixed and recursive chunking)
def chunk_fixed(text, size=512, overlap=0):
step = size - overlap
return [text[i:i + size] for i in range(0, len(text), step)]
def chunk_recursive(text, size=512, seps=("\n\n", "\n", ". ", " ")):
if len(text) <= size:
return [text]
for sep in seps:
if sep not in text:
continue
parts = text.split(sep)
chunks = []
buf = ""
for p in parts:
if len(p) > size:
if buf:
chunks.append(buf)
buf = ""
chunks.extend(chunk_recursive(p, size=size, seps=seps[1:] or (" ",)))
continue
candidate = buf + sep + p if buf else p
if len(candidate) <= size:
buf = candidate
else:
if buf:
chunks.append(buf)
buf = p
if buf:
chunks.append(buf)
return [c for c in chunks if c.strip()]
return chunk_fixed(text, size)
Step 2: 의미 기반 청킹(semantic chunking)
def chunk_semantic(text, encoder, threshold=0.6, min_chars=200, max_chars=2048):
sentences = split_sentences(text)
if not sentences:
return []
embs = encoder.encode(sentences, normalize_embeddings=True)
chunks = [[sentences[0]]]
for i in range(1, len(sentences)):
sim = float(embs[i] @ embs[i - 1])
current_len = sum(len(s) for s in chunks[-1])
if sim < threshold and current_len >= min_chars:
chunks.append([sentences[i]])
else:
chunks[-1].append(sentences[i])
result = []
for group in chunks:
text_group = " ".join(group)
if len(text_group) > max_chars:
result.extend(chunk_recursive(text_group, size=max_chars))
else:
result.append(text_group)
return result
threshold는 자기 도메인(domain)에 맞춰 튜닝합니다. 너무 높으면 단편이 많아지고, 너무 낮으면 거대한 청크 하나로 합쳐집니다.
Step 3: 부모-자식 청킹(parent-document)
def chunk_parent_child(text, parent_size=2048, child_size=256):
parents = chunk_recursive(text, size=parent_size)
mapping = []
for p_idx, parent in enumerate(parents):
children = chunk_recursive(parent, size=child_size)
for child in children:
mapping.append({"child": child, "parent_idx": p_idx, "parent": parent})
return mapping
def retrieve_parent(child_query, mapping, encoder, top_k=3):
child_embs = encoder.encode([m["child"] for m in mapping], normalize_embeddings=True)
q_emb = encoder.encode([child_query], normalize_embeddings=True)[0]
scores = child_embs @ q_emb
top = np.argsort(-scores)[:top_k]
seen, parents = set(), []
for i in top:
if mapping[i]["parent_idx"] not in seen:
parents.append(mapping[i]["parent"])
seen.add(mapping[i]["parent_idx"])
return parents
핵심 포인트는 부모 중복 제거(dedupe)입니다. 여러 자식이 같은 부모에 연결될 수 있는데, 그것들을 모두 반환하면 컨텍스트가 낭비됩니다.
Step 4: 컨텍스트 검색(contextual retrieval, Anthropic 패턴)
def contextualize_chunks(document, chunks, llm):
context_prompts = [
f"""<document>{document}</document>
Here is the chunk to situate: <chunk>{c}</chunk>
Write 50-100 words placing this chunk in the document's context."""
for c in chunks
]
contexts = llm.batch(context_prompts)
return [f"{ctx}\n\n{c}" for ctx, c in zip(contexts, chunks)]
이렇게 컨텍스트가 덧붙은 청크를 인덱싱합니다. 검색 시점에는 주변 정보가 함께 임베딩되어 있기 때문에 검색 품질이 개선됩니다.
Step 5: 평가하기(evaluate)
def recall_at_k(queries, corpus_chunks, encoder, k=5):
chunk_embs = encoder.encode(corpus_chunks, normalize_embeddings=True)
hits = 0
for q_text, gold_idxs in queries:
q_emb = encoder.encode([q_text], normalize_embeddings=True)[0]
top = np.argsort(-(chunk_embs @ q_emb))[:k]
if any(i in gold_idxs for i in top):
hits += 1
return hits / len(queries)
항상 직접 측정합니다. 자기 코퍼스(corpus)에서 가장 좋은 전략은 어떤 블로그 글과도 다를 수 있습니다.
함정(Pitfalls)
- 사실 조회 질의만으로 청킹을 평가하기. 다중 추론(multi-hop) 질의에서는 전혀 다른 승자가 나옵니다. 질의 유형별로 층화(stratify)된 평가 셋을 사용합니다.
- 최소 크기 없이 의미 기반 청킹을 적용하기. 40토큰짜리 단편이 양산되어 검색 품질을 해칩니다. 반드시
min_tokens를 강제합니다. - 카고 컬트(cargo cult)식 겹침. 2026년 연구들은 겹침이 종종 이득을 전혀 주지 않으면서 인덱스 비용만 두 배로 만든다고 보고합니다. 가정하지 말고 직접 측정합니다.
- 최소·최대 강제 없음. 5토큰 청크나 5,000토큰 청크 모두 검색을 망가뜨립니다. 반드시 범위로 묶어 둡니다(clamp).
- 문서 간(cross-doc) 청킹. 하나의 청크가 두 문서를 가로지르지 않게 합니다. 항상 문서 단위로 먼저 청킹한 뒤 합칩니다.
사용해 보기(Use It)
2026년 기준 권장 구성은 다음과 같습니다.
| 상황(situation) | 전략(strategy) |
|---|---|
| 첫 구현, 미지의 코퍼스 | 재귀, 512 토큰, 겹침 없음 |
| 사실 조회 QA | 재귀, 256~512 토큰 |
| 분석/다중 추론 | 재귀, 512~1024 토큰 + 부모-자식 |
| 상호 참조가 많은 문서(계약서, 논문) | 레이트 청킹 또는 컨텍스트 검색 |
| 대화·다이얼로그 코퍼스 | 발화 단위(turn-level) 청크 + 화자 메타데이터 |
| 짧은 발화(트윗, 리뷰) | 한 문서 = 한 청크 |
재귀 청킹 512에서 시작합니다. 50개 질의로 구성한 평가 셋에서 recall@5를 측정합니다. 거기서부터 튜닝합니다.
산출물 만들기(Ship It)
outputs/skill-chunker.md로 저장합니다.
---
name: chunker
description: Pick a chunking strategy, size, and overlap for a given corpus and query distribution.
version: 1.0.0
phase: 5
lesson: 23
tags: [nlp, rag, chunking]
---
Given a corpus (document types, avg length, domain) and query distribution (factoid / analytical / multi-hop), output:
1. Strategy. Recursive / sentence / semantic / parent-document / late / contextual. Reason.
2. Chunk size. Token count. Reason tied to query type.
3. Overlap. Default 0; justify if >0.
4. Min/max enforcement. `min_tokens`, `max_tokens` guards.
5. Evaluation plan. Recall@5 on 50-query stratified eval set (factoid, analytical, multi-hop).
Refuse any chunking strategy without min/max chunk size enforcement. Refuse overlap above 20% without an ablation showing it helps. Flag semantic chunking recommendations without a min-token floor.
이 스킬은 코퍼스 정보(문서 유형, 평균 길이, 도메인)와 질의 분포(사실 조회·분석·다중 추론 비율)를 입력으로 받아, 전략, 청크 크기, 겹침, 최소·최대 강제 조건, 평가 계획을 제시하도록 만든 것입니다. 최소·최대 청크 크기 강제 조건이 없는 전략은 거부하고, 도움이 된다는 별도 분석(ablation) 없이 20%를 넘는 겹침은 받아들이지 않으며, 최소 토큰 하한이 없는 의미 기반 청킹 제안은 경고로 표시합니다.
연습문제(Exercises)
- 쉬움. 20페이지 분량 문서 하나를
fixed(512, 0),recursive(512, 0),recursive(512, 100)세 가지로 청킹합니다. 청크 개수와 경계 품질을 비교합니다. - 중간. 5개 문서 위에 30개 질의로 구성된 평가 셋을 만듭니다. 재귀, 의미 기반, 부모-자식 세 전략에 대해 recall@5를 측정합니다. 어떤 전략이 이깁니까? 블로그 글의 주장과 일치합니까?
- 어려움. 컨텍스트 검색을 구현합니다. 기준선인 재귀 청킹 대비 MRR 향상을 측정합니다. 인덱싱 비용(LLM 호출 수)과 정확도 향상치를 함께 보고합니다.
핵심 용어(Key Terms)
| 용어 | 흔한 설명 | 실제 의미 |
|---|---|---|
| 청크(Chunk) | 문서의 한 조각 | 임베딩·인덱싱·검색의 단위가 되는 하위 문서 단위입니다. |
| 겹침(Overlap) | 안전 마진 | 인접 청크 사이에 공유되는 N 토큰입니다. 2026년 벤치마크에서는 종종 쓸모가 없습니다. |
| 의미 기반 청킹(Semantic chunking) | 똑똑한 청킹 | 인접 문장 임베딩 유사도가 떨어지는 지점에서 자르는 방식입니다. |
| 부모-자식 청킹(Parent-document) | 2단계 검색 | 작은 자식 청크로 검색하고, 더 큰 부모 청크를 반환합니다. |
| 레이트 청킹(Late chunking) | 임베딩 후 청킹 | 전체 문서를 토큰 단위로 먼저 임베딩한 뒤, 청크 벡터로 풀링합니다. |
| 컨텍스트 검색(Contextual retrieval) | Anthropic의 트릭 | 인덱싱 전에 각 청크 앞에 LLM 생성 요약을 덧붙이는 방식입니다. |
| 컨텍스트 절벽(Context cliff) | 2,500 토큰의 벽 | RAG에서 약 2.5k 컨텍스트 토큰 부근에서 품질이 떨어지는 현상입니다(2026년 1월). |
더 읽을거리(Further Reading)
- Yepes et al. / LangChain — Recursive Character Splitting docs — 실무에서 가장 흔히 쓰이는 기본값입니다.
- Vectara (2024, NAACL 2025). Chunking configurations analysis — 청킹이 임베딩 선택만큼 중요하다는 분석입니다.
- Jina AI — Late Chunking in Long-Context Embedding Models (2024) — 레이트 청킹 논문 소개 글입니다.
- Anthropic — Contextual Retrieval — LLM이 생성한 컨텍스트 접두어로 검색 성능을 35~50% 끌어올린 패턴입니다.
- NVIDIA 2026 chunk-size benchmark — Premai summary — 질의 유형별 청크 크기 벤치마크 요약입니다.