Databricks GenAI Associate 시험 노트개념 정리16 MIN
문서 구조와 모델 제약에 맞춘 청킹
고정 크기·문장·문단·재귀 분할의 차이, chunk size와 overlap을 정하는 법, 임베딩 모델의 최대 토큰 제약, 제목 기반 분할과 부모-자식 청크, 청크 수가 인덱스 한도에 걸릴 때의 대처를 정리합니다.
앞 노트에서 원본을 고르고 글을 뽑아 정제했습니다. 이제 그 글을 검색 단위로 자릅니다. 문서 한 편을 통째로 임베딩하면 벡터 하나가 수십 가지 주제를 뭉개 담아 어느 질문에도 어중간하게 걸리고, 너무 잘게 자르면 청크 하나가 문장 반쪽이라 무엇에 대한 말인지 알 수 없습니다. 시험의 출제 목표는 「문서 구조와 모델 제약에 맞는 청킹 전략을 고른다」이고, 문항은 요구사항과 증상을 주고 전략이나 파라미터를 고르게 합니다.
분할 방식
청크와 분할 단위
청크(chunk)는 문서를 잘라 만든 조각 하나이고, 인덱스에는 청크마다 임베딩 벡터 하나가 들어갑니다. 검색이 돌려주는 것도 문서가 아니라 청크입니다. 그러니 청크의 경계를 어디에 긋느냐가 곧 검색이 찾을 수 있는 답의 모양을 정합니다. 경계를 긋는 기준에 따라 방식이 넷으로 갈립니다.
| 방식 | 경계 | 장점 | 약점 |
|---|---|---|---|
| 고정 크기 | N글자 또는 N토큰마다 | 단순하고 청크 크기가 고르다 | 문장 한가운데를 자른다 |
| 문장 | 문장 끝 | 문장이 온전하다 | 문장 하나는 문맥이 부족하다 |
| 문단 | 빈 줄 | 뜻의 덩어리가 온전하다 | 문단 길이가 제각각이라 크기가 들쭉날쭉하다 |
| 재귀 | 큰 구분자부터 차례로 | 뜻을 지키면서 크기도 맞춘다 | 구분자 목록을 문서에 맞춰야 한다 |
재귀 분할
재귀 분할은 구분자 목록을 큰 것부터 시도해 크기 안에 들어올 때까지 내려가는 방식입니다. 먼저 빈 줄(문단)로 자르고, 그래도 크기를 넘는 조각은 줄바꿈으로, 그다음 공백으로 자릅니다. 문단이 크기 안에 들어오면 문단을 지키고, 안 들어올 때만 더 작은 단위로 내려가므로 고정 크기와 문단 분할의 장점을 함께 가집니다. LangChain의 RecursiveCharacterTextSplitter가 대표이고, 방식을 하나만 고르라는 문항에서 일반 문서의 기본값은 대개 이것입니다.
from langchain_text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=150,
separators=["\n\n", "\n", " ", ""],
)
chunks = splitter.split_text(text)
크기와 겹침
chunk size
chunk size는 청크 하나의 최대 길이입니다. 크기를 키우면 청크마다 문맥이 넉넉해지지만 한 청크에 여러 주제가 섞여 임베딩이 흐려지고, 검색된 청크 k개를 프롬프트에 넣을 때 컨텍스트도 빨리 찹니다. 줄이면 임베딩이 날카로워지는 대신 답에 필요한 문장이 여러 청크로 흩어집니다.
기준은 질문의 모양입니다. 「연차는 며칠인가」처럼 답이 한두 문장이면 작은 청크가, 「이 절차를 처음부터 끝까지 설명해 달라」처럼 답이 한 절이면 큰 청크가 맞습니다. 정답이 하나로 정해진 값이 아니므로 시험은 「평가 세트로 몇 가지 크기를 재 보고 고른다」를 옳은 절차로 봅니다.
overlap과 청크 수
overlap은 이웃한 두 청크가 겹치게 두는 길이입니다. 경계에서 잘린 문장이 한쪽 청크에만 반쪽으로 남는 것을 막아 줍니다. 대가는 청크 수입니다. 청크가 다음 청크로 넘어갈 때 실제로 전진하는 길이는 크기에서 겹침을 뺀 값이므로, 길이 인 문서를 크기 , 겹침 로 자르면 청크 수는 다음과 같습니다.
10,000토큰짜리 문서를 크기 500, 겹침 100으로 자르면 한 번에 400씩 나아가므로 개입니다. 겹침을 0으로 두면 20개이니 겹침 100이 청크를 25% 늘린 셈입니다. 겹침은 보통 크기의 10~20% 안에서 잡고, 크기의 절반을 넘기면 같은 문장이 청크마다 되풀이되어 검색 상위가 거의 같은 청크로 채워집니다.
임베딩 모델의 제약
최대 토큰
임베딩 모델마다 한 번에 받는 입력 길이의 상한이 있습니다. 이 상한을 넘는 청크는 오류가 나는 대신 뒤쪽이 조용히 잘려 앞부분만 벡터에 반영되는 경우가 많습니다. 청크 뒤쪽에 있던 문장은 인덱스에 들어가 있는데도 검색으로는 영영 안 걸립니다.
Databricks가 제공하는 임베딩 엔드포인트 가운데 databricks-bge-large-en은 512토큰까지, databricks-gte-large-en은 8,192토큰까지 받습니다. 같은 청크 전략도 어느 모델을 쓰느냐에 따라 맞기도 하고 넘치기도 합니다. 그래서 청크 크기는 임베딩 모델의 상한보다 작게 잡는 것이 규칙이고, 모델을 바꾸면 청크 크기도 다시 봅니다.
글자와 토큰
상한은 토큰으로 매겨지는데 분할기는 기본으로 글자를 셉니다. RecursiveCharacterTextSplitter(chunk_size=1000)의 1,000은 글자 수이고, 한국어는 영어보다 글자당 토큰이 많아 같은 1,000글자가 모델 상한을 넘기기 쉽습니다. 이 어긋남은 분할기가 모델과 같은 토크나이저로 세게 해서 없앱니다.
from transformers import AutoTokenizer
tok = AutoTokenizer.from_pretrained("BAAI/bge-large-en-v1.5")
splitter = RecursiveCharacterTextSplitter.from_huggingface_tokenizer(
tok, chunk_size=450, chunk_overlap=50
)
크기를 상한 512에 딱 맞추지 않고 450으로 둔 것은 특수 토큰과 제목을 앞에 붙일 여유를 남긴 것입니다.
구조 기반 분할
제목과 섹션
매뉴얼·규정·API 문서처럼 제목 체계가 뚜렷한 문서는 제목을 경계로 삼는 편이 낫습니다. 절 하나가 한 주제이므로 청크가 주제와 맞아떨어지고, 청크마다 어느 절에서 왔는지를 메타데이터로 남길 수 있습니다. 앞 노트의 Unstructured가 붙인 Title 요소나 마크다운의 # 머리글이 그 경계입니다.
from langchain_text_splitters import MarkdownHeaderTextSplitter
md_splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=[("#", "h1"), ("##", "h2"), ("###", "h3")]
)
docs = md_splitter.split_text(markdown_text) # metadata 예: {"h1": "복무 규정", "h2": "휴가"}
절이 너무 길면 제목으로 한 번 나눈 뒤 그 안을 재귀 분할로 다시 자릅니다. 이때 청크 본문 앞에 「복무 규정 > 휴가」 같은 제목 경로를 붙여 두면, 본문에 「휴가」라는 낱말이 없는 청크도 그 질문에 걸립니다.
부모-자식 청크와 윈도우 확장
검색에는 작은 청크가 정확하고 답에는 큰 문맥이 필요하다는 모순을 푸는 방법이 둘 있습니다.
- 부모-자식 청크는 문서를 큰 부모 청크로 나누고 그 안을 다시 작은 자식 청크로 나눈 뒤, 검색은 자식으로 하고 LLM에는 그 자식이 속한 부모를 넘기는 방식입니다. LlamaIndex의
HierarchicalNodeParser와AutoMergingRetriever, LangChain의ParentDocumentRetriever가 이 구조입니다. - 윈도우 확장은 문장 단위로 검색한 뒤 걸린 문장의 앞뒤 몇 문장을 붙여서 넘기는 방식입니다. LlamaIndex의
SentenceWindowNodeParser가 대표입니다.
두 방식 모두 검색 단위와 생성 단위를 따로 둡니다. 「검색 정확도는 좋은데 답이 문맥을 몰라 엉뚱하다」는 증상이 나오면 이 둘이 답입니다.
청크 수와 인덱스 한도
Vector Search 인덱스는 엔드포인트 종류에 따라 담을 수 있는 벡터 수와 비용에 한도가 있습니다. 1만 토큰짜리 문서 2만 편을 앞의 설정(크기 500, 겹침 100)으로 자르면 한 편당 25개, 모두 50만 개입니다. 한도에 걸리면 다음 순서로 줄입니다.
- 정제에서 중복 사본과 목차·색인을 빼 원본 자체를 줄인다
- 청크 크기를 키우고 겹침을 줄인다 — 크기 1,000, 겹침 100이면 한 편당 11개로 줄어 모두 22만 개다
- 질문과 무관한 문서군을 인덱스에서 뺀다
- 그래도 넘치면 문서군별로 인덱스를 나누거나 대용량용 엔드포인트를 쓴다
크기를 키우는 것은 모델 상한 안에서만 할 수 있다는 점을 잊지 않습니다. 512토큰 모델에서 크기를 1,000토큰으로 올리면 청크 수는 줄지만 절반이 잘려 나갑니다.
연습 문제
4,000토큰짜리 문서를 chunk size 1,000토큰, overlap 200토큰으로 고정 크기 분할하면 청크는 몇 개인가?
① 4개
② 5개
③ 6개
④ 8개②. 한 번에 1,000 − 200 = 800씩 나아가므로 입니다. 시작점이 0·800·1,600·2,400·3,200이고 마지막 청크가 4,200까지 덮어 문서 끝에 닿습니다.임베딩 모델의 최대 입력이 512토큰인데 청크가 평균 900토큰입니다. 가장 먼저 나타날 증상은?
① 인덱스 생성이 실패한다
② 청크 뒤쪽 내용이 임베딩에 반영되지 않아 그 부분을 묻는 질문에 검색이 안 걸린다
③ LLM 응답이 느려진다
④ Change Data Feed가 꺼진다②. 상한을 넘는 입력은 잘려서 앞부분만 벡터가 됩니다. 크기를 상한 아래로 줄입니다.RecursiveCharacterTextSplitter(chunk_size=500)로 한국어 문서를 잘랐더니 일부 청크가 512토큰 모델의 상한을 넘었습니다. 원인과 조치로 알맞은 것을 둘 고르시오.
① chunk_size가 글자 수로 세어졌다
② overlap이 0이라서다
③from_huggingface_tokenizer로 모델과 같은 토크나이저를 써서 센다
④ LLM의 max_tokens를 늘린다
⑤ 인덱스를 둘로 나눈다①과 ③. 분할기의 기본 길이 함수는 글자 수를 셉니다. 모델과 같은 토크나이저로 세면 어긋남이 사라집니다.검색은 질문과 딱 맞는 문장을 잘 찾는데, 그 문장 하나만 LLM에 넘어가 답이 앞뒤 문맥을 모릅니다. 알맞은 구성은?
① 청크 크기를 문장 하나로 더 줄인다
② 걸린 문장의 앞뒤 문장을 붙여 넘기는 윈도우 확장을 쓴다
③ overlap을 0으로 둔다
④ 임베딩 모델을 더 작은 것으로 바꾼다②. 검색 단위와 생성 단위를 따로 두는 방식입니다. 부모-자식 청크도 같은 문제를 풉니다.제목 체계가 뚜렷한 사내 매뉴얼을 자를 때 가장 알맞은 방식은?
① 300글자마다 고정 크기로 자른다
② 제목으로 먼저 나누고 긴 절만 재귀 분할한 뒤 제목 경로를 메타데이터로 남긴다
③ 문서 전체를 청크 하나로 둔다
④ 문장마다 자른다②. 절이 곧 주제라 청크와 주제가 맞고, 제목 경로가 본문에 없는 낱말로도 청크를 찾게 해 줍니다.문서 3만 편을 청크로 잘랐더니 벡터 수가 인덱스 한도를 넘었습니다. 먼저 해 볼 조치로 알맞지 않은 것은?
① 중복 사본과 목차 페이지를 뺀다
② 모델 상한 안에서 청크 크기를 키운다
③ overlap을 청크 크기의 60%로 올린다
④ 질문과 무관한 문서군을 뺀다③. 겹침을 늘리면 한 번에 나아가는 길이가 줄어 청크 수가 오히려 늘어납니다.
청킹 문항은 결국 검색 단위를 무엇으로 삼는가를 묻습니다. 문서 구조가 뚜렷하면 구조를, 없으면 재귀 분할을, 모델 상한은 언제나 위에서 누르는 천장으로 봅니다. 다음 노트에서는 이렇게 만든 청크를 Delta 테이블에 적재하고 검색이 잘 되는지 재는 법을 다룹니다.

