대부분의 엔지니어링 팀은 RAG(Retrieval-Augmented Generation)를 구현할 때 똑같은 벽에 부딪힙니다. 이들은 튜토리얼 방식의 플레이북을 따릅니다. 문서를 512 또는 1024 토큰의 고정된 청크로 나누고, 단일 임베딩 모델을 거쳐, 단순한 top-k 조회로 벡터 데이터베이스를 호출하는 방식입니다. 슬라이드 발표 자료에서는 이 방식이 견고해 보일지 모르지만, 실제 운영 환경에서는 무너집니다.

고정된 청크는 콘텐츠를 고려하지 않습니다. 법률 계약서를 문장 중간에서 잘라버려 책임 조항이 서로 관련 없는 두 텍스트 조각으로 흩어지게 만들기도 합니다. 혹은 API 엔드포인트 설명을 너무 큰 청크 하나에 몰아넣어, 사용자가 질문한 특정 파라미터가 노이즈 속에 파묻히게 만들기도 합니다. 그리고 검색이 느려지면, 매 밀리초의 지연 시간이 사용자 경험에 직접적인 악영향을 미칩니다. 저희는 이를 뼈아프게 배웠습니다. 그래서 검색 레이어를 완전히 허물고 다시 구축했습니다. 그 결과, Recall at ten(R@10)은 78%에서 95%로 뛰었습니다. 지연 시간은 늘어나지 않았습니다. 오히려 급격히 감소했습니다.

복사해서 붙여넣기 식 RAG의 문제점

표준 RAG 스택은 일종의 기본 설정이 되었습니다. 작은 청크, 단일 임베딩 모델, 벡터 검색, 이게 전부입니다. 이런 방식은 데모에서는 통합니다. 데모는 깔끔한 질문과 정돈된 문서를 사용하기 때문입니다. 하지만 실제 운영 데이터는 결코 깔끔하지 않습니다.

법률 문서는 계층적 구조를 가집니다. 섹션 안에 서브섹션이 있고, 서브섹션 안에 조항이 있습니다. 무딘 토큰 카운터로 이를 잘라버리면 모델이 추론하는 데 필요한 관계 자체가 파괴됩니다. API 문서도 구조가 있지만 성격이 다릅니다. 함수 시그니처, 파라미터, 반환 값, 그리고 사용 예시는 하나의 논리적 단위입니다. 이를 고정된 토큰 창에 억지로 밀어 넣으면 예시가 잘리거나 관련 없는 함수들로 청크가 채워지게 됩니다. 고객 지원 티켓은 무질서하고 대화 중심적이며 갑작스러운 주제 전환이 빈번합니다. 위키는 방대하고 상호 참조가 많습니다. 하나의 청킹 전략으로 이 모든 것을 해결할 수는 없지만, 팀들은 관행적으로 그렇게 하고 있습니다. 저희는 그것이 불가능하다는 것을 인정하고 더 이상 그런 척하지 않기로 했습니다.

전략적 청킹: 자료에 맞는 방법론 적용

저희는 콘텐츠 인식(content-aware) 청킹으로 전환했습니다. 법률 문서의 경우, 문서 계층 구조를 존중하는 재귀적 청킹(recursive chunking)을 사용합니다. 이를 통해 조항을 온전하게 유지하고 섹션 간의 부모-자식 관계를 보존합니다. API 문서의 경우, 각 함수나 엔드포인트를 경계로 취급하는 함수 인식 청킹(function-aware chunking)을 구축했습니다. 파라미터 설명이 길어지면 토큰 제한이 아니라 해당 함수를 중심으로 청크가 확장됩니다. 고객 지원 티켓에는 자연스러운 주제 경계를 감지하는 의미론적 청킹(semantic chunking)을 사용합니다. 고객이 결제 불만에서 기술적 버그로 갑자기 화제를 전환하면, 그 전환점에서 분할이 일어납니다. 위키나 비정형 지식 베이스의 경우, 경량 LLM이 텍스트를 평가하여 의미 있는 경계가 어디인지 결정하는 에이전트 방식 청킹(agentic chunking)을 사용합니다. 문자 단위 분할(character split)보다 설정하는 데 시간은 더 걸리지만, 이는 제대로 작동하는 검색과 추측만 하는 검색의 차이를 만듭니다.

하이브리드 검색: 벡터 검색만으로는 부족한 이유

벡터 검색은 의미를 이해하지만, 정확한 일치(exact match)를 놓칠 수 있습니다. 사용자가 ERR_CONNECTION_RESET_0x5F3와 같은 에러 코드를 붙여넣으면, 의미론적 유사성은 단순히 네트워크 오류를 논하는 문단보다 해당 코드를 낮게 평가할 수 있습니다. 반면 BM25는 정확한 문자열은 찾아내지만 개념적 연관성은 놓칩니다. 두 가지 모두가 필요합니다.

저희는 벡터 검색과 BM25를 병렬로 실행합니다. 그런 다음 상호 순위 결합(Reciprocal Rank Fusion, RRF)을 통해 결과를 결합합니다. RRF는 두 검색 공간의 점수를 동일한 척도로 강제하지 않으면서 정규화합니다. 결합 후에는 상위 후보군을 크로스 인코더 리랭커(cross-encoder reranker)로 보냅니다. 약간의 지연 시간이 추가되지만, 정밀도 향상은 상당합니다. 리랭커는 쿼리와 각 후보군을 함께 읽어 초기 임베딩의 코사인 유사도보다 훨씬 정확한 관련성 점수를 할당합니다. 실제로 이 조합은 순수 벡터 검색이 놓치는 정확한 에러 코드를 잡아내는 동시에, 키워드 검색이 무시할 수 있는 개념적으로 연관된 문제 해결 단계까지 찾아냅니다.

쿼리 확장: 인덱스에 도달하기 전 사용자 입력 수정하기

사용자는 완벽한 검색 쿼리를 작성하지 않습니다. "왜 지난 배포가 실패했나요? 그리고 어떻게 롤백하나요?"와 같이 두 개의 서로 다른 지식 영역을 찾아 연결해야 하는 멀티홉(multi-hop) 질문을 던지기도 합니다. 또는 인덱스와 잘 매칭되지 않는 모호한 질문을 하기도 합니다.

우리는 검색 전 쿼리를 변환합니다. 멀티홉 질문은 하위 질문으로 분해됩니다. 모호한 의도는 여러 개의 구체적인 검색 쿼리로 확장됩니다. 하나의 사용자 쿼리를 다섯 개의 서로 다른 검색 쿼리로 확장하면 재현율(recall)을 78%에서 96%로 높일 수 있다는 사실을 발견했습니다. 이는 LLM에 더 강한 프롬프트를 주는 문제가 아닙니다. 검색 시스템이 적절한 컨텍스트를 찾을 수 있는 기회를 더 많이 제공하는 것에 관한 것입니다. 생성된 각 쿼리는 서로 다른 관점이나 용어를 포착하며, 병합된 결과는 전체적인 그림을 그려냅니다.

베이지안 최적화: 추측은 그만하세요

여러 청킹 전략, 하이브리드 검색, 쿼리 확장을 갖추고 나면 새로운 문제에 직면하게 됩니다. 조절해야 할 요소(knobs)가 너무 많기 때문입니다. 청크 크기, 오버랩 비율, 벡터 가중치 대 BM25 가중치, 리랭킹 임계값, top-k 값 등이 모두 비선형적인 방식으로 상호작용합니다. 수동 튜닝은 결국 추측 게임이 되어버립니다.

우리는 추측을 멈췄습니다. 우리는 ...를