RAG Week3 문서 청킹
이번 주는 긴 문서를 RAG가 다루기 좋은 작은 조각으로 나누는 단계입니다. 검색 품질은 임베딩 모델만으로 결정되지 않고, 문서를 어떻게 쪼개는지도 크게 좌우된다는 점을 확인하는 주차입니다.
이번 주 목표
- 청킹이 왜 필요한지 실습으로 이해한다.
- 청크 크기와 오버랩에 따라 결과가 어떻게 달라지는지 본다.
- 청크에 메타데이터를 붙이는 이유를 이해한다.
핵심 개념
- 청킹: 긴 문서를 검색과 임베딩에 적합한 작은 조각으로 나누는 과정
- 청크 크기: 한 조각에 담는 텍스트 길이
- 오버랩: 앞뒤 청크가 일부 겹치도록 두는 설정
- 메타데이터: 파일명, 페이지 번호, 날짜, 카테고리 같은 부가 정보
실습 순서
1. 패키지 설치
.\.venv\Scripts\activate
py -m pip install -U langchain langchain-community langchain-text-splitters pypdf
2. 기본 샘플로 청킹 실습
python chunking_experiment.py
sample_investment_note.txt
삼성전자는 2024년 들어 반도체 업황 회복의 영향을 받았다.
특히 HBM과 고대역폭 메모리 관련 수요가 늘어나면서 메모리 부문의 기대감이 커졌다.
일부 증권사는 서버용 메모리 판매 비중 확대를 긍정적으로 평가했다.
다만 스마트폰 출하량은 시장 경쟁 심화와 교체 수요 둔화의 영향을 받았다.
모바일 부문은 프리미엄 제품 중심 전략으로 수익성을 방어하려는 흐름을 보였다.
환율 변동과 마케팅 비용 증가는 단기 실적 변동성 요인으로 언급되었다.
투자 보고서를 RAG에 넣으려면 문서를 적절한 길이의 청크로 나눠야 한다.
청크가 너무 크면 한 벡터에 여러 주제가 섞여 검색 정확도가 떨어질 수 있다.
청크가 너무 작으면 문맥이 끊겨 답변에 필요한 정보가 사라질 수 있다.
따라서 청크 크기와 오버랩을 바꿔 보면서 검색 품질이 어떻게 달라지는지 관찰하는 과정이 중요하다.
각 청크에는 파일명, 페이지 번호, 생성 날짜, 카테고리 같은 메타데이터를 함께 저장하는 것이 좋다.
이 메타데이터는 나중에 출처를 표시하거나 특정 보고서만 필터링할 때 사용된다.
상세 코드
from __future__ import annotations
# 앞으로 작성할 타입 힌트를 조금 더 편하게 쓰기 위한 설정입니다.
# 지금 코드에서는 큰 차이를 느끼기 어렵지만,
# 나중에 복잡한 타입 힌트를 쓸 때 도움이 됩니다.
import argparse
# 터미널에서 프로그램 실행 시 옵션(--file, --category 같은 것)을 받을 수 있게 해주는 모듈입니다.
from datetime import datetime
# 현재 날짜와 시간을 다루기 위한 모듈입니다.
# 여기서는 청크 메타데이터에 "추가된 날짜"를 넣을 때 사용합니다.
from pathlib import Path
# 파일 경로를 다루기 쉽게 도와주는 모듈입니다.
# 문자열로 경로를 다루는 것보다 더 안전하고 편리합니다.
from langchain_text_splitters import RecursiveCharacterTextSplitter
# 긴 문서를 작은 청크로 나누는 도구입니다.
# RecursiveCharacterTextSplitter는 문단, 줄바꿈, 문장 경계 등을 최대한 고려해서
# 자연스럽게 잘라주려고 시도하는 대표적인 청킹 도구입니다.
from langchain_community.document_loaders import PyPDFLoader, TextLoader
# 문서를 읽어오는 로더(loader)입니다.
# PyPDFLoader는 PDF 파일을 읽을 때 사용하고,
# TextLoader는 일반 텍스트(.txt) 파일을 읽을 때 사용합니다.
DEFAULT_FILE = Path(__file__).with_name("sample_investment_note.txt")
# 이 파이썬 파일과 같은 폴더에 있는 "sample_investment_note.txt" 파일을
# 기본 입력 파일로 사용하겠다는 뜻입니다.
#
# __file__ : 현재 실행 중인 파이썬 파일의 경로
# with_name("sample_investment_note.txt")
# : 현재 파일과 같은 위치에 있는 다른 파일 이름으로 바꿔 줌
#
# 즉, 사용자가 --file 옵션을 따로 주지 않으면
# sample_investment_note.txt를 자동으로 읽게 됩니다.
def load_documents(file_path: Path):
# 전달받은 파일 경로의 확장자를 소문자로 가져옵니다.
# 예: ".PDF"여도 ".pdf"로 통일해서 비교하려고 lower()를 사용합니다.
suffix = file_path.suffix.lower()
# 파일 확장자가 PDF라면 PDF 전용 로더를 사용합니다.
if suffix == ".pdf":
loader = PyPDFLoader(str(file_path))
# PyPDFLoader는 문자열 경로를 받기 때문에 Path 객체를 str로 바꿔 줍니다.
# PDF가 아니면 일반 텍스트 파일이라고 보고 TextLoader를 사용합니다.
else:
loader = TextLoader(str(file_path), encoding="utf-8")
# encoding="utf-8"은 한글이 들어간 텍스트 파일을 제대로 읽기 위해 지정한 것입니다.
# loader.load()를 실행하면 문서 내용을 LangChain의 Document 형태로 읽어옵니다.
# PDF의 경우 보통 페이지 단위로 여러 Document가 생길 수 있고,
# TXT의 경우 보통 하나의 Document로 읽히는 경우가 많습니다.
return loader.load()
def enrich_metadata(chunks, category: str) -> None:
# 오늘 날짜를 "2026-04-08" 같은 문자열 형태로 만듭니다.
added_date = datetime.now().strftime("%Y-%m-%d")
# enumerate(chunks)는 청크를 하나씩 꺼내면서
# 동시에 0, 1, 2, 3 ... 같은 번호(index)도 함께 줍니다.
for index, chunk in enumerate(chunks):
# 각 청크의 metadata 딕셔너리에 추가 정보를 넣습니다.
chunk.metadata.update(
{
"chunk_id": index,
# 각 청크에 번호를 붙입니다.
# 나중에 "몇 번째 청크인지" 구분할 때 사용합니다.
"source_type": Path(chunk.metadata.get("source", "")).suffix.lower() or ".txt",
# 원본 문서의 source 경로에서 확장자를 뽑아옵니다.
# 예: report.pdf -> ".pdf"
#
# chunk.metadata.get("source", "")
# : metadata 안에 source 값이 있으면 가져오고,
# 없으면 빈 문자열("")을 사용합니다.
#
# Path(...).suffix.lower()
# : source에서 확장자만 뽑아서 소문자로 바꿉니다.
#
# or ".txt"
# : 만약 확장자를 못 찾으면 기본값으로 ".txt"를 넣습니다.
"added_date": added_date,
# 이 청크를 메타데이터에 기록한 날짜를 저장합니다.
"category": category,
# 사용자가 지정한 카테고리(기본값: 투자보고서)를 저장합니다.
# 나중에 검색 시 특정 카테고리만 필터링할 때 쓸 수 있습니다.
}
)
def print_chunk_preview(title: str, chunks) -> None:
# 화면에 제목을 출력합니다.
# 예: "전략 1: 작은 청크"
print(title)
# 전체 청크 개수를 출력합니다.
print(f"청크 수: {len(chunks)}")
# 청크가 하나도 없다면 더 이상 출력할 내용이 없으므로 함수 종료
if not chunks:
return
# 첫 번째 청크를 꺼냅니다.
# 샘플 확인용으로 첫 청크만 보여줍니다.
first_chunk = chunks[0]
# 보기 좋게 구분선 출력
print("-" * 60)
# 첫 번째 청크의 실제 텍스트 내용 출력
print("첫 번째 청크 내용:")
print(first_chunk.page_content)
# 다시 구분선 출력
print("-" * 60)
# 첫 번째 청크에 붙은 메타데이터 출력
print("첫 번째 청크 메타데이터:")
print(first_chunk.metadata)
# 큰 구분선 출력
print("=" * 60)
def main() -> None:
# argparse.ArgumentParser는 터미널 인자를 받을 준비를 해주는 객체입니다.
parser = argparse.ArgumentParser(description="Week3 청킹 실험")
# --file 옵션 정의 (args.file)
parser.add_argument(
"--file",
default=str(DEFAULT_FILE),
# 사용자가 --file을 입력하지 않으면 DEFAULT_FILE을 사용합니다.
help="실험할 TXT 또는 PDF 파일 경로",
# help는 사용자가 --help를 입력했을 때 보이는 설명입니다.
)
# --category 옵션 정의 (args.category)
parser.add_argument(
"--category",
default="투자보고서",
# 사용자가 --category를 입력하지 않으면 기본값은 "투자보고서"입니다.
help="청크 메타데이터에 넣을 카테고리",
)
# 실제로 터미널에서 입력한 값들을 읽어옵니다.
args = parser.parse_args()
# 입력받은 파일 경로를 Path 객체로 변환합니다.
file_path = Path(args.file)
# 해당 경로에 파일이 실제로 존재하는지 확인합니다.
if not file_path.exists():
# 파일이 없으면 프로그램을 종료하면서 에러 메시지를 보여줍니다.
raise SystemExit(f"파일을 찾을 수 없습니다: {file_path}")
# 파일을 읽어서 Document 목록으로 가져옵니다.
documents = load_documents(file_path)
# 어떤 파일을 대상으로 실험하는지 출력
print(f"대상 파일: {file_path.name}")
# 로드된 문서 수 출력
# PDF는 페이지별로 여러 개가 될 수 있고,
# TXT는 보통 1개일 가능성이 큽니다.
print(f"로드된 문서 수: {len(documents)}")
# 첫 번째 문서의 글자 수 출력
# 문서가 얼마나 긴지 대략 확인할 수 있습니다.
print(f"첫 문서 글자 수: {len(documents[0].page_content)}")
# 원본 문서에 원래 들어 있던 메타데이터 출력
# 예: source 경로, page 번호 등
print(f"원본 메타데이터: {documents[0].metadata}")
# 보기 좋게 구분선 출력
print("=" * 60)
# 작은 청크 전략 생성
splitter_small = RecursiveCharacterTextSplitter(
chunk_size=120,
# 청크 하나의 최대 길이를 120자로 설정
# 여기서는 초보자 실습용으로 아주 작게 잡아 차이를 쉽게 보이게 했습니다.
chunk_overlap=20,
# 앞 청크와 뒤 청크가 20자 정도 겹치게 만듭니다.
# 문장이 청크 경계에서 끊길 때 문맥 손실을 줄이는 역할을 합니다.
length_function=len,
# 길이를 셀 때 Python의 len()을 사용하겠다는 뜻입니다.
# 즉, 여기서는 "토큰 수"가 아니라 "문자 수" 기준에 더 가깝게 동작합니다.
)
# 큰 청크 전략 생성
splitter_large = RecursiveCharacterTextSplitter(
chunk_size=220,
# 청크 하나의 최대 길이를 220자로 설정
# 작은 청크보다 더 큰 덩어리로 자르게 됩니다.
chunk_overlap=40,
# 더 큰 청크이므로 겹치는 부분도 40자로 조금 늘렸습니다.
length_function=len,
# 여기서도 문자 수 기준으로 길이를 셉니다.
)
# 작은 청크 전략으로 문서를 분할합니다.
chunks_small = splitter_small.split_documents(documents)
# 큰 청크 전략으로 문서를 분할합니다.
chunks_large = splitter_large.split_documents(documents)
# 작은 청크들에 메타데이터를 추가합니다.
enrich_metadata(chunks_small, args.category)
# 큰 청크들에도 메타데이터를 추가합니다.
enrich_metadata(chunks_large, args.category)
# 작은 청크 전략 결과를 화면에 미리 보여줍니다.
print_chunk_preview("전략 1: 작은 청크", chunks_small)
# 큰 청크 전략 결과를 화면에 미리 보여줍니다.
print_chunk_preview("전략 2: 큰 청크", chunks_large)
# 마지막으로 사용자가 관찰해야 할 핵심 포인트를 출력합니다.
print("관찰 포인트:")
print("1. 청크 크기가 작아지면 개수는 늘고, 내용은 더 잘게 분리됩니다.")
print("2. 오버랩이 있으면 앞뒤 문맥이 일부 겹쳐 정보 손실을 줄일 수 있습니다.")
print("3. 메타데이터를 붙여 두면 나중에 출처와 카테고리를 함께 추적할 수 있습니다.")
if __name__ == "__main__":
# 이 파일을 직접 실행했을 때만 main() 함수를 실행합니다.
# 다른 파일에서 import해서 사용할 때는 main()이 자동 실행되지 않게 해줍니다.
main()
확인할 것:
- 작은 청크와 큰 청크의 개수 차이
- 첫 번째 청크 내용이 어떻게 달라지는지
- 메타데이터에 어떤 값이 붙는지
py chunking_experiment.py --help
usage: week3_chunking_experiment.py [-h] [--file FILE] [--category CATEGORY]
Week3 청킹 실험
options:
-h, --help show this help message and exit
--file FILE 실험할 TXT 또는 PDF 파일 경로
--category CATEGORY 청크 메타데이터에 넣을 카테고리
py chunking_experiment.py
대상 파일: sample_investment_note.txt
로드된 문서 수: 1
첫 문서 글자 수: 532
원본 메타데이터: {'source': 'sample_investment_note.txt'}
============================================================
전략 1: 작은 청크
청크 수: 7
------------------------------------------------------------
첫 번째 청크 내용:
삼성전자는 2024년 들어 반도체 업황 회복의 영향을 받았다.
특히 HBM과 고대역폭 메모리 관련 수요가 늘어나면서 메모리 부문의 기대감이 커졌다.
일부 증권사는 서버용 메모리 판매 비중 확대를 긍정적으로 평가했다.
------------------------------------------------------------
첫 번째 청크 메타데이터:
{'source': 'sample_investment_note.txt', 'chunk_id': 0, 'source_type': '.txt', 'added_date': '2026-04-08', 'category': '투자보고서'}
============================================================
전략 2: 큰 청크
청크 수: 4
------------------------------------------------------------
첫 번째 청크 내용:
삼성전자는 2024년 들어 반도체 업황 회복의 영향을 받았다.
특히 HBM과 고대역폭 메모리 관련 수요가 늘어나면서 메모리 부문의 기대감이 커졌다.
일부 증권사는 서버용 메모리 판매 비중 확대를 긍정적으로 평가했다.
------------------------------------------------------------
첫 번째 청크 메타데이터:
{'source': 'sample_investment_note.txt', 'chunk_id': 0, 'source_type': '.txt', 'added_date': '2026-04-08', 'category': '투자보고서'}
============================================================
관찰 포인트:
1. 청크 크기가 작아지면 개수는 늘고, 내용은 더 잘게 분리됩니다.
2. 오버랩이 있으면 앞뒤 문맥이 일부 겹쳐 정보 손실을 줄일 수 있습니다.
3. 메타데이터를 붙여 두면 나중에 출처와 카테고리를 함께 추적할 수 있습니다.
결과 차이가 없는 것 같아 수정
- chunk 여러 개 출력 (
print_chunk_preview)
python week3_chunking_experiment.py
대상 파일: sample_investment_note.txt
로드된 문서 수: 1
첫 문서 글자 수: 532
원본 메타데이터: {'source': 'sample_investment_note.txt'}
============================================================
전략 1: 작은 청크
청크 수: 7
------------------------------------------------------------
0번 청크 글자 수: 120
삼성전자는 2024년 들어 반도체 업황 회복의 영향을 받았다.
특히 HBM과 고대역폭 메모리 관련 수요가 늘어나면서 메모리 부문의 기대감이 커졌다.
일부 증권사는 서버용 메모리 판매 비중 확대를 긍정적으로 평가했다.
------------------------------------------------------------
1번 청크 글자 수: 85
다만 스마트폰 출하량은 시장 경쟁 심화와 교체 수요 둔화의 영향을 받았다.
모바일 부문은 프리미엄 제품 중심 전략으로 수익성을 방어하려는 흐름을 보였다.
------------------------------------------------------------
2번 청크 글자 수: 39
환율 변동과 마케팅 비용 증가는 단기 실적 변동성 요인으로 언급되었다.
============================================================
전략 2: 큰 청크
청크 수: 4
------------------------------------------------------------
0번 청크 글자 수: 120
삼성전자는 2024년 들어 반도체 업황 회복의 영향을 받았다.
특히 HBM과 고대역폭 메모리 관련 수요가 늘어나면서 메모리 부문의 기대감이 커졌다.
일부 증권사는 서버용 메모리 판매 비중 확대를 긍정적으로 평가했다.
------------------------------------------------------------
1번 청크 글자 수: 125
다만 스마트폰 출하량은 시장 경쟁 심화와 교체 수요 둔화의 영향을 받았다.
모바일 부문은 프리미엄 제품 중심 전략으로 수익성을 방어하려는 흐름을 보였다.
환율 변동과 마케팅 비용 증가는 단기 실적 변동성 요인으로 언급되었다.
------------------------------------------------------------
2번 청크 글자 수: 125
투자 보고서를 RAG에 넣으려면 문서를 적절한 길이의 청크로 나눠야 한다.
청크가 너무 크면 한 벡터에 여러 주제가 섞여 검색 정확도가 떨어질 수 있다.
청크가 너무 작으면 문맥이 끊겨 답변에 필요한 정보가 사라질 수 있다.
============================================================
관찰 포인트:
1. 청크 크기가 작아지면 개수는 늘고, 내용은 더 잘게 분리됩니다.
2. 오버랩이 있으면 앞뒤 문맥이 일부 겹쳐 정보 손실을 줄일 수 있습니다.
3. 메타데이터를 붙여 두면 나중에 출처와 카테고리를 함께 추적할 수 있습니다.
GPT 추가설명
좋아요. 이제는 왜 저렇게 나왔는지가 꽤 잘 보입니다.
결론부터 말하면, 코드가 정상적으로 잘 동작한 결과예요.
왜 0번 청크는 같고, 뒤에서 달라졌나요?
당신의 텍스트는 사실상 문단 단위로 아주 깔끔하게 나뉘어 있어요.
구조를 보면 대충 이렇게 됩니다.
- 반도체/메모리 관련 문단
- 스마트폰/모바일 관련 문단
- RAG 청킹 설명 문단
- 청킹 실험 중요성/메타데이터 문단
RecursiveCharacterTextSplitter는 이름은 character splitter지만, 실제로는 무조건 글자 수에서 딱 자르는 게 아니라 먼저
- 문단 구분
- 줄바꿈
- 문장 경계
같은 자연스러운 경계를 최대한 살리려고 합니다.
그래서 첫 문단이 120자 정도인데,
- 작은 청크
chunk_size=120 - 큰 청크
chunk_size=220
둘 다 첫 문단 하나를 그대로 담을 수 있으니까 0번 청크가 똑같이 나온 거예요.
즉, “왜 안 달라지지?”가 아니라
“첫 문단이 딱 한 청크로 들어가기 좋게 생겨 있어서 둘 다 같은 결과가 나온 것” 입니다.
작은 청크 결과를 보면
전략 1: 작은 청크
- 0번: 반도체 문단
- 1번: 스마트폰 문단의 앞부분
- 2번: 스마트폰 문단의 마지막 문장
- 나머지: 뒤 문단들도 더 잘게 쪼개졌을 가능성 큼
작은 청크에서는 한 문단 전체가 안 들어가면 문단 안에서도 다시 나뉘는 거예요.
예를 들어 스마트폰 관련 부분은:
- 첫 2문장
- 마지막 1문장
이렇게 갈라졌죠.
이건 작은 청크에서는 더 세밀하게 검색될 수 있는 장점이 있지만, 반대로 문맥이 끊길 가능성도 있다는 뜻입니다.
예를 들어 사용자가 “스마트폰 사업의 위험 요인이 뭐야?” 라고 물었을 때,
- “시장 경쟁 심화”
- “교체 수요 둔화”
- “환율 변동”
- “마케팅 비용 증가”
가 원래는 한 흐름인데, 작은 청크에서는 둘로 갈라져 있어서 검색이 한쪽만 잡힐 수도 있어요.
큰 청크 결과를 보면
전략 2: 큰 청크
- 0번: 반도체 문단 전체
- 1번: 스마트폰 문단 전체
- 2번: RAG 청킹 설명 문단 전체
- 3번: 마지막 문단 전체일 가능성 높음
큰 청크는 문단 하나를 거의 통째로 담고 있어서 문맥 보존은 더 잘 됩니다.
예를 들어 스마트폰 문단의 3문장이 한 청크에 다 있으니, 검색되면 관련 정보가 한 번에 같이 따라옵니다.
대신 단점은, 나중에 문서가 훨씬 길어지면 한 청크 안에 주제가 너무 많이 섞일 수 있다는 점이에요.
지금 샘플 문서는 문단 구분이 아주 좋아서 큰 청크가 꽤 예쁘게 보이지만, 실제 투자 보고서는 표, 각주, 리스크, 실적 전망, 밸류에이션이 섞여 있어서 큰 청크가 항상 좋은 건 아닙니다.
지금 실험에서 관찰할 수 있는 핵심
당신 결과는 오히려 교재용 예시로 좋습니다.
1. 청크 크기가 작아지면 개수가 늘어남
- 작은 청크: 7개
- 큰 청크: 4개
이건 아주 명확하죠.
2. 작은 청크는 문단 내부도 나눔
작은 청크에서는 스마트폰 문단이 둘로 갈렸어요. 즉, 더 세밀한 검색은 가능하지만 문맥 손실 위험도 생깁니다.
3. 큰 청크는 문단 단위 보존이 더 잘 됨
큰 청크에서는 스마트폰 문단 전체가 한 덩어리로 유지됐어요. 즉, 문맥 보존은 유리합니다.
왜 오버랩 효과는 잘 안 보이나요?
지금 텍스트는 문단과 문장 경계가 너무 깔끔해서, splitter가 굳이 어색하게 잘라 넣지 않아도 됩니다.
그래서 chunk_overlap=20, 40을 줬더라도
출력만 봐서는 오버랩이 눈에 띄지 않을 수 있어요.
오버랩은 보통 이런 경우에 더 체감됩니다.
- 한 문장이 길어서 청크 경계를 넘길 때
- 문단이 매우 길어서 중간에서 잘릴 때
- 자연스러운 구분자가 부족할 때
지금 샘플은 너무 예쁘게 정리된 텍스트라서 오버랩 효과가 크게 드러나지 않은 거예요.
이 결과를 바탕으로 보고서처럼 쓰면
이렇게 정리할 수 있어요.
작은 청크 설정에서는 문서가 더 많은 청크로 분할되었고, 일부 문단은 둘 이상의 청크로 나뉘었다. 반면 큰 청크 설정에서는 각 문단이 하나의 청크로 유지되는 경우가 많아 문맥 보존이 더 잘 이루어졌다. 따라서 작은 청크는 세밀한 검색에 유리할 수 있지만 정보가 분산될 가능성이 있고, 큰 청크는 관련 문장을 함께 유지하는 데 유리하지만 하나의 청크에 더 많은 내용이 포함될 수 있다.
한 가지 더
지금 샘플은 문단 단위 비교에는 좋지만, 오버랩 효과 확인용으로는 조금 약합니다.
오버랩까지 잘 보고 싶으면 나중에 이런 문서를 써보면 좋아요.
- 문단 구분이 적은 긴 텍스트
- 한 문장이 매우 긴 보고서 문장
- 표 설명이나 주석이 길게 이어지는 문서
그럼 “앞 청크 끝 문장이 뒤 청크에도 일부 남아 있는 모습”이 더 잘 보입니다.
3. 직접 준비한 파일로 실습
py chunking_experiment.py --file sample_investment_note.txt
py chunking_experiment.py --file 2024ltr.pdf
python chunking_experiment.py --file sample_investment_note.txt는 위의 결과와 동일해 생략.2024ltr.pdf: 워렌버핏 주주서한(2024): 링크
py week3_chunking_experiment.py --file 2024ltr.pdf
대상 파일: 2024ltr.pdf
로드된 문서 수: 15
첫 문서 글자 수: 2189
원본 메타데이터: {'producer': 'Acrobat Distiller 8.1.0 (Windows)', 'creator': 'PyPDF', 'creationdate': '2025-02-22T07:14:18-06:00', 'moddate': '2025-02-22T07:14:40-06:00', 'title': 'printmgr file', 'source': '2024ltr.pdf', 'total_pages': 15, 'page': 0, 'page_label': '1'}
============================================================
전략 1: 작은 청크
청크 수: 439
------------------------------------------------------------
0번 청크 글자 수: 72
BERKSHIRE HATHAWAY INC.
To the Shareholders of Berkshire Hathaway Inc.:
------------------------------------------------------------
1번 청크 글자 수: 86
This letter comes to you as part of Berkshire’s annual report. As a public company, we
------------------------------------------------------------
2번 청크 글자 수: 70
are required to periodically tell you many specific facts and figures.
============================================================
전략 2: 큰 청크
청크 수: 211
------------------------------------------------------------
0번 청크 글자 수: 160
BERKSHIRE HATHAWAY INC.
To the Shareholders of Berkshire Hathaway Inc.:
This letter comes to you as part of Berkshire’s annual report. As a public company, we
------------------------------------------------------------
1번 청크 글자 수: 161
are required to periodically tell you many specific facts and figures.
“Report,” however, implies a greater responsibility. In addition to the mandated data, we
------------------------------------------------------------
2번 청크 글자 수: 178
believe we owe you additional commentary about what you own and how we think. Our goal is
to communicate with you in a manner that we would wish you to use if our positions were
============================================================
관찰 포인트:
1. 청크 크기가 작아지면 개수는 늘고, 내용은 더 잘게 분리됩니다.
2. 오버랩이 있으면 앞뒤 문맥이 일부 겹쳐 정보 손실을 줄일 수 있습니다.
3. 메타데이터를 붙여 두면 나중에 출처와 카테고리를 함께 추적할 수 있습니다.
확인할 것:
- 텍스트 파일과 PDF 모두 로드 가능한지
- 실제 투자 문서에서 어느 정도 크기가 적절해 보이는지
- 청크가 너무 작거나 너무 크면 어떤 문제가 생기는지
- 청크가 너무 작으면 문맥이 끊기지 않는지
- 청크가 너무 크면 검색 결과가 덜 정확해지지 않는지
GPT 추가설명 (확인할 것)
좋아요. 이 결과는 Week 3 실습이 아주 잘 된 사례예요. 텍스트 파일보다 PDF에서 청킹 차이가 훨씬 뚜렷하게 드러났어요.
이제 확인할 것들에 맞춰 하나씩 보면 됩니다.
1. 텍스트 파일과 PDF 모두 로드 가능한지
네, 둘 다 정상적으로 로드되었습니다.
이전 txt 실험에서는:
- 로드된 문서 수: 1
- 원본 메타데이터:
source
이번 PDF 실험에서는:
- 로드된 문서 수: 15
- 원본 메타데이터:
producer,creator,creationdate,moddate,title,source,total_pages,page,page_label
즉, PDF는 단순히 글만 읽는 게 아니라 페이지 단위로 나뉘어서 들어오고, 메타데이터도 더 풍부하게 붙는다는 점이 잘 보입니다.
특히 여기서 중요한 건:
total_pages: 15page: 0page_label: '1'
이런 정보가 있어서, 나중에 RAG 답변에서 “이 내용은 몇 페이지에서 나왔는지” 추적하기가 더 쉬워진다는 거예요.
2. 실제 투자 문서에서 어느 정도 크기가 적절해 보이는지
지금 결과만 보면, 너무 작은 청크는 지나치게 잘게 쪼개진 느낌이 있어요.
결과를 보면:
- 작은 청크: 439개
- 큰 청크: 211개
15페이지짜리 문서에서 작은 청크가 439개면, 페이지당 거의 29개 정도로 나뉜 셈이에요. 꽤 잘게 쪼개졌죠.
예를 들어 작은 청크의 앞부분은 이렇게 나왔어요.
- 0번: 제목 + 수신자
- 1번: 문장의 앞부분
- 2번: 같은 문장의 뒷부분
이건 초보자 실습용으로는 차이가 잘 보여서 좋지만, 실제 검색 품질 관점에서는 조금 너무 잘게 잘린 상태에 가깝습니다.
반면 큰 청크도 아직 엄청 크지는 않아요.
- 0번: 160자
- 1번: 161자
- 2번: 178자
이 정도면 여전히 비교적 작은 편이고, 적어도 문장이 좀 더 이어져 있어서 문맥 보존은 더 좋아 보입니다.
그래서 지금 실험 결과만 보면:
- 작은 청크(120/20) → 너무 잘게 나뉘는 경향
- 큰 청크(220/40) → 그나마 실제 문서용으로 더 적절해 보임
이라고 볼 수 있어요.
다만 이것도 여전히 실전용 최적값이라기보다는 비교 실험용에 가깝습니다. 실제 투자 PDF라면 보통 지금보다 더 큰 범위도 시험해보는 게 좋아요.
예를 들면:
- 400~800자 수준
- 혹은 토큰 기준으로 300~800 정도
처럼 더 넓게 실험해볼 수 있어요.
3. 청크가 너무 작거나 너무 크면 어떤 문제가 생기는지
너무 작은 경우
지금 작은 청크 결과가 딱 그 예시예요.
예를 들어:
-
1번 청크:
This letter comes to you as part of Berkshire’s annual report. As a public company, we -
2번 청크:
are required to periodically tell you many specific facts and figures.
원래는 한 흐름의 문장인데, 청크가 나뉘면서 문장이 중간에서 끊긴 모습이 보입니다.
이렇게 되면 문제는:
- 문맥이 둘로 찢어짐
- 검색 시 앞부분만 잡히거나 뒷부분만 잡힐 수 있음
- 답변 생성 시 문장이 어색하게 이어질 수 있음
즉, 검색은 세밀해질 수 있지만 문맥 손실 위험이 커집니다.
너무 큰 경우
이번 실험에서는 “너무 큰 청크”까지는 아직 아니에요. 220자 정도는 아주 큰 편은 아닙니다.
하지만 더 커지면:
- 서로 다른 주제가 한 청크에 섞일 수 있음
- 검색 시 딱 맞는 부분보다 넓은 덩어리가 같이 들어옴
- 임베딩 벡터가 여러 주제를 한꺼번에 대표하게 됨
그래서 검색 정확도가 떨어질 수 있어요.
예를 들어 한 청크 안에
- 보험 사업
- 철도 사업
- 현금 보유
- 일본 상사 투자
가 같이 섞여 있으면, 사용자가 “보험 float”만 물어봤을 때도 청크 전체가 묶여서 검색될 수 있겠죠.
4. 청크가 너무 작으면 문맥이 끊기지 않는지
네, 이번 PDF 결과에서는 실제로 문맥이 끊기는 모습이 보입니다.
가장 분명한 예가 이 부분이에요.
-
큰 청크 0번:
This letter comes to you as part of Berkshire’s annual report. As a public company, we -
큰 청크 1번:
are required to periodically tell you many specific facts and figures.
이건 한 문장이 청크 경계에서 갈라졌다고 볼 수 있어요.
즉, 지금 설정에서는 작은 청크뿐 아니라 큰 청크도 여전히 문장 분리 문제가 남아 있습니다. 다만 작은 청크 쪽이 더 심합니다.
이 결과를 통해 말할 수 있는 건:
실제 긴 투자 PDF에서는 너무 작은 청크 설정을 사용하면 문장 또는 논리 흐름이 여러 청크로 나뉘어 문맥 보존이 약해질 수 있다.
5. 청크가 너무 크면 검색 결과가 덜 정확해지지 않는지
이건 이번 결과만으로 직접 증명된 것은 아니고, 그렇게 될 가능성을 이해하는 단계예요.
왜냐하면 지금은 아직:
- 청킹만 해봤고
- 실제 검색(retrieval) 실험은 안 했기 때문이에요.
즉, 지금 결과만 보고 “큰 청크가 검색 정확도를 떨어뜨렸다” 라고 단정할 수는 없어요.
대신 이렇게 쓰는 건 맞아요:
청크가 커질수록 더 많은 문맥을 함께 보존할 수 있지만, 하나의 청크 안에 여러 정보가 함께 포함되어 검색 시 필요한 내용만 정확히 매칭되지 않을 가능성이 있다.
즉, 현재 단계에서는:
- 작은 청크의 문제 → 실제 출력에서 보임
- 큰 청크의 문제 → 이론적으로 예상 가능
- 실제로 어느 쪽이 더 검색 정확도가 좋은지 → 다음 단계에서 벡터 DB + 질의 실험으로 확인해야 함
입니다.
이번 결과로 쓸 수 있는 관찰 정리
이렇게 정리하면 좋아요.
실습 관찰 예시
실제 PDF 문서인 워렌 버핏 2024 주주서한을 대상으로 청킹을 수행한 결과, 텍스트 파일보다 훨씬 많은 청크가 생성되었고 청크 크기에 따른 차이도 뚜렷하게 나타났다. 작은 청크 전략에서는 문서가 439개 청크로 나뉘어 일부 문장과 문맥이 여러 청크로 분리되는 모습이 관찰되었다. 반면 큰 청크 전략에서는 211개 청크로 나뉘어 문맥 보존은 더 나아졌지만, 앞으로 청크 크기를 더 크게 설정할 경우 하나의 청크에 여러 주제가 함께 포함되어 검색 정확도가 낮아질 가능성이 있다. 따라서 실제 투자 문서에서는 너무 작은 청크보다는 문단이나 문장 흐름이 유지될 수 있는 적절한 중간 크기의 청크를 실험적으로 찾는 것이 중요하다고 볼 수 있다.
아주 짧게 답하면
- TXT와 PDF 모두 정상 로드됨
- PDF는 페이지 단위 메타데이터까지 들어와서 RAG용으로 더 의미 있는 실험이 됨
- 작은 청크는 실제로 문맥이 끊기는 모습이 보임
- 큰 청크는 문맥 보존이 더 낫지만, 검색 정확도는 실제 retrieval 실험으로 확인해야 함
- 현재 결과만 보면 작은 청크보다 큰 청크 쪽이 실제 투자 문서에 더 적절해 보임
GPT 추가설명 - 메타데이터
메타데이터는 데이터에 붙어 있는 설명 정보예요. 쉽게 말하면, 내용 그 자체는 아니지만 그 내용을 설명해주는 꼬리표라고 보면 됩니다.
예를 들어 청크의 실제 본문이 이런 거라면:
삼성전자는 2024년 들어 반도체 업황 회복의 영향을 받았다.
이 문장 자체가 본문 데이터예요.
그런데 여기에 이런 정보가 같이 붙어 있으면:
- 이 문장이 나온 파일 이름
- 몇 페이지에서 왔는지
- 몇 번째 청크인지
- 언제 저장했는지
- 어떤 카테고리 문서인지
이런 게 바로 메타데이터입니다.
비유하면
책 내용이 데이터라면,
- 책 제목
- 저자
- 출판일
- 몇 쪽인지
이런 건 메타데이터예요.
사진으로 비유하면,
사진 자체는 데이터고,
- 찍은 날짜
- 카메라 종류
- 위치
- 파일명
이런 건 메타데이터예요.
즉, 내용을 설명하는 부가 정보예요.
지금 청킹 코드에서의 메타데이터
당신 코드에서는 청크마다 이런 게 붙었죠.
{
'source': 'sample_investment_note.txt',
'chunk_id': 0,
'source_type': '.txt',
'added_date': '2026-04-08',
'category': '투자보고서'
}
이걸 하나씩 보면:
-
source→ 원본 파일이 무엇인지 →demo_note.txt -
chunk_id→ 몇 번째 청크인지 →0 -
source_type→ 파일 종류가 무엇인지 →.txt -
added_date→ 이 메타데이터를 붙인 날짜 →2026-04-08 -
category→ 어떤 종류의 문서인지 →투자보고서
이 정보들은 청크 내용 자체는 아니죠. 하지만 그 청크를 이해하고 관리하는 데 필요한 정보예요. 그래서 메타데이터라고 부릅니다.
왜 필요한데요?
RAG에서는 메타데이터가 꽤 중요해요.
1. 출처를 보여줄 수 있음
나중에 답변할 때
- 어떤 파일에서 왔는지
- 몇 페이지인지
를 같이 보여줄 수 있어요.
예:
- 출처:
samsung_report.pdf, 12페이지
이러면 사용자가 “이 말이 어디서 나왔는지” 확인할 수 있죠.
2. 필터링할 수 있음
예를 들어 문서가 엄청 많아지면 이런 식으로 찾고 싶을 수 있어요.
- 투자보고서만 검색
- PDF만 검색
- 2026년에 넣은 문서만 검색
이럴 때 메타데이터가 있으면 가능해요.
예:
category == "투자보고서"인 청크만 검색source_type == ".pdf"인 것만 검색
3. 결과를 정리하기 쉬움
검색 결과가 나왔을 때
- 어떤 문서에서 왔는지
- 같은 파일 안에서 몇 번째 청크인지
알 수 있어서 디버깅하기 좋아요.
예를 들어 “왜 이상한 답이 나왔지?” 했을 때 메타데이터를 보면 어느 파일의 어느 청크가 검색되었는지 추적할 수 있어요.
본문 데이터와 메타데이터 차이
본문 데이터
삼성전자는 2024년 들어 반도체 업황 회복의 영향을 받았다.
메타데이터
{
"source": "sample_investment_note.txt",
"chunk_id": 0,
"category": "투자보고서"
}
즉,
- 본문 데이터 = 실제 내용
- 메타데이터 = 그 내용을 설명하는 정보
입니다.
청크 하나를 상자라고 생각하면 쉬워요
청크 하나에는 보통 두 가지가 있어요.
1. 내용
chunk.page_content
2. 설명 정보
chunk.metadata
즉 LangChain의 Document나 청크는 보통 이런 느낌이에요.
{
"page_content": "삼성전자는 2024년 들어 반도체 업황 회복의 영향을 받았다.",
"metadata": {
"source": "sample_investment_note.txt",
"chunk_id": 0,
"category": "투자보고서"
}
}
꼭 넣으면 좋은 메타데이터 예시
RAG용 문서에서는 보통 이런 것들을 많이 넣어요.
source: 공개 가능한 샘플 파일명 또는 문서 식별자page: PDF 페이지 번호chunk_id: 몇 번째 청크인지title: 문서 제목date: 문서 날짜category: 보고서/뉴스/공시 같은 분류author: 작성자company: 삼성전자, 엔비디아 같은 대상 기업
지금 단계에서 아주 쉽게 기억하면
메타데이터는
“이 텍스트가 어디서 왔고, 어떤 성격인지 알려주는 추가 정보”
입니다.
한 줄 예시
이 문장:
“HBM 수요 증가로 메모리 기대감이 커졌다.”
에 대해 메타데이터는 이런 느낌이에요:
- 어디서 왔는가? →
demo_note.txt - 몇 번째 조각인가? →
chunk_id = 0 - 무슨 종류 문서인가? →
투자보고서 - 언제 넣었는가? →
2026-04-08
GPT 추가설명 - 누가 메타데이터를 넣는가?
상황에 따라 달라요. 일부는 로더가 자동으로 넣어주고, 일부는 개발자가 직접 넣습니다.
지금 코드 기준으로 나눠서 보면 이해가 쉬워요.
1. 로더가 자동으로 넣어주는 메타데이터
예를 들어 이런 코드가 있죠.
loader = PyPDFLoader(str(file_path))
documents = loader.load()
또는
loader = TextLoader(str(file_path), encoding="utf-8")
documents = loader.load()
이때 loader.load()를 하면 LangChain의 Document 객체를 만들어주는데,
그 안에 기본 메타데이터를 어느 정도 같이 넣어주는 경우가 많아요.
예를 들면:
source: 원본 파일 경로page: PDF라면 페이지 번호
같은 것들요.
당신 결과에도 이미 이게 보였죠.
원본 메타데이터: {'source': 'sample_investment_note.txt'}
이 source는 TextLoader가 넣어준 것이라고 보면 됩니다.
즉, 이건 OpenAI API가 넣어준 게 아니라 LangChain의 document loader가 문서를 읽으면서 붙여준 메타데이터예요.
2. 개발자가 직접 넣는 메타데이터
반면 당신 코드의 이 부분은:
chunk.metadata.update(
{
"chunk_id": index,
"source_type": Path(chunk.metadata.get("source", "")).suffix.lower() or ".txt",
"added_date": added_date,
"category": category,
}
)
이건 당신이 직접 추가한 메타데이터예요.
즉,
chunk_idsource_typeadded_datecategory
이 4개는 로더가 자동으로 준 게 아니라 코드 작성자가 필요해서 넣은 것입니다.
아주 간단히 구분하면
자동으로 들어올 수 있는 것
- 파일 경로 (
source) - 페이지 번호 (
page) - 문서 관련 기본 정보
직접 넣는 것
chunk_idcategoryadded_date- 회사명, 보고서 종류, 작성자 같은 커스텀 정보
Q. OpenAI API가 넣어주는 건가요?
보통 아니에요.
OpenAI API는 기본적으로:
- 임베딩 만들기
- 답변 생성하기
같은 일을 하지,
문서의 메타데이터를 알아서 관리해주지는 않습니다.
즉,
- 문서를 읽어오는 단계 → 로더가 일부 메타데이터 생성
- 추가 정보 붙이는 단계 → 개발자가 직접 넣음
- 임베딩 API / LLM API → 텍스트 처리
이런 흐름이에요.
지금 코드 흐름으로 보면
1단계: 파일 로드
documents = load_documents(file_path)
여기서 로더가 기본 메타데이터를 붙일 수 있음
예: source
2단계: 청킹
chunks_small = splitter_small.split_documents(documents)
문서를 청크로 나눔 기존 메타데이터는 보통 청크로 이어짐
3단계: 직접 추가
enrich_metadata(chunks_small, args.category)
여기서 chunk_id, category 같은 걸 직접 붙임
비유하면
로더는 택배 상자에 원래 붙어 있는 기본 라벨을 붙여주는 사람이고,
- 발송지
- 송장번호 비슷한 기본 정보
개발자는 거기에 추가로 포스트잇을 붙이는 사람이에요.
- 중요
- 투자보고서
- 3번째 조각
- 오늘 추가함
이런 식으로요.
그래서 결론
메타데이터는
- 일부는 로더가 자동으로 넣어줄 수 있고
- 대부분 실무에서는 개발자가 필요한 걸 직접 더 붙입니다
- OpenAI API가 자동으로 넣어주는 건 보통 아닙니다
정도로 이해하면 딱 맞아요.
파일 설명
chunking_experiment.py: 청크 크기 2가지와 메타데이터를 함께 비교하는 실습 스크립트sample_investment_note.txt: 바로 실행해 볼 수 있는 기본 샘플 문서
실습 후 직접 답해보기
- 문서 전체를 한 번에 임베딩하면 왜 검색 품질이 떨어질까?
- 청크 오버랩이 없으면 어떤 정보 손실이 생길까?
- 출처 추적을 위해 어떤 메타데이터가 꼭 필요할까?
GPT 추가설명
좋아요. 지금 쓴 답은 방향은 맞는데, 조금만 더 구체적으로 적으면 훨씬 좋아져요. 특히 마지막 질문은 헷갈릴 수 있는데, 그 이유까지 같이 설명해볼게요.
문서 전체를 한 번에 임베딩하면 왜 검색 품질이 떨어질까?
지금 쓴 답:
하나의 임베딩 결과에 문서 전체를 담기 어려워서.
이 말은 맞아요. 다만 조금 더 풀면 더 좋습니다.
추천 답:
문서 전체를 한 번에 임베딩하면 하나의 벡터 안에 여러 주제와 정보가 함께 섞이게 된다. 그러면 사용자의 질문과 직접 관련된 부분만 정확하게 찾기 어려워져 검색 품질이 떨어질 수 있다.
쉽게 말하면, 한 문서 안에
- 반도체
- 스마트폰
- 환율
- RAG 설명
이 다 들어 있으면, 그 전체를 한 벡터로 만들었을 때 질문과 딱 맞는 부분만 뽑아내기 어려워지는 것이에요.
청크 오버랩이 없으면 어떤 정보 손실이 생길까?
지금 쓴 답:
문맥을 잃어버릴 수 있다.
이것도 맞아요. 다만 “어떻게” 잃는지 한 줄만 더 쓰면 훨씬 좋아요.
추천 답:
청크 오버랩이 없으면 문장이나 문맥이 청크 경계에서 끊어질 수 있다. 그 결과 앞 청크와 뒤 청크의 연결이 약해져, 검색 시 중요한 정보가 일부 빠지거나 의미가 불완전하게 전달될 수 있다.
예를 들면 한 문장이
- 앞 청크 끝
- 뒤 청크 시작
으로 갈라졌는데 오버랩이 없으면, 검색된 청크 하나만 봤을 때 문장이 반쪽만 보일 수 있겠죠.
출처 추적을 위해 어떤 메타데이터가 꼭 필요할까?
먼저, 왜 출처 추적이 필요하냐면:
- 답변이 어느 문서에서 나온 말인지 확인하려고
- 사용자가 원문을 다시 찾아볼 수 있게 하려고
- 잘못된 답변이 나오면 어떤 문서/청크에서 왔는지 디버깅하려고
즉, RAG는 그냥 답만 하는 게 아니라 **“이 말이 어디서 나왔는지 보여줄 수 있어야 신뢰성이 높아진다”**는 점이 중요해요.
그래서 추천 답은:
출처 추적을 위해서는 최소한 원본 파일명(source), 페이지 번호(page), 그리고 몇 번째 청크인지 구분할 수 있는 chunk_id 같은 메타데이터가 필요하다. 이러한 정보가 있으면 답변이 어떤 문서의 어느 위치에서 나온 것인지 확인할 수 있다.
더 간단히 쓰고 싶으면:
출처 추적을 위해서는 파일명과 페이지 번호가 가장 중요하다. 필요하면 chunk_id와 날짜, 카테고리도 함께 저장해 어떤 문서의 어느 부분에서 나온 정보인지 확인할 수 있게 해야 한다.
정리해서 다시 쓰면
문서 전체를 한 번에 임베딩하면 왜 검색 품질이 떨어질까?
문서 전체를 한 번에 임베딩하면 하나의 벡터 안에 여러 주제와 정보가 함께 섞이게 된다. 그러면 질문과 직접 관련된 부분만 정확하게 찾기 어려워져 검색 품질이 떨어질 수 있다.
청크 오버랩이 없으면 어떤 정보 손실이 생길까?
청크 오버랩이 없으면 문장이나 문맥이 청크 경계에서 끊어질 수 있다. 그 결과 검색 시 중요한 정보가 일부 빠지거나 의미가 불완전하게 전달될 수 있다.
출처 추적을 위해 어떤 메타데이터가 꼭 필요할까?
출처 추적을 위해서는 원본 파일명, 페이지 번호, 그리고 청크 번호 같은 메타데이터가 필요하다. 이러한 정보가 있으면 답변이 어떤 문서의 어느 부분에서 나온 것인지 확인할 수 있다.
추천 자료
완료 기준
-
python chunking_experiment.py실행 후 청킹 결과를 확인했다 - 청크 크기를 바꿨을 때 개수와 내용 차이를 설명할 수 있다
- 청크 메타데이터에 출처와 날짜가 들어가는 이유를 설명할 수 있다
- 청킹 결과를 보고 어떤 설정이 더 적절한지 이유를 말할 수 있다