본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 26. · 3 Views
Parent-Child Chunking 완벽 가이드
RAG 시스템에서 검색 정확도와 컨텍스트 품질을 동시에 높이는 Parent-Child Chunking 전략을 실무 중심으로 학습합니다. 작은 청크로 검색하고 큰 청크로 제공하는 계층적 접근법을 마스터하세요.
목차
1. 작은 청크로 검색, 큰 청크로 제공
검색은 정확하게, 답변은 풍부하게 신입 개발자 이민준 씨는 회사의 RAG 시스템을 개선하는 프로젝트를 맡았습니다. 사용자들이 "검색 결과는 정확한데 답변이 부족하다"고 불평하는 일이 잦았습니다.
선배 최지혜 씨가 코드를 보더니 이렇게 말했습니다. "검색용 청크와 답변용 청크를 분리해보는 게 어때요?"
핵심 개념을 3-4문장으로 Parent-Child Chunking은 한마디로 검색과 답변에 서로 다른 크기의 청크를 사용하는 것입니다. 마치 도서관에서 카드 목록으로 책을 찾고, 실제로는 책 전체를 읽는 것과 같습니다.
작은 청크로 정확하게 검색하고, 큰 청크로 풍부한 컨텍스트를 제공합니다. 이것을 제대로 이해하면 검색 정확도와 답변 품질을 동시에 높일 수 있습니다.
다음 코드를 살펴봅시다.
from langchain.text_splitter import RecursiveCharacterTextSplitter
# Parent Splitter: 큰 청크 생성 (답변용)
parent_splitter = RecursiveCharacterTextSplitter(
chunk_size=2000, # 컨텍스트 제공용
chunk_overlap=200
)
# Child Splitter: 작은 청크 생성 (검색용)
child_splitter = RecursiveCharacterTextSplitter(
chunk_size=400, # 정확한 검색용
chunk_overlap=50
)
# 문서를 Parent 청크로 분할
parent_chunks = parent_splitter.split_documents(documents)
# 각 Parent의 Child 청크 생성
for parent in parent_chunks:
children = child_splitter.split_text(parent.page_content)
# Child는 검색에 사용, Parent는 답변에 사용
store_parent_child_relationship(parent, children)
이북처럼 술술 읽히는 설명 이민준 씨는 입사 2개월 차 신입 개발자입니다. 회사에서 운영하는 기술 문서 검색 시스템이 있는데, 사용자 피드백이 엇갈렸습니다.
"검색은 잘 되는데, 답변이 부족해요." 선배들이 모인 회의에서 최지혜 씨가 말했습니다. "Parent-Child Chunking을 도입해보면 어떨까요?" 이민준 씨는 고개를 갸우뚱했습니다.
청크를 두 종류로 나눈다니, 복잡해 보였습니다. 검색과 답변, 서로 다른 요구사항 그렇다면 왜 청크를 두 종류로 나눠야 할까요?
쉽게 비유하자면, 검색은 마치 백과사전의 색인과 같습니다. "인공지능"이라는 키워드로 관련 페이지를 찾을 때는 짧고 정확한 단어가 효과적입니다.
반면 답변은 실제 페이지 내용을 읽는 것과 같습니다. 키워드만 보여주면 문맥을 이해할 수 없습니다.
전후 문맥이 필요합니다. 이처럼 Parent-Child Chunking도 검색용 작은 청크와 답변용 큰 청크를 분리해서 관리합니다.
기존 방식의 딜레마 Parent-Child Chunking이 없던 시절에는 어땠을까요? 개발자들은 청크 크기를 정할 때 항상 고민에 빠졌습니다.
청크를 작게 만들면 검색은 정확하지만 답변이 부족했습니다. "이 함수가 무엇인가요?"라는 질문에 함수 시그니처만 반환되는 식이었습니다.
반대로 청크를 크게 만들면 답변은 풍부하지만 검색 정확도가 떨어졌습니다. 불필요한 내용까지 검색되어 관련 없는 결과가 나왔습니다.
더 큰 문제는 이 딜레마를 해결할 방법이 없었다는 것입니다. 하나의 청크 크기로는 검색과 답변 두 마리 토끼를 잡을 수 없었습니다.
계층적 접근법의 등장 바로 이런 문제를 해결하기 위해 Parent-Child Chunking이 등장했습니다. Parent-Child Chunking을 사용하면 검색 정확도와 답변 품질을 동시에 개선할 수 있습니다.
작은 Child 청크로 정확한 위치를 찾고, 큰 Parent 청크로 충분한 컨텍스트를 제공합니다. 무엇보다 사용자 경험이 크게 향상된다는 이점이 있습니다.
동작 원리: 2단계 프로세스 위의 코드를 한 줄씩 살펴보겠습니다. 먼저 parent_splitter는 2000자 크기의 큰 청크를 만듭니다.
이것이 실제 답변에 사용될 Parent 청크입니다. 다음으로 child_splitter는 400자 크기의 작은 청크를 생성합니다.
이것이 벡터 검색에 사용될 Child 청크입니다. 핵심은 관계 저장입니다.
각 Child 청크는 자신이 속한 Parent 청크의 ID를 기억합니다. 검색 시에는 Child로 찾고, 답변 시에는 Parent를 가져옵니다.
검색 흐름 이해하기 사용자가 "Python 데코레이터란 무엇인가요?"라고 질문했다고 가정해봅시다. 시스템은 먼저 질문을 벡터로 변환합니다.
그 다음 Child 청크들과 유사도를 계산합니다. "데코레이터"라는 키워드가 포함된 작은 청크들이 정확하게 검색됩니다.
이제 시스템은 검색된 Child의 Parent ID를 확인합니다. 그리고 전체 Parent 청크를 가져와 LLM에게 전달합니다.
사용자는 데코레이터의 정의뿐만 아니라 사용 예제, 주의사항까지 포함된 풍부한 답변을 받게 됩니다. 실무 활용 사례 실제 현업에서는 어떻게 활용할까요?
예를 들어 기술 문서 검색 시스템을 개발한다고 가정해봅시다. API 문서에서 특정 엔드포인트를 검색할 때 Child 청크로 정확한 엔드포인트를 찾고, Parent 청크로 요청/응답 예제, 에러 코드, 사용 가이드까지 함께 제공할 수 있습니다.
많은 SaaS 기업에서 이런 패턴을 적극적으로 사용하고 있습니다. 주의사항: 저장 공간 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 모든 청크를 벡터DB에 저장하는 것입니다. Child와 Parent를 모두 임베딩하면 저장 공간이 두 배로 늘어납니다.
따라서 Child만 벡터DB에, Parent는 문서 저장소에 보관하는 것이 효율적입니다. 성공 사례 다시 이민준 씨의 이야기로 돌아가 봅시다.
Parent-Child Chunking을 적용한 후 사용자 만족도가 크게 올랐습니다. "이제 답변이 정말 유용해요!" Parent-Child Chunking을 제대로 이해하면 검색 시스템의 품질을 한 단계 높일 수 있습니다.
여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 실전 팁
- Child 크기는 400-600자, Parent는 1500-2000자가 적당합니다
- 문서 타입에 따라 비율을 조정하세요 (코드는 1:3, 문서는 1:5)
- Parent는 NoSQL DB에, Child만 벡터DB에 저장하면 비용을 절감할 수 있습니다
2. 계층적 문서 구조
문서의 자연스러운 계층 활용하기 주니어 개발자 박서준 씨는 회사의 사용자 매뉴얼 검색 시스템을 개선하고 있었습니다. 문서를 임의로 자르다 보니 제목과 본문이 분리되는 문제가 발생했습니다.
시니어 김나연 씨가 조언했습니다. "문서의 구조를 살려서 청킹하면 어떨까요?
섹션을 Parent로, 문단을 Child로 사용하는 거죠."
핵심 개념을 3-4문장으로 계층적 문서 구조는 문서가 가진 자연스러운 구조를 청킹에 활용하는 것입니다. 마치 책의 챕터와 문단 관계처럼, 섹션 단위를 Parent로 하위 내용을 Child로 구성합니다.
임의로 자르는 대신 의미 있는 경계를 활용합니다. 이것을 제대로 이해하면 문맥이 보존된 고품질 청크를 만들 수 있습니다.
다음 코드를 살펴봅시다.
from langchain.schema import Document
def create_hierarchical_chunks(markdown_doc):
sections = split_by_headers(markdown_doc) # ## 헤더로 분할
parent_child_pairs = []
for section in sections:
# Parent: 전체 섹션 (제목 + 내용)
parent = Document(
page_content=section.full_content,
metadata={"type": "parent", "title": section.title}
)
# Child: 섹션 내 문단들
paragraphs = section.content.split('\n\n')
children = [
Document(
page_content=f"{section.title}\n{para}", # 제목 포함
metadata={"type": "child", "parent_id": section.id}
)
for para in paragraphs if para.strip()
]
parent_child_pairs.append((parent, children))
return parent_child_pairs
이북처럼 술술 읽히는 설명 박서준 씨는 입사 4개월 차로 RAG 시스템 개선 프로젝트를 맡았습니다. 기존 시스템은 문서를 500자 단위로 기계적으로 자르고 있었습니다.
그러다 보니 이런 문제가 생겼습니다. "설치 방법"이라는 제목이 내용과 분리되어 사용자가 무슨 설치인지 알 수 없었습니다.
김나연 선배가 모니터를 보다가 말했습니다. "문서 구조를 활용해보면 어떨까요?" 문서는 이미 구조를 가지고 있다 그렇다면 계층적 문서 구조란 정확히 무엇일까요?
쉽게 비유하자면, 문서는 마치 건물의 구조와 같습니다. 책에는 챕터가 있고, 그 안에 섹션이 있으며, 섹션 안에 문단이 있습니다.
마크다운 문서도 마찬가지입니다. # 제목, ## 부제목, 그리고 본문 문단들이 계층을 이룹니다.
이처럼 계층적 문서 구조는 이미 존재하는 의미 경계를 청킹에 활용하는 것입니다. 임의 분할의 문제점 기존의 고정 길이 분할 방식에는 어떤 문제가 있었을까요?
개발자들은 문서를 500자, 1000자 같은 고정 길이로 잘랐습니다. 하지만 문서는 그런 식으로 작성되지 않습니다.
중요한 제목이 본문과 분리되기도 하고, 하나의 개념 설명이 두 청크로 쪼개지기도 했습니다. 더 큰 문제는 문맥 손실이었습니다.
"이 방법은 위험합니다"라는 문장이 무슨 방법인지 알 수 없게 되는 식이었습니다. 검색 결과를 받은 사용자는 혼란스러웠습니다.
"이게 무슨 내용이지?" 프로젝트가 커질수록 이런 불만이 늘어났습니다. 자연스러운 경계 활용 바로 이런 문제를 해결하기 위해 계층적 청킹이 등장했습니다.
계층적 청킹을 사용하면 의미 있는 단위로 문서를 나눌 수 있습니다. 섹션은 완전한 개념을 담고 있고, 문단은 하나의 주제를 설명합니다.
또한 제목 정보도 자연스럽게 보존됩니다. 무엇보다 문맥이 유지된다는 큰 이점이 있습니다.
코드 분석: 구조 파싱 위의 코드를 한 줄씩 살펴보겠습니다. 먼저 split_by_headers 함수는 마크다운의 ## 헤더를 기준으로 섹션을 분리합니다.
이것이 핵심입니다. 각 섹션 전체가 Parent 청크가 됩니다.
다음으로 섹션 내용을 \n\n(빈 줄)으로 분리하여 문단을 추출합니다. 각 문단이 Child 청크가 되는데, 중요한 점은 제목을 포함한다는 것입니다.
마지막으로 Parent ID를 Child의 메타데이터에 저장하여 관계를 보존합니다. 제목 포함의 중요성 왜 Child 청크에 제목을 포함해야 할까요?
"requirements.txt에 추가합니다"라는 문장만 있으면 무엇을 추가하는지 알 수 없습니다. 하지만 "설치 방법: requirements.txt에 추가합니다"처럼 제목과 함께 저장하면 문맥이 명확해집니다.
검색 시에도 제목이 키워드 역할을 하여 정확도가 올라갑니다. 실무 활용: 기술 문서 실제 현업에서는 어떻게 활용할까요?
예를 들어 API 문서를 관리한다고 가정해봅시다. "인증" 섹션을 Parent로, "API 키 발급", "토큰 갱신", "권한 확인" 같은 하위 항목을 Child로 구성할 수 있습니다.
사용자가 "토큰 갱신"을 검색하면 해당 문단을 찾고, 답변할 때는 "인증" 섹션 전체를 제공하여 전체적인 인증 흐름을 이해할 수 있게 합니다. 많은 문서 플랫폼에서 이런 패턴을 사용하고 있습니다.
다양한 문서 포맷 마크다운뿐만 아니라 다른 포맷에도 적용할 수 있습니다. HTML은 <h1>, <h2> 태그를 기준으로 분할할 수 있습니다.
PDF는 폰트 크기와 스타일로 제목을 감지합니다. Notion이나 Confluence 같은 문서 도구는 자체 계층 구조를 제공합니다.
중요한 것은 문서가 가진 자연스러운 구조를 활용한다는 원칙입니다. 주의사항: 불균형 처리 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 섹션 크기를 고려하지 않는 것입니다. 어떤 섹션은 10줄이고 어떤 섹션은 1000줄일 수 있습니다.
너무 큰 섹션은 추가로 분할하고, 너무 작은 섹션은 병합하는 로직이 필요합니다. 따라서 최소/최대 크기 제한을 두는 것이 좋습니다.
성공 사례 다시 박서준 씨의 이야기로 돌아가 봅시다. 계층적 청킹을 적용한 후 사용자 불만이 80% 감소했습니다.
"이제 검색 결과가 이해하기 쉬워요!" 계층적 문서 구조를 제대로 이해하면 더 자연스럽고 의미 있는 청킹을 할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 실전 팁
- 마크다운은
##레벨을 Parent로 사용하는 것이 적당합니다 - Child 청크에는 항상 Parent의 제목을 포함하세요
- 섹션이 2000자를 넘으면 추가 분할을 고려하세요
3. 실습: 2단계 청킹 시스템
실전 코드로 구현해보기 신입 개발자 정수민 씨는 이론은 이해했지만 실제 구현이 막막했습니다. "Parent와 Child를 어떻게 저장하고 검색하죠?" 멘토 이준호 씨가 노트북을 열었습니다.
"실제 코드를 함께 작성해봅시다. ChromaDB로 간단하게 구현할 수 있어요."
핵심 개념을 3-4문장으로 2단계 청킹 시스템은 Parent-Child 관계를 실제로 구현하는 것입니다. 마치 데이터베이스의 외래 키처럼, Child가 Parent를 참조하도록 설계합니다.
Child로 검색하고 Parent로 답변하는 전체 파이프라인을 구축합니다. 이것을 제대로 이해하면 실무에서 바로 사용할 수 있는 시스템을 만들 수 있습니다.
다음 코드를 살펴봅시다.
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
import uuid
class ParentChildRetriever:
def __init__(self):
self.embeddings = OpenAIEmbeddings()
# Child 청크만 벡터DB에 저장
self.child_store = Chroma(embedding_function=self.embeddings)
# Parent 청크는 별도 저장소에
self.parent_store = {}
def add_documents(self, parent_child_pairs):
for parent, children in parent_child_pairs:
parent_id = str(uuid.uuid4())
# Parent 저장
self.parent_store[parent_id] = parent.page_content
# Child에 parent_id 추가하여 벡터DB에 저장
for child in children:
child.metadata['parent_id'] = parent_id
self.child_store.add_documents(children)
def retrieve(self, query, k=3):
# Child로 검색
child_results = self.child_store.similarity_search(query, k=k)
# Parent 가져오기
parent_ids = [doc.metadata['parent_id'] for doc in child_results]
parents = [self.parent_store[pid] for pid in set(parent_ids)]
return parents
이북처럼 술술 읽히는 설명 정수민 씨는 입사 1개월 차로 처음으로 RAG 시스템을 맡았습니다. 이론 강의에서 Parent-Child Chunking을 배웠지만, 막상 코드를 작성하려니 막막했습니다.
"어디서부터 시작하지?" 멘토 이준호 씨가 옆자리에 앉았습니다. "같이 차근차근 만들어봅시다." 시스템 설계 개요 그렇다면 2단계 청킹 시스템을 어떻게 설계해야 할까요?
쉽게 비유하자면, 이 시스템은 마치 도서관의 이중 카탈로그와 같습니다. 얇은 색인 카드로 빠르게 책을 찾고, 실제로는 두꺼운 책 전체를 빌려주는 것입니다.
Child 청크는 검색을 위한 색인이고, Parent 청크는 실제 콘텐츠입니다. 이처럼 2단계 시스템도 검색 레이어와 저장 레이어를 분리합니다.
저장 구조 설계 시스템을 구현하기 전에 어떤 문제를 해결해야 했을까요? 개발자들은 모든 청크를 벡터DB에 저장하면 비용이 많이 든다는 것을 알았습니다.
Parent 청크는 크기가 크고, 벡터 임베딩 비용도 높습니다. 또한 Parent를 검색에 사용하지 않는데도 저장 공간을 차지했습니다.
더 큰 문제는 중복된 정보였습니다. Child의 내용이 이미 Parent에 포함되어 있는데 두 번 저장하는 셈이었습니다.
효율적인 설계가 필요했습니다. 분리 저장 전략 바로 이런 문제를 해결하기 위해 분리 저장이 등장했습니다.
분리 저장을 사용하면 비용을 절감할 수 있습니다. Child만 벡터 임베딩하므로 임베딩 비용이 줄어듭니다.
또한 빠른 검색이 가능합니다. 작은 Child 청크들 사이에서 검색하므로 속도가 빠릅니다.
무엇보다 유연한 확장이 가능하다는 이점이 있습니다. Parent는 관계형 DB, NoSQL, 심지어 파일 시스템에도 저장할 수 있습니다.
코드 분석: 초기화 위의 코드를 한 줄씩 살펴보겠습니다. 먼저 __init__ 메서드에서 두 개의 저장소를 초기화합니다.
child_store는 ChromaDB로 벡터 검색을 담당합니다. 이것이 핵심입니다.
parent_store는 단순한 딕셔너리로, Parent 청크의 텍스트를 저장합니다. 실제 프로덕션에서는 Redis나 MongoDB를 사용할 수 있습니다.
문서 추가 로직 add_documents 메서드는 어떻게 동작할까요? 각 Parent-Child 쌍을 받아서 처리합니다.
먼저 Parent에 고유한 UUID를 생성합니다. 이 ID가 관계의 키가 됩니다.
Parent 내용을 parent_store에 저장하고, 모든 Child의 메타데이터에 parent_id를 추가합니다. 마지막으로 Child들을 벡터DB에 저장합니다.
이렇게 하면 Child에서 Parent로 역참조가 가능해집니다. 검색 파이프라인 retrieve 메서드가 핵심입니다.
사용자 질문이 들어오면 먼저 Child 청크들과 유사도 검색을 수행합니다. 상위 k개의 Child를 찾습니다.
다음으로 각 Child의 메타데이터에서 parent_id를 추출합니다. 중복을 제거한 후 해당하는 Parent들을 parent_store에서 가져옵니다.
이 Parent들이 최종적으로 LLM에 전달됩니다. 실무 활용: 확장 버전 실제 현업에서는 어떻게 활용할까요?
예를 들어 고객 지원 챗봇을 개발한다고 가정해봅시다. 수천 개의 FAQ 문서를 Parent로, 각 문단을 Child로 구성합니다.
사용자가 "환불 정책이 어떻게 되나요?"라고 물으면 Child 청크로 관련 문단을 찾고, Parent로 전체 환불 정책 섹션을 제공합니다. 이렇게 하면 사용자는 부분적인 답변이 아닌 완전한 정보를 받게 됩니다.
많은 SaaS 기업에서 이런 패턴을 사용하고 있습니다. 성능 최적화 더 나아가 성능을 개선할 수도 있습니다.
Parent 저장소로 Redis를 사용하면 캐싱 효과를 얻을 수 있습니다. 자주 검색되는 Parent는 메모리에 상주하여 응답 속도가 빨라집니다.
또한 Parent를 압축 저장하면 저장 공간을 절약할 수 있습니다. 검색 시에만 압축을 해제하면 됩니다.
주의사항: Parent ID 관리 하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 Parent ID 충돌을 고려하지 않는 것입니다.
UUID를 사용하지 않고 단순히 증가하는 숫자를 쓰면 문서를 여러 번 추가할 때 충돌이 발생합니다. 따라서 UUID나 해시값을 사용해야 합니다.
또한 Parent 저장소가 꽉 찼을 때의 처리 로직도 필요합니다. 테스트와 검증 다시 정수민 씨의 이야기로 돌아가 봅시다.
이준호 멘토와 함께 코드를 완성한 정수민 씨는 테스트를 돌렸습니다. "검색도 잘 되고, 답변도 풍부하네요!" 멘토가 웃으며 말했습니다.
"이제 실전에 투입해볼까요?" 2단계 청킹 시스템을 제대로 이해하면 확장 가능한 RAG 시스템을 구축할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 실전 팁
- Parent 저장소는 Redis나 MongoDB를 사용하면 확장성이 좋습니다
- Child 수가 많으면 배치로 처리하여 메모리를 절약하세요
- Parent ID는 항상 UUID를 사용하여 충돌을 방지하세요
4. 실습: 컨텍스트 품질 개선
답변 품질을 한 단계 높이기 중급 개발자 한지우 씨는 Parent-Child 시스템을 구축했지만 여전히 문제가 있었습니다. 때로는 관련 없는 Parent가 반환되어 답변이 산만했습니다.
아키텍트 송민석 씨가 코드를 리뷰하며 말했습니다. "Parent 선택을 더 정교하게 해야 해요.
리랭킹과 필터링을 추가해봅시다."
핵심 개념을 3-4문장으로 컨텍스트 품질 개선은 올바른 Parent를 선택하고 불필요한 정보를 제거하는 것입니다. 마치 도서관 사서가 관련 없는 책을 제외하고 꼭 필요한 책만 추천하는 것과 같습니다.
리랭킹, 중복 제거, 관련도 필터링을 통해 컨텍스트를 정제합니다. 이것을 제대로 이해하면 LLM에게 최적의 정보를 제공할 수 있습니다.
다음 코드를 살펴봅시다.
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor
class ImprovedParentChildRetriever(ParentChildRetriever):
def __init__(self, llm):
super().__init__()
self.llm = llm
# 리랭킹을 위한 컴프레서
self.compressor = LLMChainExtractor.from_llm(llm)
def retrieve_with_quality(self, query, k=3, min_score=0.7):
# 1단계: Child로 검색 (더 많이 가져오기)
child_results = self.child_store.similarity_search_with_score(
query, k=k*2
)
# 2단계: 점수 필터링
filtered = [
(doc, score) for doc, score in child_results
if score >= min_score
]
# 3단계: Parent 가져오기 및 중복 제거
parent_ids = list(set([
doc.metadata['parent_id'] for doc, _ in filtered
]))
parents = [
self.parent_store[pid] for pid in parent_ids
]
# 4단계: Parent 리랭킹 (질문과의 관련도 재평가)
reranked = self.rerank_parents(query, parents)
return reranked[:k]
def rerank_parents(self, query, parents):
# LLM으로 관련도 재평가
scored = []
for parent in parents:
relevance = self.compute_relevance(query, parent)
scored.append((parent, relevance))
return [p for p, _ in sorted(scored, key=lambda x: x[1], reverse=True)]
이북처럼 술술 읽히는 설명 한지우 씨는 입사 1년 차로 RAG 시스템을 운영하고 있었습니다. Parent-Child 청킹을 도입한 후 답변 품질은 개선되었지만, 가끔씩 이상한 결과가 나왔습니다.
사용자가 "로그인 오류"를 물었는데 "회원가입" 섹션이 함께 반환되는 식이었습니다. 아키텍트 송민석 씨가 모니터링 대시보드를 보며 말했습니다.
"컨텍스트 품질을 높여야겠네요." 품질 문제의 근본 원인 그렇다면 왜 관련 없는 Parent가 선택되는 걸까요? 쉽게 비유하자면, 이것은 마치 도서관에서 비슷한 제목의 책을 모두 가져오는 것과 같습니다.
"Python 입문"을 찾는데 "Python 고급", "Python 데이터 분석"까지 가져오면 혼란스럽습니다. Child 검색에서는 "Python"이라는 키워드가 일치하지만, 실제 질문의 의도와는 거리가 멀 수 있습니다.
이처럼 Child의 관련도가 Parent의 관련도를 보장하지 않습니다. 기존 접근법의 한계 단순한 Parent-Child 검색에는 어떤 문제가 있었을까요?
개발자들은 Child로 검색한 후 자동으로 Parent를 반환했습니다. 하지만 하나의 Parent 안에 여러 주제가 섞여 있을 수 있습니다.
"인증" 섹션에 "로그인", "회원가입", "비밀번호 재설정"이 모두 포함되어 있다면 어떨까요? "로그인" Child가 매칭되어도 Parent 전체는 질문과 부분적으로만 관련됩니다.
더 큰 문제는 여러 Child가 같은 Parent를 가리킬 때였습니다. 중복된 Parent가 여러 번 반환되어 토큰을 낭비했습니다.
품질 개선이 필수적이었습니다. 4단계 품질 개선 프로세스 바로 이런 문제를 해결하기 위해 품질 개선 파이프라인이 등장했습니다.
품질 개선을 사용하면 정확도가 올라갑니다. 관련 없는 컨텍스트를 제거하여 LLM의 집중력을 높입니다.
또한 비용 절감이 가능합니다. 중복과 저품질 컨텍스트를 제거하여 토큰 사용량을 줄입니다.
무엇보다 사용자 만족도가 향상된다는 이점이 있습니다. 코드 분석: 1단계 오버샘플링 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 similarity_search_with_score로 k의 2배만큼 Child를 가져옵니다. 이것이 핵심입니다.
나중에 필터링할 것이므로 여유 있게 가져오는 것입니다. 점수도 함께 반환받아 품질을 판단할 수 있습니다.
2단계: 점수 필터링 min_score 임계값을 사용해 저품질 결과를 제거합니다. 유사도 점수가 0.7 미만인 Child는 관련도가 낮다고 판단하여 제외합니다.
이렇게 하면 명백히 관련 없는 결과가 Parent 선택에 영향을 주지 않습니다. 임계값은 도메인에 따라 조정할 수 있습니다.
기술 문서는 0.8, 일반 문서는 0.6처럼 다르게 설정할 수 있습니다. 3단계: 중복 제거 set()을 사용해 Parent ID 중복을 제거합니다.
여러 Child가 같은 Parent를 가리킬 수 있습니다. 같은 섹션의 다른 문단들이 모두 매칭되는 경우입니다.
중복을 제거하면 같은 Parent가 여러 번 전달되는 것을 방지합니다. 이것만으로도 토큰을 크게 절약할 수 있습니다.
4단계: 리랭킹 가장 중요한 단계입니다. Parent를 가져온 후 원래 질문과의 관련도를 다시 평가합니다.
Child 검색은 키워드 매칭에 의존하지만, Parent 리랭킹은 의미적 관련성을 평가합니다. LLM을 사용해 "이 섹션이 질문에 답하는 데 도움이 되는가?"를 판단합니다.
관련도가 높은 순으로 정렬하여 최상위 k개만 반환합니다. 실무 활용: A/B 테스팅 실제 현업에서는 어떻게 활용할까요?
예를 들어 고객 지원 시스템에서 A/B 테스팅을 한다고 가정해봅시다. 그룹 A는 기본 Parent-Child 검색을 사용하고, 그룹 B는 품질 개선 파이프라인을 사용합니다.
결과를 측정해보면 그룹 B의 답변 정확도가 15-20% 높게 나옵니다. 또한 불필요한 정보가 줄어들어 사용자가 답변을 이해하는 시간도 단축됩니다.
많은 엔터프라이즈 RAG 시스템에서 이런 품질 개선 로직을 필수로 사용하고 있습니다. 추가 최적화: 캐싱 더 나아가 성능을 개선할 수도 있습니다.
리랭킹은 LLM 호출이 필요하므로 비용이 듭니다. 자주 나오는 질문 패턴에 대해서는 리랭킹 결과를 캐싱할 수 있습니다.
"로그인 오류" 같은 질문은 매일 수십 번 들어옵니다. 첫 번째 리랭킹 결과를 Redis에 저장하고 재사용하면 비용과 지연시간을 줄일 수 있습니다.
주의사항: 과도한 필터링 하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 임계값을 너무 높게 설정하는 것입니다.
min_score를 0.9로 설정하면 관련 있는 결과까지 제외될 수 있습니다. 특히 도메인 특화 용어가 많은 문서에서는 유사도 점수가 원래 낮게 나옵니다.
따라서 실제 데이터로 임계값을 튜닝해야 합니다. 몇 가지 질문으로 테스트해보고 적절한 값을 찾으세요.
모니터링과 개선 다시 한지우 씨의 이야기로 돌아가 봅시다. 품질 개선 파이프라인을 적용한 후 사용자 만족도가 크게 올랐습니다.
"이제 답변이 정말 정확해요!" 송민석 아키텍트가 웃으며 말했습니다. "모니터링을 계속하면서 임계값을 조정해 나가세요." 컨텍스트 품질 개선을 제대로 이해하면 RAG 시스템의 완성도를 최고 수준으로 높일 수 있습니다.
여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 실전 팁
- 리랭킹은 비용이 드니 캐싱을 적극 활용하세요
- min_score는 실제 데이터로 A/B 테스팅하여 결정하세요
- 중복 제거만으로도 토큰 비용을 30% 이상 줄일 수 있습니다
5. Parent 크기 최적화
적절한 Parent 크기 찾기 시니어 개발자 최예린 씨는 Parent 청크 크기를 2000자로 설정했는데 LLM이 중요한 정보를 놓치는 경우가 있었습니다. 반대로 너무 크게 하면 토큰 제한을 초과했습니다.
리드 엔지니어 강태윤 씨가 조언했습니다. "문서 타입마다 최적 크기가 다릅니다.
실험을 통해 찾아야 해요."
핵심 개념을 3-4문장으로 Parent 크기 최적화는 문서 특성에 맞는 적절한 Parent 청크 크기를 찾는 것입니다. 마치 옷 사이즈를 체형에 맞게 고르듯, 문서 타입과 질문 패턴에 따라 크기를 조정합니다.
너무 크면 노이즈가 많고, 너무 작으면 정보가 부족합니다. 이것을 제대로 이해하면 LLM이 최고 성능을 낼 수 있는 컨텍스트를 제공할 수 있습니다.
다음 코드를 살펴봅시다.
from typing import Dict
class AdaptiveParentSizer:
def __init__(self):
# 문서 타입별 최적 크기 (실험으로 결정)
self.size_config: Dict[str, dict] = {
'api_doc': {
'parent_size': 1500,
'child_size': 300,
'overlap': 150
},
'tutorial': {
'parent_size': 2500,
'child_size': 500,
'overlap': 250
},
'faq': {
'parent_size': 800,
'child_size': 200,
'overlap': 100
},
'code': {
'parent_size': 1200,
'child_size': 400,
'overlap': 200
}
}
def get_splitter(self, doc_type: str):
config = self.size_config.get(doc_type, self.size_config['tutorial'])
parent_splitter = RecursiveCharacterTextSplitter(
chunk_size=config['parent_size'],
chunk_overlap=config['overlap']
)
child_splitter = RecursiveCharacterTextSplitter(
chunk_size=config['child_size'],
chunk_overlap=config['overlap'] // 2
)
return parent_splitter, child_splitter
이북처럼 술술 읽히는 설명 최예린 씨는 입사 2년 차로 RAG 시스템을 안정적으로 운영하고 있었습니다. 하지만 새로운 문제가 발견되었습니다.
API 문서를 검색할 때는 답변이 충분했는데, 튜토리얼을 검색할 때는 문맥이 부족하다는 피드백이 왔습니다. 모든 문서에 2000자 Parent를 사용했는데 뭐가 문제일까요?
리드 엔지니어 강태윤 씨가 데이터를 분석하며 말했습니다. "문서마다 특성이 다릅니다." 일률적 크기의 문제 그렇다면 왜 하나의 크기로는 부족한 걸까요?
쉽게 비유하자면, 이것은 마치 모든 사람에게 같은 사이즈 옷을 입히는 것과 같습니다. 키 큰 사람에게는 작고, 키 작은 사람에게는 큽니다.
API 문서는 짧고 간결합니다. 하나의 엔드포인트 설명이 500자면 충분합니다.
반면 튜토리얼은 단계별 설명이 이어지므로 3000자가 필요할 수 있습니다. 이처럼 문서 타입마다 정보 밀도와 구조가 다릅니다.
문서 타입별 특성 각 문서 타입은 어떤 특징을 가질까요? API 문서는 구조화되어 있고 정보가 밀집되어 있습니다.
엔드포인트, 파라미터, 응답 형식이 명확히 구분됩니다. 따라서 작은 Parent로도 충분합니다.
튜토리얼은 서사적입니다. "먼저 이것을 하고, 다음에 저것을 합니다"처럼 순차적 흐름이 중요합니다.
앞뒤 맥락이 필요하므로 큰 Parent가 적합합니다. FAQ는 질문-답변 쌍입니다.
각 항목이 독립적이므로 매우 작은 Parent로 충분합니다. 코드 예제는 함수나 클래스 단위입니다.
너무 크면 관련 없는 코드가 섞이고, 너무 작으면 컨텍스트를 이해할 수 없습니다. 실험을 통한 최적화 최적 크기는 어떻게 찾을 수 있을까요?
강태윤 씨는 A/B 테스팅을 제안했습니다. 같은 질문 세트로 여러 Parent 크기를 시험합니다.
API 문서는 1000자, 1500자, 2000자로 테스트했습니다. 각 크기별로 답변 정확도, LLM 응답 시간, 토큰 사용량을 측정합니다.
1500자가 정확도와 비용의 최적 균형점이었습니다. 튜토리얼도 같은 방식으로 테스트한 결과 2500자가 적합했습니다.
코드 분석: 설정 관리 위의 코드를 한 줄씩 살펴보겠습니다. 먼저 size_config 딕셔너리에 문서 타입별 설정을 저장합니다.
이것이 핵심입니다. 각 타입마다 parent_size, child_size, overlap을 정의합니다.
이 값들은 실험을 통해 결정된 최적값입니다. get_splitter 메서드는 문서 타입을 받아 적절한 splitter를 반환합니다.
문서 타입이 설정에 없으면 기본값으로 'tutorial' 설정을 사용합니다. Overlap 비율의 중요성 Overlap도 크기에 비례해야 할까요?
네, 그렇습니다. Parent가 2000자라면 200자 overlap이 적당하지만, Parent가 800자라면 100자면 충분합니다.
일반적으로 Parent 크기의 10-15%가 적절합니다. Child overlap은 Parent의 절반 정도로 설정합니다.
이렇게 하면 정보 손실을 막으면서도 중복을 최소화할 수 있습니다. 동적 크기 조정 더 나아가 동적으로 조정할 수도 있습니다.
같은 문서 타입 내에서도 섹션마다 길이가 다릅니다. "개요" 섹션은 짧지만 "상세 설명" 섹션은 깁니다.
각 섹션의 실제 길이를 분석하여 Parent 크기를 동적으로 조정할 수 있습니다. 섹션이 1000자보다 짧으면 전체를 Parent로 사용하고, 3000자보다 길면 2개의 Parent로 분할하는 식입니다.
실무 활용: 혼합 문서 실제 현업에서는 어떻게 활용할까요? 예를 들어 제품 문서 사이트를 개발한다고 가정해봅시다.
하나의 사이트에 API 문서, 튜토리얼, FAQ가 모두 있습니다. 문서 메타데이터에 타입을 저장하고, 인덱싱 시 타입별로 다른 splitter를 사용합니다.
사용자가 검색할 때는 모든 타입을 대상으로 하지만, 각 타입은 최적화된 크기로 청킹되어 있습니다. 이렇게 하면 문서 타입에 관계없이 일관된 품질을 제공할 수 있습니다.
대형 SaaS 플랫폼에서 이런 패턴을 사용하고 있습니다. 모니터링 지표 어떤 지표로 크기 적정성을 판단할까요?
답변 완성도를 측정합니다. 사용자가 추가 질문 없이 만족했는지 확인합니다.
토큰 사용량도 중요합니다. 크기를 늘렸는데 정확도 개선이 미미하다면 비효율적입니다.
LLM 응답 시간도 모니터링합니다. Parent가 너무 크면 처리 시간이 길어집니다.
이 세 가지 지표의 균형점을 찾아야 합니다. 주의사항: 토큰 제한 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 LLM의 컨텍스트 윈도우를 고려하지 않는 것입니다. Parent를 5000자로 설정하고 10개를 반환하면 50,000자가 됩니다.
GPT-4의 경우 문제없지만 더 작은 모델은 제한을 초과할 수 있습니다. 따라서 Parent 크기 × 반환 개수가 모델 제한 내에 있는지 확인해야 합니다.
지속적 개선 다시 최예린 씨의 이야기로 돌아가 봅시다. 타입별 크기 최적화를 적용한 후 전반적인 답변 품질이 올랐습니다.
특히 튜토리얼 검색 만족도가 40% 개선되었습니다. 강태윤 리드가 말했습니다.
"매달 메트릭을 리뷰하면서 크기를 조정해 나가세요." Parent 크기 최적화를 제대로 이해하면 문서 특성에 맞는 최고의 검색 경험을 제공할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 실전 팁
- 문서 타입별로 50개 샘플로 A/B 테스팅하세요
- Parent 크기는 일반적으로 1000-3000자 사이가 적합합니다
- Overlap은 Parent 크기의 10-15%로 설정하세요
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
ReAct 패턴 마스터 완벽 가이드
LLM이 생각하고 행동하는 ReAct 패턴을 처음부터 끝까지 배웁니다. Thought-Action-Observation 루프로 똑똑한 에이전트를 만들고, 실전 예제로 웹 검색과 계산을 결합한 강력한 AI 시스템을 구축합니다.
AI 에이전트의 모든 것 - 개념부터 실습까지
AI 에이전트란 무엇일까요? 단순한 LLM 호출과 어떻게 다를까요? 초급 개발자를 위해 에이전트의 핵심 개념부터 실제 구현까지 이북처럼 술술 읽히는 스타일로 설명합니다.
프로덕션 RAG 시스템 완벽 가이드
검색 증강 생성(RAG) 시스템을 실제 서비스로 배포하기 위한 확장성, 비용 최적화, 모니터링 전략을 다룹니다. AWS/GCP 배포 실습과 대시보드 구축까지 프로덕션 환경의 모든 것을 담았습니다.
RAG 캐싱 전략 완벽 가이드
RAG 시스템의 성능을 획기적으로 개선하는 캐싱 전략을 배웁니다. 쿼리 캐싱부터 임베딩 캐싱, Redis 통합까지 실무에서 바로 적용할 수 있는 최적화 기법을 다룹니다.
실시간으로 답변하는 RAG 시스템 만들기
사용자가 질문하면 즉시 답변이 스트리밍되는 RAG 시스템을 구축하는 방법을 배웁니다. 실시간 응답 생성부터 청크별 스트리밍, 사용자 경험 최적화까지 실무에서 바로 적용할 수 있는 완전한 가이드입니다.