본문 바로가기
AI.IT

RAG 파이프라인 만들다 3번 갈아엎었다, 실패 패턴이 있었다

by bamsik 2026. 5. 9.
반응형

RAG 파이프라인 구축을 처음 시작했을 때, 임베딩 모델을 잘 고르는 게 핵심이라고 생각했다. 모델이 좋으면 검색도 좋아진다는 논리였다. 결론부터 말하면 완전히 틀렸다. 구조를 세 번 갈아엎고 나서야 진짜 문제가 청크 전략에 있었다는 걸 알았다.

처음엔 임베딩 모델만 좋으면 된다고 생각했다

첫 번째 RAG 시스템은 내부 문서 검색 툴이었다. PDF 30개 정도를 인덱싱해서 질문에 답변을 뽑아내는 간단한 구조였다. LangChain 튜토리얼 따라가면 하루 만에 끝났다. 문서를 불러오고, 512 토큰 단위로 쪼개고, 임베딩 걸고, ChromaDB에 넣었다. 그런데 막상 써보니 답변이 영 엉망이었다.

"3분기 매출은?" 이라고 물었더니 엉뚱한 분기 데이터를 가져왔다. "계약서 해지 조건이 뭐야?" 물었더니 계약서의 다른 항목을 읽어왔다. 처음엔 모델 문제라고 생각해서 임베딩 모델을 바꿔봤다. 별 차이 없었다.

알고 보니 문제는 청크였다. 512 토큰짜리 덩어리가 문장 중간에서 잘리면서 의미 있는 단위가 분리됐다. 표 하나가 세 개 청크로 나뉘어져 각각 저장됐고, 검색하면 그 중 하나만 가져왔다. 임베딩 모델은 잘못이 없었다. 처음부터 청크 설계를 먼저 봤어야 했다.

청킹 전략이 전부다 — 고정 크기, 재귀, 시맨틱의 차이

청킹 전략이 임베딩 모델보다 검색 품질에 훨씬 큰 영향을 준다. 프로덕션 RAG 분석 자료에 따르면, 임베딩 모델 간 성능 차이는 2~5% 수준인 반면 청킹 전략 차이는 15~30%에 달한다. 모델 선택에 시간 쏟기 전에 청크 설계를 먼저 봐야 한다.

고정 크기 청킹의 함정

가장 단순한 방법이다. 토큰 수를 기준으로 일정 크기마다 자른다. 구현이 쉽다는 장점이 있지만, 문장 중간이나 단락 사이를 아무 데나 잘라버리는 게 치명적이다. "이 계약은 다음 조건 중 하나가 충족될 경우"라는 문장이 두 청크로 나뉘면, 검색했을 때 조건 목록 없이 해당 문장만 가져오는 경우가 생긴다.

10~20% 토큰 오버랩을 주면 경계 손실 문제의 60~70%는 해결된다. 하지만 근본적인 해결책은 아니다. 오버랩을 키울수록 저장 용량과 검색 비용도 같이 늘어난다.

재귀 청킹이 기본값인 이유

LangChain의 RecursiveCharacterTextSplitter가 이 방식이다. 단락 단위로 먼저 자르고, 단락이 너무 크면 문장 단위로, 그래도 크면 문자 단위로 내려가며 자른다. 자연스러운 경계를 최대한 유지하면서 크기 제한도 맞춘다.

구조화된 문서(코드 문서, API 레퍼런스, 기술 매뉴얼)에서는 고정 크기 대비 검색 재현율이 5~12% 높다. 별도 의존성 없이 쓸 수 있고 혼합 형식 문서도 잘 처리한다. 어디서 시작해야 할지 모르겠으면 이걸 기본으로 쓰면 된다.

시맨틱 청킹, 언제 쓰면 좋은가

규칙 기반이 아니라 임베딩 유사도로 경계를 찾는다. 인접한 문장 사이의 의미 유사도가 임계값 아래로 떨어지는 지점을 청크 경계로 삼는다. 서술형 문서(블로그, 논문, 고객 지원 문서)에서는 고정 크기 대비 정확도가 10~20% 높다.

단점은 속도와 비용이다. 청킹 시 임베딩 모델을 계속 호출해야 하므로 문서가 많으면 인덱싱 비용이 올라간다. 자주 바뀌지 않는 문서 라이브러리에 쓰는 게 낫고, 실시간 업데이트가 잦은 문서에는 부담이 된다. 계층형 청킹(작은 청크로 검색하고, 부모 청크를 언어 모델에 전달)과 조합하면 복잡한 멀티홉 질문에서 15~25% 성능이 올라간다.

ChromaDB에서 pgvector로 갈아탄 이유

ChromaDB는 시작하기 정말 좋다. 설치 5분, 코드 10줄이면 로컬에서 벡터 검색이 돌아간다. LangChain과 연동도 자연스럽다. 첫 번째 RAG 프로젝트에 ChromaDB 쓴 건 잘한 선택이었다.

문제는 서비스 규모가 커지면서 생겼다. 백업 정책 없이 디스크 파일 형태로 관리하다 보니 운영이 불안했다. 트랜잭션 지원이 약해서 동시 쓰기가 많아지면 꼬이는 경우가 있었다. 결정적으로, 기존 PostgreSQL 스택과 따로 운영하는 게 점점 번거로워졌다.

pgvector는 PostgreSQL 확장이라 기존 인프라에 그냥 붙인다. CREATE EXTENSION vector; 한 줄 추가하고 벡터 컬럼 하나 만들면 끝이다. 백업, 복제, 모니터링 모두 기존 PostgreSQL 방식 그대로 쓸 수 있다. 마이그레이션도 생각보다 어렵지 않았다. LangChain 벡터 스토어 인터페이스가 추상화되어 있어서 연결 설정 교체 수준에서 끝났다.

  • ChromaDB: 로컬 개발, PoC, 프로토타이핑에 적합
  • pgvector: 기존 PostgreSQL 사용 중인 팀, 운영 안정성이 필요한 서비스에 적합

단, 초기 인덱스 설정은 신경 써야 한다. 기본 설정으로 두면 벡터 수가 늘어날수록 검색이 느려진다. HNSW 인덱스를 적용해야 쿼리 속도가 유지된다. 이 부분에서 한 번 더 삽질했다.

하이브리드 검색 도입 후 달라진 것

벡터 검색에는 잘 알려진 약점이 있다. 정확한 키워드 매칭에 약하다. "SOC 2 Type II 요건"을 검색하면, 벡터 검색은 '보안 인증' 관련 문서를 의미론적으로 비슷하다고 판단해서 가져오는데, 정작 "SOC 2 Type II"라는 표현이 딱 들어있는 청크를 놓치는 경우가 있다.

BM25 키워드 검색은 반대다. 정확한 단어 매칭에는 강하지만 의미 기반 검색에는 약하다. 두 가지를 합치면 서로의 약점을 보완한다. 벡터 검색과 BM25 결과를 각각 뽑아서 RRF(Reciprocal Rank Fusion) 알고리즘으로 합치는 방식이 가장 무난하다. LangChain의 EnsembleRetriever를 쓰면 비교적 쉽게 붙일 수 있다.

솔직히 하이브리드 검색은 도입하기 귀찮다. 인덱스 두 개 관리해야 하고, 가중치 조정도 시행착오가 필요하다. 근데 정확한 고유명사나 코드 키워드가 검색 대상에 많다면 효과가 확실하다. 내 경우는 계약서나 기술 매뉴얼 검색에서 체감 차이가 컸다. 벡터 0.7, BM25 0.3 비율이 가장 무난한 시작점이었다.

세 번 갈아엎고 나서 지금 쓰는 구조는 재귀 청킹(512 토큰, 10% 오버랩), pgvector, 하이브리드 검색이다. 처음부터 이 구조로 갔으면 시간 많이 아꼈을 텐데, 실패하면서 배운 것도 있으니 아깝지는 않다. RAG 시작한다면 임베딩 모델 고르는 것보다 청크 설계에 먼저 시간을 써야 한다.


📎 참고 자료


📌 함께 보면 좋은 글

반응형