딥 RAG: Chunking 전략, Vector Databases 및 최적화
Source: Dev.to
위에 제공된 링크에 있는 전체 텍스트를 번역하려면, 해당 내용을 복사해서 여기 채팅에 붙여 주세요. 그러면 원본 서식과 마크다운을 유지하면서 한국어로 번역해 드리겠습니다.
몇 달 전, 한 고객이 40 000개의 법률 문서—계약서, 비밀 유지 계약서, 서비스 약관—에 대한 검색 시스템을 구축해 달라고 요청했습니다.
첫 번째 버전은 재앙이었습니다. 사용자들은 전혀 합리적인 질문을 했지만 시스템은 요청과 전혀 관련 없는 텍스트 조각을 반환했습니다. 한 변호사가 문자 그대로 이렇게 썼습니다:
“이건 Ctrl+F보다 더 나빠요.”
그 말이 맞았습니다.
문제는 모델이 아니었습니다. 모델 이전의 모든 것이었습니다: 문서를 어떻게 분할했는지, 어떤 벡터 데이터베이스를 사용했는지, 정보를 어떻게 검색했는지. 저는 다음 2주 동안 전체 파이프라인을 해체하고 신중하게 재구성했습니다. 여기서 공유하는 내용은 그 과정의 결과물이며—제가 저지른 실수와 여러분이 피할 수 있는 실수를 포함합니다.
왜 512 토큰 청킹이 함정인가
RAG를 시작했을 때, 모두가 하는 대로 문서를 고정된 크기의 청크로 나눴습니다. 보통 512 토큰에 오버랩 50‑100 토큰을 적용하죠. 거의 모든 튜토리얼에서 기본 설정으로 사용됩니다. 그리고 어느 정도는 동작합니다… 간단한 경우엔 말이죠.
문서에 실제 구조가 있을 때 문제가 발생합니다. 예를 들어 법률 계약서는 조항으로 구성되며, 하나의 조항은 세 단락 정도 될 수 있습니다. 고정 크기 청킹을 하면 조항을 중간에 잘라버릴 수 있고, 이제 두 개의 조각은 각각 완전한 의미를 갖지 못합니다. 시스템이 그 조각 중 하나를 검색해도 모델은 불완전한 정보를 가지고 작업하게 됩니다.
제가 이해하는 데 시간이 걸린 점은 청킹이 소프트웨어 엔지니어링 문제—가 아니라 의미론적 문제라는 것입니다. 올바른 질문은 “몇 토큰인가?”가 아니라 “단독으로 답변이 될 수 있는 정보 단위는 무엇인가?”가 됩니다.
법률 문서의 경우, 정답은 전체 조항이었습니다.
소스 코드의 경우, 함수가 됩니다.
기술 기사에서는 단락이나 하위 섹션이 적절합니다.
Source: …
실제로 시험해 본 세 가지 청킹 전략
1. 재귀 청킹
고정 크기 방식에서 처음으로 도약한 방법이며 즉각적인 개선을 가져왔습니다. LangChain은 RecursiveCharacterTextSplitter를 제공하는데, 이는 먼저 단락으로, 다음은 문장으로, 마지막으로 단어 단위로 나누려고 시도합니다. 산문 텍스트에는 잘 작동하지만 의미론을 고려하지는 못합니다.
2. 의미 청킹
여기서부터가 흥미로워졌습니다. 아이디어는: 임베딩을 사용해 연속 문장 간 유사도를 측정하고, 유사도가 일정 임계값 이하로 떨어질 때 구분하는 것입니다. 이렇게 하면 주제 전환을 자연스럽게 포착할 수 있습니다.
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings
# LangChain 0.3의 SemanticChunker — langchain-experimental 필요
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
chunker = SemanticChunker(
embeddings,
breakpoint_threshold_type="percentile", # 또는 "standard_deviation"
breakpoint_threshold_amount=85, # 불일치도가 85번째 백분위수를 넘으면 구분
)
chunks = chunker.create_documents([texto_del_contrato])
# 테스트 결과: 청크는 더 크지만 의미적으로 더 일관됨
# 단점: 모든 임베딩을 먼저 계산해야 하므로 느림
생성된 청크는 평균 300‑800 토큰으로 더 크지만, 완전한 아이디어를 담고 있었습니다. 검색 성능이 눈에 띄게 향상되었으며—즉각적인 극적인 변화는 아니었지만 테스트 전반에 걸쳐 일관된 개선을 보였습니다.
3. 부모‑문서 검색
가장 놀라웠던 기술입니다. 아이디어는 작은 청크를 인덱싱하여 검색 정확도를 높이고, 매치가 발견되면 더 큰 “부모” 청크를 반환하는 것입니다. 두 가지 granularity를 동시에 활용하는 셈이죠.
ParentDocumentRetriever를 사용해 구현했으며, 전체 문서는 InMemoryStore(또는 프로덕션에서는 Redis) 에 저장했습니다. 결과는: 컨텍스트를 잃지 않으면서도 정확한 검색이 가능했습니다. 계층 구조를 가진 긴 문서에 가장 선호하는 설정입니다.
내가 저지른 실수: 인덱싱용 작은 청크는 충분히 작아야 합니다(50‑100 토큰). 너무 크면 임베딩이 특정 아이디어를 포착하지 못해 정확도 이점을 잃게 됩니다.
벡터 데이터베이스: 다섯 가지를 테스트해 본 솔직한 의견
| 데이터베이스 | 장점 | 단점 | 추천 사용 사례 |
|---|---|---|---|
| Chroma | 프로토타입에 최적; 빠른 설정; LangChain과의 좋은 통합. | 하이브리드 검색(벡터 + 키워드) 및 복잡한 메타데이터 필터에 제한적. | 프로토타입 및 개념 증명. |
| Pinecone | 관리형 서비스; 확장 용이. | v3 API로의 마이그레이션이 혼란스러웠으며; 문서 업데이트가 늦었고; 가격이 빠르게 상승; 실제 벤더 종속성 존재. | 예산이 유연하고 완전 관리가 필요한 프로젝트. |
| Weaviate | BM25 통합; 객체당 다중 벡터 지원. | 많은 경우에 필요 이상으로 복잡한 설정. | 고급 하이브리드 검색이 필요하고 설정에 시간을 투자할 수 있을 때. |
| pgvector | 비인기 의견: 많은 경우에 충분하고 운영이 훨씬 간단합니다. 이미 PostgreSQL을 사용 중이라면 pgvector를 추가하는 것은 매우 간단합니다. 검색 속도는 대규모 전용 솔루션보다 느리지만, 중간 규모 볼륨에서는 잘 작동합니다. | 이미 PostgreSQL 데이터베이스가 있고 가벼운 솔루션을 찾는 경우에 이상적. |
pgvector를 사용한 교차 인코더 재정렬 예시
# Paso 1: recuperar más candidatos de los que necesitamos
candidatos = retriever.get_relevant_documents(query, k=20)
# Paso 2: crear pares (query, documento) para el cross‑encoder
pares = [(query, doc.page_content) for doc in candidatos]
# Paso 3: reranking — devuelve scores, no índices
scores = reranker.predict(pares)
# Paso 4: ordenar por score y retornar top_k
candidatos_con_score = sorted(
zip(candidatos, scores),
key=lambda x: x[1],
reverse=True,
)
return [doc for doc, _ in candidatos_con_score[:top_k]]
법률 고객에게 결과를 보여줬을 때 차이가 명확했습니다: ‘대략적으로 관련된 답변’에서 ‘정확한 조항을 인용하는 답변’으로 전환되었습니다. 로컬 교차 인코더의 지연 오버헤드는 약 200‑400 ms이며, 법률 검색 애플리케이션에 충분히 허용 가능한 수준입니다.
하이브리드 검색 (벡터 + BM25)
매우 구체적인 법률 용어 — 조항명, 조문, 교차 참조 — 에는 벡터 검색만으로는 실패합니다. 임베딩은 일반적인 의미를 포착하는 경향이 있기 때문입니다. BM25는 드문 용어에 대한 정확한 일치에 더 적합합니다. 가중치를 조정할 수 있게 두 방식을 결합한 것이 평가 세트에서 정확도가 **70 %**에서 **88 %**로 차이가 나는 요인이었습니다. Qdrant에서는 이것을 sparse vectors 로 구현합니다.
오늘 제가 사용할 것
No voy a darte “depende de tu caso de uso” porque eso no ayuda a nadie. Voy a decirte lo que yo montaría si empezara un proyecto RAG mañana:
| 분야 | 추천 |
|---|---|
| Chunking | Parent‑document retrieval를 작은 청크(≈100 토큰)로 인덱싱하고 부모 청크(300‑500 토큰)를 컨텍스트로 회수합니다. 문서에 명확한 구조(섹션, 조항, 함수)가 있다면 눈으로 잘라내는 대신 명시적으로 추출하세요. |
| 벡터 데이터베이스 | 제어가 필요하고 고급 기능을 원하며 관리형 서비스에 비용을 지불하고 싶지 않다면 Qdrant를 사용하세요. 이미 Postgres를 사용 중이고 데이터 양이 관리 가능하다면 pgvector가 좋습니다. 클라이언트가 완전 관리형 클라우드 솔루션을 요구하지 않는 한 Pinecone은 피하는 것이 좋습니다. |
| 검색 | 항상 reranking을 수행합니다. 계산 비용은 품질 향상에 비해 거의 무시할 수준입니다. 예산이 허용한다면 Cohere Rerank를 사용하고, 그렇지 않다면 로컬 모델 ms-marco-MiniLM이 잘 작동합니다. 전문 용어가 많은 도메인(법률, 의료, 소스 코드)에는 BM25 하이브리드를 추가하세요. |
| 평가 | 지표 없이 파이프라인이 개선됐는지 알 수 없습니다. RAGAS는 수동 라벨링 없이 LLM 자체를 사용해 관련성과 faithfulness를 판단하는 합리적인 RAG 평가 라이브러리입니다. 완벽하지는 않으며—특정 도메인에서는 메트릭이 잘 확장되지 않을 수도 있지만—직관에만 의존하는 것보다 낫습니다. |
Ctrl + F보다 내 시스템이 더 못하다고 말한 변호사가 3주 후에 이렇게 썼다: “이제 10초 만에 한 시간 걸리던 것을 찾는다.”
그것은 마법이 아니었습니다 — 올바른 청킹, Qdrant, 그리고 22 MB 크기의 cross‑encoder가 보통 수준의 EC2 인스턴스에서 실행된 결과였습니다.