실제 환경에서의 RAG: 청킹 실험을 2주간 진행한 후 내가 배운 것
Source: Dev.to
위의 링크에 있는 전체 글을 번역하려면, 해당 글의 텍스트를 제공해 주시겠어요?
텍스트를 주시면 원본 형식과 마크다운을 그대로 유지하면서 한국어로 번역해 드리겠습니다.
약 3개월 전, 제가 정말 자랑스러워했던 RAG 파이프라인을 배포했습니다
내부 문서에 대한 의미 검색, OpenAI 임베딩, 백엔드에 Pinecone. 현대적인 느낌이었습니다. 그런데 팀원 중 한 명이 “우리의 육아 휴직 정책이 뭐야?” 라고 물었고, 파이프라인은 완전히 조작된 3‑문단 답변을 자신 있게 반환했습니다—오래된 HR 문서, PTO에 관한 Confluence 페이지, 그리고 제가 추측하기로는 분위기(vibes)까지 뒤섞어 만든 것이었습니다.
그것이 저에게 큰 경각심을 주었습니다. 임베딩 모델은 고장 난 것이 아니었습니다. 벡터 DB도 고장 난 것이 아니었습니다. 검색 단계—튜토리얼에서 복사‑붙여넣기하고 넘어갔던 부분—가 문제였습니다. 저는 다음 2주를 집착적으로 고치는 데 보냈고, 그 결과는 다음과 같습니다.
당신의 청크 크기가 아마도 잘못됐을 겁니다 (제 경우도 마찬가지)
대부분의 튜토리얼은 512 토큰으로 청크를 나누고 끝이라고 말합니다. 저도 그렇게 했습니다. 짧은 사실 조회에는 괜찮았지만, 질문이 긴 문서 전체에 걸쳐 정보를 종합해야 할 때—예를 들어, 세 섹션에 걸쳐 교차 참조가 있는 정책—무너졌습니다.
| 청크 크기 | 장점 | 단점 |
|---|---|---|
| 작은 청크 (≈512 토큰) | 높은 검색 정밀도 (관련 문장이 실제로 top‑k 결과에 포함) | 컨텍스트가 잘려 답변 품질이 저하 |
| 큰 청크 (≈1,200 토큰) | 컨텍스트 유지, 종합에 유용 | 정밀도 낮음; 관련 정보가 큰 청크 안에 묻혀 있을 수 있음 |
저는 우리 문서 코퍼스(≈800 문서, Markdown과 PDF 혼합)에서 통제된 실험을 진행했습니다. 세 가지 전략:
- 고정 크기 청크 – 512 토큰, 50 토큰 오버랩. 베이스라인. 구현이 쉬우며 성능이 예측 가능. 제가 시작한 지점이기도 합니다.
- 의미 청크 – 문장 경계에서 나눈 뒤, 연속 문장 임베딩 간 코사인 거리로 의미 전이가 감지될 때까지 문장을 그룹화.
langchain의SemanticChunker(LangChain v0.2.x)를 사용했습니다. 문서 구조에 따라 80~600 토큰 사이의 청크가 생성되었습니다. - 계층형 / 부모 문서 검색 – 검색을 위해 작은 청크를 저장하지만, 청크가 검색되면 해당 청크의 더 큰 부모 청크를 LLM에 반환합니다. 이것이 실제로 성능을 끌어올린 방법입니다.
from langchain.retrievers import ParentDocumentRetriever
from langchain.storage import InMemoryStore
from langchain_text_splitters import RecursiveCharacterTextSplitter
# Child chunks — what gets embedded and searched
child_splitter = RecursiveCharacterTextSplitter(chunk_size=200)
# Parent chunks — what the LLM actually sees
parent_splitter = RecursiveCharacterTextSplitter(chunk_size=800)
store = InMemoryStore()
retriever = ParentDocumentRetriever(
vectorstore=vectorstore,
docstore=store,
child_splitter=child_splitter,
parent_splitter=parent_splitter,
)
# Index child chunks but store parent chunks
retriever.add_documents(docs)
# At query time, retrieval happens on child embeddings,
# but the returned context is the full parent chunk
results = retriever.invoke("what is the parental leave policy?")
결과 (수동 라벨링한 QA 쌍 100개)
| 전략 | 정확도 |
|---|---|
| 고정‑크기 (512 토큰) | 61 % |
| 의미 청크 | 68 % |
| 부모‑문서 검색 | 79 % |
핵심 요점: 먼저 고정‑크기 베이스라인(≈512 토큰)으로 시작하세요. 그런 다음 의미 청크에 투자하기 전에 부모‑문서 검색을 시도해 보세요. 의미 청크는 대부분 실제 코퍼스에서 기대만큼의 이득을 주지 못했습니다.
벡터 데이터베이스를 선택하면서 정신을 잃지 않는 방법
저는 네 가지 옵션을 테스트했습니다: Pinecone, Qdrant, Weaviate, 그리고 pgvector. 제 설정은 약 30명의 팀을 위한 단일 노드 배포였으며, 백만 명 사용자용 제품은 아니었으니 성능 수치는 상황에 맞게 해석하시기 바랍니다.
| DB | 장점 | 단점 |
|---|---|---|
| Pinecone | 완전 관리형, 깔끔한 Python SDK, 인프라 제로‑ops | 메타데이터 필터링에 함정이 존재 |
| Qdrant | 오픈소스, 실시간 업데이트 지원, 풍부한 필터링 | 복잡한 클러스터링 설정이 필요할 수 있음 |
| Weaviate | 그래프 기반 검색, 모듈식 확장성, 내장 LLM 통합 | 초기 설정이 다소 복잡 |
| pgvector | PostgreSQL에 바로 임베딩 저장, 기존 DB 인프라 활용 가능 | 대규모 벡터 검색에선 성능 제한 가능 |
(이하 내용은 다음 파트에 이어집니다)
l 청크가 더 높은 순위에 매겨졌고 “너무 유사함”으로 판단되었습니다. 실제로 리콜 수치가 감소했습니다. 결국 MMR을 비활성화하고 대신 청크 전략을 통해 중복성을 해결했습니다. 특정 데이터셋에서 테스트해 보기 전까지는 무료라고 가정하지 마세요.
- 코퍼스에 키워드가 많이 포함되어 있다면 하이브리드 검색을 구현하세요.
- 그 후에도 검색 품질이 만족스럽지 않다면 재랭커를 추가하세요 — 이는 종종 가장 높은 ROI 개선을 제공합니다.
- MMR에 대해 회의적인 태도를 유지하세요.
Evaluating Whether Any of This Actually Helps
Nobody talks about this enough: you need an eval harness before you start tuning, or you’re flying blind. I built mine with ragas (v0.1.x) and ~100 manually curated QA pairs from our actual documentation.
Four metrics I tracked
- Faithfulness — does the answer stick to what’s in the retrieved context?
- Answer relevancy — is the answer actually responsive to the question?
- Context precision — are the retrieved chunks relevant?
- Context recall — is the relevant information making it into the retrieved chunks at all?
My initial pipeline had fine faithfulness (the LLM wasn’t hallucinating beyond what was in the retrieved docs) but terrible context recall — I was only surfacing the relevant chunk ~60 % of the time. That’s why the parental‑leave answer was wrong: the relevant doc wasn’t making it into the top‑5 results. Once I identified that, the fix was obvious — better chunking plus hybrid search to catch “parental leave” as a keyword match.
Without the eval setup I would have kept tweaking the prompt. That’s the trap.
Source: …
오늘 실제로 만들고 싶은 것
- 팀이 이미 Postgres를 사용하고 있다면
pgvector부터 시작하세요. 인프라 의존성을 없애고 대부분의 내부 도구에 충분히 강력합니다. - 규모 문제가 발생하거나 하이브리드 검색이 절실히 필요할 때 Qdrant로 마이그레이션하세요; 데이터 마이그레이션이 그리 고통스럽지는 않습니다.
임베딩
- OpenAI의
text-embedding-3-large(3072 차원) 또는 - 견고한 오픈‑소스 옵션인
nomic-embed-text.
대부분의 RAG 사용 사례에서 최신 임베딩 모델이 text-embedding-3-large에 비해 비용 프리미엄을 정당화한다고는 생각되지 않지만, 가장 최근 릴리스를 직접 벤치마크해 보지는 않았습니다.
검색 전략
- 시맨틱 청킹보다 부모‑문서 검색: 구현이 더 간단하고 디버깅이 쉬우며, 제 테스트에서는 성능이 더 좋았습니다.
- 벡터 DB 선택을 직접 제어한다면 처음부터 하이브리드 검색을 적용하세요. BM25가 사라진 것은 아닙니다.
재정렬
- 컨텍스트를 LLM에 전달하기 전에 크로스‑인코더 재정렬 한 번을 수행합니다. 지연 시간 비용이 그만큼의 가치를 제공합니다.
평가
- 무엇보다 먼저 평가용 하네스를 구축하세요. 50개의 QA 쌍만 있어도 변경 사항이 도움이 되는지 해를 끼치는지 판단할 수 있습니다. 이를 없으면 감에 의존해 반복하게 되고, 저는 그 때문에 두 주를 힘들게 보냈습니다.