본문 바로가기

RAG의 시작 청킹, 그냥 자르면 안되는 이유

개발자MM 2026. 4. 30.

썸네일

 

RAG에서 청킹(Chunking)은 문서를 검색 단위로 자르는 작업이다. 청크 경계가 의미를 끊으면 임베딩 벡터가 어긋나고 LLM 이 받는 문맥도 같이 망가진다. 지금은 고정 길이를 시작으로 Late Chunking 까지가 실무 표준으로 굳어져있다.

 

나는 PDF로 RAG 시스템을 처음 만들 때 청크 크기를 1024 토큰으로 두고 시작했다. 답변이 자꾸 어긋나기에 검색 결과를 확인해보니 표 한가운데가 잘려 있었다. 청크 한 줄이 답을 어디로 끌고 가는지 그제야 눈에 들어왔다. 이후 자료를 펼쳐 보며 청킹 전략을 바꿔 가며 정확도를 올렸던 경험이 있다. 즉, 자료 종류마다 청크를 다루는 방법이 달랐다.

반응형

청크가 어긋나면 AI 답변도 어긋난다

section1

RAG에서 검색이 가져오는 단위가 청크다

RAG 는 질의·검색·생성 3단계로 돈다. 사용자가 던진 질의를 임베딩으로 바꿔 벡터 DB 에서 유사도가 높은 청크를 가져오고, 그 청크를 LLM 이 읽어 대답을 한다.

 

청크가 문서의 단락 한가운데를 자르면 임베딩 벡터의 의미도 깨진다. 정확한 문맥을 LLM 에 넘기지 못하니 답변도 같이 어긋난다. 모델 크기를 키우는 것보다 청킹 단계에서 답변 품질이 더 크게 갈린다는 보고가 자주 나온다.

 

청크 경계를 어떻게 정하느냐가 RAG 의 첫 번째 중요한 결정이다. 임베딩·리랭커·프롬프트 튜닝은 그 다음 단계 작업이다.

 

청크 사이에 오버랩을 두는 이유도 같은 맥락이다. 청크 경계에 걸친 인물·수치·정의가 양쪽 청크에 모두 살아 있으면 검색이 한쪽만 가져와도 문맥이 끊기지 않는다.

 

청킹 전략은 고정 길이, 구분자, 의미, Late 네가지가 있다.

실무에서 자주 쓰는 청킹 방식은 네 가지로 정리된다. 단순한 쪽에서 비싼 쪽 순서로 짚어 본다.

 

첫번째, 고정 길이(Fixed-size) 는 일정 토큰씩 그냥 잘라낸다. 가장 단순하지만 단락·문장 경계를 무시한다.

 

두번째, 구분자 기반(Separator) 은 `\n\n`, `\n`, `. ` 같은 구분자에서 자른다. 단락 단위가 살아 가독성이 보존되고, LangChain 의 RecursiveCharacterTextSplitter 가 사실상 이 방식을 후퇴 우선순위로 운용한다. 이름의 'Recursive' 가 의미 분석을 떠올리게 하지만, 실제로는 구분자 후보를 우선순위대로 시도하면서 청크 크기에 맞을 때까지 재귀적으로 후퇴할 뿐이다. 의미 기반은 LangChain 의 별도 클래스인 `SemanticChunker` 가 담당한다.

세번째, 의미 기반(Semantic) 은 문장을 임베딩한 뒤 코사인 거리가 임계값을 넘는 지점에서 자른다. 문맥이 바뀌는 자리에서 끊어 정확도가 높아지지만 임베딩 비용이 추가로 든다.

 

네번째, Late Chunking 은 문서 전체를 먼저 긴 컨텍스트 임베딩 모델로 인코딩한 뒤 청크 단위로 토큰 임베딩을 평균낸다. (출처) 청크가 짧아도 임베딩에 문서 전체 맥락이 포함되어 있어 검색 정확도가 가장 높게 나온다고 한다.

즉, 구분자 기반으로 시작해 정확도가 부족하면 의미·Late로 시도해보는 것이 무난하다. 학술 논문은 재귀 구분자, 재무 보고서는 페이지·고정 길이, 기술 문서는 의미 + 오버랩이 권장된다.

번외로 Agentic Chunking 도 있다. LLM 에게 직접 분할 지점을 정하게 하는 방식이다. 정확도는 가장 높지만 LLM 호출 비용이 청크 수만큼 누적돼 대규모 문서에 쓰기에는 아직 부담이 크다.

 

청크 128~512 토큰에 오버랩 10~20% 가 권장 구간

청크의 권장 구간이 어느 정도 정해져 있다. 청크 크기 128~512 토큰, 오버랩 10~20% 가 일반적인 출발점이다. 정확한 한 줄 답을 찾는 질의는 작은 청크가 유리하고, 추론이 긴 질의는 큰 청크가 답을 더 잘 잡는다.

LangChain 코드는

from langchain.text_splitter import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=512,
    chunk_overlap=64,                            # 약 12% 오버랩
    separators=["\n\n", "\n", ". ", " ", ""],    # 구분자
)
chunks = splitter.split_text(document)

 

단락 → 줄 → 문장 → 단어 → 글자 순으로 구분자에 맞춰 청크 크기에 맞춘다. 단순한 코드지만 대부분의 문서에서 1차 베이스라인을 만들기에 충분하다.

청크 크기를 정할 때 임베딩 모델이 받을 수 있는 최대 토큰도 같이 본다. text-embedding-3-small 과 BGE-M3 는 8192 토큰까지 받지만, KoSimCSE 같은 한국어 특화 모델은 512 가 한계다. 

 

모델 한계를 넘는 청크는 라이브러리에 따라 조용히 앞부분만 남기고 잘리거나(truncation) 오류를 던진다. 잘릴 때는 한계 너머의 토큰이 임베딩 계산에 들어가지 않으니 그 부분 의미는 사실상 사라진다. 모델부터 정한 뒤 청크 크기를 맞추는 순서가 안전한 이유다.

 

청킹이 어려운 상황 - 실전편

section2

 

표·코드 블록은 자르면 살아나지 않는다

PDF 보고서나 기술 문서에서 가장 자주 깨지는 곳이 표와 코드 블록이다. 행 단위로 의미를 가지는 데이터는 한가운데가 잘리면 검색에 끌려와도 LLM 이 행과 헤더를 다시 이어 붙이지 못한다.

표는 별도 추출 후 마크다운으로 직렬화해 청크에 넣거나, 표 전체를 하나의 청크로 강제한다. 페이지 안의 표 영역을 먼저 잡아 주는 레이아웃 분석 라이브러리를 거치고 본문 청킹과 분리하는 흐름이 무난하다. 코드 블록은 백틱 경계를 구분자에 추가해 잘리지 않도록 막는다.

여러 단으로 나뉜 문서도 비슷한 함정이 있다. 단순 텍스트 추출기는 좌측 단·우측 단을 구분하지 않고 행 순서대로 읽어, 한 문장이 단을 넘나들며 섞인 채 추출되는 경우가 많다. 단(column) 경계를 인식하는 PDF 파서를 쓰거나 단을 먼저 분리한 뒤 청킹해야 임베딩에 깨진 문장이 들어가는 것을 막을 수 있다.

 

구분자를 문서 작성부터 강제한다

자료를 자르는 입장이 아니라 자료를 쓰는 입장에서 청킹을 통제할 수 있다. 모든 섹션을 `## ` H2 로 시작하고, 각 H2 가 한 청크로 떨어지도록 길이를 맞춰 쓸수도 있다.

RecursiveCharacterTextSplitter 의 separator 우선순위를 `["\n## ", "\n### ", "\n\n"]` 로 두면 마크다운 구조 그대로 청크가 떨어진다. 작성 단계에서 섹션 분량을 일정하게 맞춰 두면 청크 크기 튜닝 자체가 줄어든다. 사내 RAG 처럼 자료를 직접 만드는 환경에서 시도해볼법한 정확도 개선 경로다.

 

1페이지짜리 묶음 데이터는 통째로 둔다

카드뉴스, 1페이지 보고서처럼 1페이지가 그 자체로 의미 단위인 자료가 있다. 이런 페이지는 통째로 하나의 청크로 보내는 게 맞다. 1024 토큰을 넘는다고 무리해서 자르면 페이지 안에서 정보, 맥락이 분리된다.

PDF 페이지 단위 청크는 페이지를 한 장씩 떼어 낸 뒤 PageNumber 를 메타데이터로 청크에 붙여 둔다. 검색 결과에 페이지 번호가 따라붙어 LLM 이 답변과 출처를 함께 돌려주기에도 좋다.

 

실제로 데이터를 청킹하면서 느낀점

section3

 

큰 보고서는 페이지 단위로 다시 잡았다

실무에서 다룬 자료는 큰 PDF 보고서가 많았다. 100페이지짜리 분석 보고서, 그 안에 표·차트·요약이 섞여 있는 형태다. 처음에는 1024사이즈 씩 단순 분할로 시작했는데 답변이 일관되지 않고 조금씩 어긋났다.

바꾼 첫 번째 단계가 페이지 단위 청크였다. PDF 를 페이지 한 장씩 잘라낸 뒤 필요한 페이지만 손으로 골라 데이터로 썼다. 한 페이지가 보통 한 가지 주제를 담고 있어서 페이지 경계가 자연스러운 의미 경계 역할을 했다.

 

오버랩 대신 앞뒤 청크를 같이 보냈다

청크와 청크 사이의 문맥을 잇는 방법으로 처음에는 오버랩을 늘려 보았다. 효과가 크지 않아 방향을 바꿨다. 검색이 어떤 청크를 가져오면, 그 청크의 앞 청크·뒤 청크까지 함께 묶어 LLM 에 전달하는 방식을 택했다.

임베딩과 LLM 호출 비용은 좀 더 들지만, 결론과 근거가 같이 잡혀 답변 정확도가 눈에 띄게 좋아졌다. 오버랩이 청크 안쪽에서 문맥을 잇는다면, 이 방식은 청크 바깥에서 문맥을 잇는 셈이다.

 

1페이지짜리 묶음 데이터는 청크를 더 자르지 않는다

또 다른 자료들 중에는 처음부터 1페이지에 모든 내용이 들어간 자료가 있었다. 즉, 페이지에 한 토픽이 정리된 원페이지 문서 같은 것이다.

이런 데이터는 1페이지를 통째로 하나의 청크로 만드는 게 가장 좋았다. 토큰 수가 1500 을 넘어도 임베딩 모델이 받아 주기만 하면 그대로 두는 쪽이 정확도가 높았다.

같은 토픽 안에서 결론과 근거가 한 페이지에 묶여 있는 자료가 많았다. 청크를 더 잘게 자르면 결론만 검색되고 근거가 빠지는 경우가 자주 보였다.

 

마치며

RAG는 사실 그럭저럭 일정 수준의 답변을 만드는 일은 크게 어렵지 않다. 그러나 남들이 못 만드는 1%, 5% 정확도를 더 올리는 일에는 청킹부터 임베딩, 리랭커, 평가셋까지 세세한 부분이 모두 중요하다.  그 중 첫 번째 결정이 청크를 어디서 자를지다.


청크는 길이를 자르는 작업이 아니라 자료의 의미 경계를 어디서 끊을지 정하는 결정이다. 사실 자료마다 그 경계가 달라 정답이 없다. 자료를 한 번 펼쳐 보고 어디서 끊고 싶은지부터 살피는 게 청킹의 시작이다. 그 첫 단계가 잡히면 임베딩과 리랭커 튜닝의 효과도 함께 좋아질 것이다.

반응형

댓글