본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 29. 03:30

RAG 청킹(Chunking): 중첩(Overlap)=0은 경계에서의 사실을 누락시킨다

요약

RAG 시스템에서 고정 크기 청커의 중첩(Overlap)이 0일 경우, 데이터 경계에서 사실 정보가 유실되는 문제를 분석합니다. 적절한 중첩을 통해 회상(Recall) 성능을 크게 개선할 수 있음을 실험 데이터로 증명합니다.

핵심 포인트

  • overlap=0인 고정 크기 청킹은 핵심 정보(앵커와 값)를 분리하여 정보 유실을 초래함
  • 적절한 중첩(Overlap) 추가만으로도 회상(Recall) 성능을 1/5에서 4/5로 대폭 향상 가능
  • 시맨틱 청커 도입 전, 중첩을 통한 회상 성능 측정이 우선되어야 함
  • 중첩은 인덱스 크기를 증가시키는 트레이드오프가 존재함

당신의 RAG 데모는 모든 질문에 답변합니다. 하지만 제품을 출시하면 코퍼스(Corpus) 내의 가장 단순한 사실조차 제대로 답변하지 못합니다. 모델은 괜찮습니다. 리트리버(Retriever)도 괜찮습니다. 문제를 일으킨 것은 청커(Chunker)이며, 해결책은 당신이 곧 설치하려는 시맨틱 스플리터(Semantic Splitter)가 아닙니다.

overlap=0인 고정 크기 청커(Fixed-size chunker)는 윈도우(Window) 경계에서 사실을 반으로 잘라버립니다. 당신이 검색한 청크(Chunk)는 질문의 단어들은 포함하고 있지만, 그에 대한 답이 되는 값(Value)은 포함하지 않습니다. 작은 합성 코퍼스(Synthetic corpus)에서 단순한 청킹(Naive chunking)은 5개의 사실 중 1개만을 회상(Recall)합니다. 24자의 중첩(Overlap)을 추가하면 5개 중 3개에서 4개로 회상 성능이 회복됩니다. 다섯 번째 사례는 중첩이 더 이상 도움이 되지 않는 지점을 보여줍니다. 출력 내용을 포함한 전체 내용은 다음과 같습니다.

corpus: 502 chars, chunk=80
NAIVE  overlap=0   -> 7 chunks, recall@1 = 1/5
   MISS  'refund window'      need '30 days'
...

요약 (TL;DR)

  • 고정 크기 청커에서 overlap=0은 사실을 두 개의 청크로 조용히 분리합니다. 어느 청크도 전체 답변을 보유하지 못합니다.
  • 질문에 대해 가장 높은 점수를 받은 청크가 질문의 단어들만 있고 값(Value)은 전혀 없는 절반일 수 있습니다.
  • 약간의 중첩(여기서는 24자)은 앵커(Anchor)와 값(Value)이 가까운 사실들을 다시 결합합니다. 이 코퍼스에서 회상(Recall)은 1/5에서 4/5로 증가했습니다.
  • 이는 최소한의 방책일 뿐, 완벽한 해결책은 아닙니다. 앵커와 값이 중첩 범위보다 더 멀리 떨어져 있는 사실은 여전히 누락됩니다. 또한 중첩은 인덱스(Index) 크기를 키웁니다 (여기서는 7개에서 9개 청크로 증가).
  • 시맨틱 청커(Semantic chunker)를 찾기 전에 회상(Recall)을 먼저 측정하세요. 저렴한 해결책이 종종 격차의 대부분을 메워줍니다.

이것이 실제로 문제가 되는 지점

저는 리뷰와 스토어프론트(Storefront) 데이터를 스크래핑하며 생계를 유지합니다. 32개의 게시된 액터(Actor)에 대해 2,190번의 프로덕션 실행(Trustpilot 스크래퍼 하나만 해도 962번의 실행이 있음)을 거치며 확인한 결과, 지식의 단위는 거의 항상 문단이 아니었습니다. 그것은 짧은 구절입니다: 환불 기간(Refund window), 주문 ID(Order ID), 추적 번호(Tracking number), 리뷰 날짜(Review date), 가격(Price) 등입니다. 레코드당 두세 개씩 존재하며, 각각은 아주 작은 '앵커+값' 쌍입니다.

그러한 형태는 고정 크기 청커(fixed-size chunker)가 가장 취약하게 다루는 바로 그 형태입니다. 겹침(overlap) 없이 매 N자마다 텍스트를 자르게 되면, 고정된 오프셋(offset)을 기준으로 문서에 눈먼 절단(blind cuts)을 가하는 셈이 됩니다. 긴 문단은 중복성(redundancy) 덕분에 중간에 잘리더라도 살아남습니다. 하지만 네 단어로 이루어진 사실(fact)은 그렇지 않습니다. 만약 절단 지점이 "환불 기간은(the refund window is)"와 "30일(30 days)" 사이에서 발생한다면, 그 사실은 사라져 버리며, 어느 쪽 절반도 이를 재구성할 수 없는 경계 너머로 쪼개지게 됩니다.

출처(provenance)에 대해 정확히 말씀드리자면, 코퍼스(corpus)와 아래 데모에 사용된 다섯 가지 사실은 이 특정 실패 모드(failure mode)를 격리하기 위해 제가 직접 작성한 합성 리터럴(synthetic literals)입니다. 저는 캡처된 고객 문서를 재현하고 있는 것도 아니며, 특정 장애를 보고하고 있는 것도 아닙니다. 실제인 것은 데이터의 '형태(shape)'(짧은 사실구절)와 왜 그러한 형태가 경계 절단(boundary cut)을 그토록 발생하기 쉽게 만드는가 하는 점입니다. 실행에 사용된 수치들은 정확하며 재현 가능합니다. 실제 트래픽에서의 적중률(hit-rate)은 제가 주장할 영역이 아니라 여러분이 직접 측정해야 할 영역입니다.

가장 높은 점수를 받은 청크가 왜 잘못된 절반일 수 있는가

이 부분이 바로 이 문제를 명시적인 버그가 아닌 '침묵하는 버그(silent bug)'로 만드는 지점입니다.

검색(Retrieval)은 쿼리(query)와 얼마나 잘 일치하는지에 따라 청크의 순위를 매깁니다. "특송 배송(express shipping)"이라는 쿼리를 예로 들어보겠습니다. _express_와 _shipping_이라는 단어를 포함하는 청크가 가장 높은 점수를 받고 검색됩니다. 그 청크는 실제적이고 관련성이 있으며 최상위 순위에 있습니다. 다만 80자 기준의 절단 지점이 하필 "Express shipping" 바로 뒤에 떨어졌을 뿐입니다. 값인 "영업일 기준 2일(2 business days)"은 다음 청크의 시작 부분에 놓여 있으며, 이 청크는 쿼리 단어를 포함하지 않기 때문에 더 낮은 점수를 받았습니다.

결과적으로 검색기는 자신의 역할을 완벽하게 수행하여, 질문은 포함하고 있지만 정답은 포함하지 않은 청크를 모델에게 전달합니다. 그러면 모델은 숫자를 환각(hallucinate)하거나 모른다고 답하게 됩니다. 당신은 모델을 탓하겠지만, 모델은 실제로 사실을 포함하지 않은 청크를 전달받은 것입니다.

이것이 바로 함정입니다. 이 실패는 생성(generation) 문제처럼 보이지만, 실제로는 데이터 수집(ingest) 단계에서의 기하학적(geometry) 문제입니다.

데모

임포트(import) 없음. 네트워크 없음, 시계 없음, 무작위성 없음, 파일 I/O 없음. 이것은 장난감 수준의 리트리버(substring 토큰 매칭 방식, 임베딩 아님)이지만, 여기서 보여주는 경계 효과(boundary effect)는 실제이며 리트리버의 지능 여부와는 무관합니다. 파일을 저장하고 python3 -I chunk_overlap_recall.py를 실행하세요. 출력 결과는 매번 바이트 단위로 일치합니다.

"""
chunk_overlap_recall.py -- 왜 overlap=0인 고정 크기 RAG 청커(chunker)가
청크 경계에 걸쳐 있는 사실들을 조용히 누락시키는지, 그리고 약간의
...

출력 결과 읽기

다섯 개의 사실이 있습니다. overlap=0일 때, 그중 네 개는 80자 경계에 걸쳐 있으며, 오직 "restocking fee"만이 하나의 윈도우(window) 안에 온전히 들어옵니다. 재현율(Recall)은 5개 중 1개입니다. 이 단 한 번의 적중이 중요한 이유는, 이것이 정직한 대조군(control)이기 때문입니다. 모든 것이 망가지는 것은 아닙니다. 어떤 사실들은 우연히 윈도우 안에 깔끔하게 떨어지기도 하며, 이것이 바로 운 좋은 경우만 테스트하는 데모에서 이 버그가 매우 잘 숨겨지는 이유입니다.

overlap=24로 전환하면 단계(step)가 80자에서 56자로 줄어듭니다. 이제 연속된 윈도우들은 끝부분의 24자를 공유하므로, 경계에서 분할된 사실이 중첩된 윈도우 중 적어도 하나에는 온전하게 나타납니다. 경계에 걸쳐 있던 네 개의 사실 중 세 개가 돌아옵니다. 재현율이 5개 중 4개로 상승합니다. 새로운 라이브러리도, 임베딩(embeddings)도, 의미론적 분할기(semantic splitter)도 필요 없습니다. 정수 하나면 충분합니다.

이것이 역설적인 부분이며, 저 또한 아주 느린 방식으로 이를 배웠음을 인정합니다. 검색(retrieval)이 이런 식으로 사실을 놓쳤을 때, 저는 의미론적 청커(semantic chunker), 즉 의미 경계에 따라 분할하는 도구를 찾았습니다. 그것은 훌륭한 도구입니다. 하지만 그것은 잘못된 첫 번째 조치였습니다. 저렴한 해결책은 24자의 중첩(overlap)을 추가하는 것과, 무엇을 변경하기 전에 재현율(recall)을 측정하는 규율을 갖추는 것이었습니다. 제 정보 격차의 대부분은 파라미터(parameter) 하나 값으로 메워졌습니다.

중첩(overlap)이 해결책이 아닌 최소한의 기준인 이유

다섯 번째 사실은 여기서 정직해지려는 핵심적인 이유입니다. "customs clearance"가 "9 dollars"를 필요로 하는 부분을 보세요. overlap=0일 때도 놓치고(MISS), overlap=24일 때도 여전히 놓칩니다(MISS). 스크립트는 이를 숨기는 대신 왜 그런지 출력합니다:

해당 문장에서 앵커(anchor, "customs clearance")와 값(value, "9 dollars") 사이에는 24자보다 훨씬 더 많은 필러(filler, 불필요한 수식어)가 떨어져 있습니다. 80자 크기의 윈도우(window)로는 두 요소를 모두 담을 수 없으며, 24자의 중첩(overlap)으로는 그만큼 넓은 간격을 메울 수 없습니다. 중첩(Overlap)은 앵커와 값 사이의 거리가 중첩 크기보다 작을 때의 사실들을 복구합니다. 그 범위를 넘어서면 정말로 다른 무언가가 필요합니다: 더 큰 윈도우, 더 큰 중첩, 사실의 앵커와 값을 동일한 청크(chunk)에 유지하는 구조적 분할(structural split), 또는 더 똑똑한 청커(chunker)가 필요합니다. 중첩(Overlap)은 하한선(floor)을 높여줄 뿐, 상한선(ceiling)을 제거하지는 못합니다.

따라서 이 포스트는 "중첩(overlap)이 RAG를 해결한다"는 내용이 아닙니다. 더 좁고 방어 가능한 관점, 즉 overlap=0은 경계에 있는 사실들을 놓치며, 작은 중첩(overlap)은 조각들이 서로 가까운 사실들만을 되돌려준다는 것입니다. 먼저 측정하고, 남은 누락(miss)들이 더 무거운 도구를 사용할 만큼 정당한지 결정하십시오.

중첩(Overlap)의 비용

중첩(Overlap)은 공짜가 아니며, 실행 결과가 이를 증명합니다. 동일한 502자를 커버하기 위해 인덱스(index)가 7개 청크에서 9개로 늘어났습니다. 이는 텍스트가 중복되었음을 의미하며, 저장해야 할 벡터(vector)가 많아지고, 점수를 매겨야 할 후보(candidate)가 많아지며, 검색(retrieval) 시 모델의 컨텍스트(context)에 더 많은 토큰(token)이 밀어 넣어짐을 의미합니다. 중첩(overlap)을 높일수록 그 비용도 함께 증가합니다. 재현율(recall)과 인덱스 비대화(index bloat) 사이에는 실제적인 트레이드오프(trade-off)가 존재하며, "그냥 중첩(overlap)을 50%로 확 올려버려"라고 하는 것은 전략이 아니라 청구서(bill)를 받는 것과 같습니다. 당신이 중요하게 생각하는 사실들을 복구할 수 있는 가장 작은 중첩(overlap) 값을 선택하고, 신뢰할 수 있는 재현율(recall) 수치로 이를 확인하십시오.

이것은 그라운딩(grounding) 문제가 아닙니다

만약 제가 이전에 작성한 소스에 없는 사실을 자신 있게 답변하는 RAG 시스템에 관한 포스트를 읽으셨다면, 이번 내용은 그와 유사해 보이지만 파이프라인의 반대편에 위치합니다. 이전 포스트는 _생성 후 그라운딩 (post-generation grounding)_에 관한 것이었습니다. 즉, 모델이 이미 답변을 생성했고, 그 답변이 실제로 검색된 텍스트에 의해 뒷받침되는지 확인하는 과정입니다. 반면 이것은 _생성 전 재현율 (pre-generation recall)_에 관한 것입니다. 청킹 (chunking)이 무언가가 순위가 매겨지기 전에 사실을 분할해 버렸기 때문에, 해당 사실이 애초에 검색기 (retriever)에 도달하지 못한 상황입니다. 전자는 사후에 잘못된 답변을 잡아내는 것이라면, 후자는 올바른 사실이 답변에 사용될 수 없도록 원천 차단하는 것입니다. 당신은 두 가지 확인 절차를 모두 필요로 하며, 이들은 서로 다른 단계에서 이루어집니다.

연구 결과가 말하는 것

청킹이 실패의 원인으로서 과소평가되고 있다는 주장을 제가 처음 하는 것은 아닙니다. 2026년 5월에 발표된 논문, "Retrieval-Augmented Generation에서의 청킹 방법론 - 계산 비용 및 한계에 대한 효과성 평가" (arXiv 2606.00881)는 초록에서 이를 명확히 밝히고 있습니다: "청킹은 흔히 단순한 전처리 단계로 취급되지만, 우리는 이것이 영향력이 크고 종종 간과되는 다양한 문제들을 야기한다는 것을 보여준다." 동일한 저자들은 제안된 청킹 방법론들의 목록이 늘어나고 있지만, 이들이 "다양한 시나리오에 걸친 효과성에 대한 증거가 제한적인 상태에서 ... 성능 향상을 주장하는 경우가 많다"고 언급합니다. 저는 해당 논문의 수치들을 한 줄씩 직접 검증하지 않았으므로, 벤치마크가 아닌 프레임워크(framing)로서만 참고하시기 바랍니다: 청킹은 해롭지 않은 전처리 단계가 아니며, 더 화려한 방법론이 자동으로 더 나은 방법론인 것도 아닙니다. 이는 저렴한 방식의 데모가 보여주는 결과와 일치합니다.

제가 실제로 배포한다면

저에게 가장 많은 시간을 아껴주었던 작업 순서는 다음과 같습니다:

  1. 청커(Chunker)를 건드리기 전에 재현율(Recall) 테스트를 구축하세요. 코퍼스(Corpus) 내에 존재한다는 것을 알고 있는 몇 가지 질문-답변 구간(Question-and-answer-span) 쌍을 준비합니다. 상위 k개(Top-k)를 검색하여 답변 구간이 포함되어 있는지 확인합니다. 이것이 측정의 전부이며, 대부분의 RAG 설정에서 결여되어 있는 부분입니다.
  2. 중첩(Overlap)을 추가하고 다시 측정하세요. 작게 시작하십시오. 이 코퍼스의 경우 80자 윈도우(Window)에서 24자의 중첩이 효과적이었습니다. 적절한 값은 앵커(Anchor)가 값으로부터 얼마나 떨어져 있는지에 따라 달라지며, 이는 재현율(Recall) 테스트를 통해 알 수 있습니다.
  3. 인덱스(Index) 크기를 주시하세요. 중첩은 텍스트를 중복시킵니다. 청크(Chunk) 수가 급격히 늘어난다면, 저장 공간과 쿼리당 토큰(Token) 비용을 지불하게 됩니다. 의도적으로 절충(Trade-off)하십시오.
  4. 그다음에야 시맨틱(Semantic) 또는 구조적 분할기(Structural splitter)를 고려하세요. 사실(Fact)이 중첩으로 해결할 수 없는 경우(세관 사례와 같이)를 위해서입니다. 직관이 아닌, 측정된 격차를 바탕으로 더 무거운 도구를 도입하십시오.

이 순서대로 진행해야 하는 이유는 지루할 정도로 명확합니다. 저렴한 해결책이 격차의 대부분을 메워주며, 측정하기 전까지는 얼마나 남았는지 알 수 없기 때문입니다. 재현율(Recall) 테스트 전에 구매한 시맨틱 청커(Semantic chunker)는 더 큰 비용이 드는 추측에 불과합니다.

데모의 검색기(Retriever)에 대한 솔직한 주의사항을 하나 말씀드리자면, 저는 서브스트링(Substring) 토큰 매칭을 사용했기 때문에 스크립트에 의존성이 전혀 필요 없으며 바이트 단위로 재현 가능(Byte-reproducible)합니다. 실제 임베딩(Embedding) 검색은 점수가 다르게 산출되며, 정확한 재현율(Recall) 수치는 변동될 것입니다. 하지만 경계 효과(Boundary effect) 자체는 변하지 않습니다. 두 개의 청크에 걸쳐 나뉜 사실은 어떻게 순위를 매기든 상관없이 분리됩니다. 단일 청크가 그 사실을 보유하지 못하기 때문입니다. 이것이 이 테스트(Toy)를 통해 증명되는 부분입니다.

AI의 도움과 인간의 검토를 거쳐 작성되었습니다. 코퍼스, 5가지 사실, 그리고 세관 한도 사례는 경계 효과를 격리하기 위해 선택된 합성 리터럴(Synthetic literals)이며, 실제 운영 문서나 보고된 사건이 아닙니다. 코드는 실제이며, 표준 라이브러리만으로 실행되고, 위에 표시된 출력을 결정론적(Deterministically)으로 생성합니다. 운영 컨텍스트(32개 액터에 걸친 2,190회 실행, Trustpilot 스크레이퍼에서 962회 실행)는 실제이며, 왜 짧은 사실 문구(Fact-phrases)가 일반적인 형태인지를 설명해 주지만, 이것이 측정된 적중률(Hit-rate)은 아닙니다.

실제 운영 환경(production runs)에서 도출된 더 많은 수치들을 위해 계속 팔로우해 주세요. 만약 짧은 사실 문구(fact-phrases)에 대해 RAG를 실행한다면, 귀하의 재현율(recall) 테스트는 어떤 중첩(overlap) 값으로 수렴했나요? 그리고 중첩(overlap)이 구제하지 못한 사실(facts)은 무엇이었나요?

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0