
RAG는 죽었다. 누가 RAG를 죽였나?
요약
기존 벡터 기반 RAG의 한계인 텍스트 파편화 문제를 지적하며, 문서의 구조적 맥락을 유지하는 PageIndex 방식의 필요성을 설명합니다. 단순 유사도 검색 대신 문서의 목차와 계층 구조를 활용한 트리 기반 검색의 이점을 강조합니다.
핵심 포인트
- 기존 벡터 RAG는 문서를 잘게 쪼개어 구조적 맥락을 상실하는 문제가 있음
- 단순 코사인 유사도 기반 검색은 복잡한 문서의 논리적 흐름을 놓치기 쉬움
- PageIndex는 문서의 장, 섹션 등 계층 구조를 트리 형태로 구축하여 해결
- 구조 기반 추론을 통해 문서의 의미 지도를 유지하며 정확한 정보 검색 가능
당신의 RAG 파이프라인은 또다시 잘못된 답변을 반환했습니다.
질문은 간단했고, 문서에는 답이 있었습니다. 상위 5개 청크(chunks)가 돌아오는 것을 지켜봤지만 그 어느 것도 정답을 담고 있지 않았습니다. 올바른 문단은 임베딩 모델이
가정 4: 문서는 텍스트의 주머니(bags of text)이다.
그렇지 않습니다. 10-K(연례 보고서)에는 구조가 있습니다. 매뉴얼에는 장(chapters)이 있습니다. 계약서에는 다른 조항을 참조하는 조항(clauses)들이 있습니다. 벡터 RAG(Vector RAG)는 수집(ingest) 과정에서 이 모든 것을 버려버리고, 이후 검색(retrieval)이 길을 찾지 못할 때 놀라는 척을 합니다.
지난 2년 동안 제가 디버깅한 모든 RAG 실패 사례는 이 네 가지 중 하나로 거슬러 올라갑니다. 더 큰 임베딩 모델(embedding models)도 이를 해결하지 못합니다. 리랭킹(Reranking) 논문들도 이를 덮어버릴 뿐입니다. 하이브리드 BM25(Hybrid BM25)는 단지 이를 미룰 뿐입니다. 문제는 검색기(retriever)가 아니라, 전제(premise)입니다.
인간은 코사인 유사도(cosine similarity)로 200페이지짜리 문서를 읽지 않습니다. 목차를 펼칩니다.
실제로 정답을 찾는 방법
잠시 데이터베이스는 잊으세요. 400페이지짜리 요리책이 있고, 당신이 로스트 치킨 레시피를 찾고 싶다고 상상해 보십시오.
당신은 _치킨_과 가장 "유사한" 구절을 찾기 위해 400페이지 전체를 읽지 않습니다. 목차를 펼쳐서 **가금류(Poultry)**를 찾고, 해당 장으로 넘어가서, 손가락으로 **로스트 치킨(Roast Chicken)**을 따라 내려간 뒤, 재료, 오븐 온도, 조리 시간까지 순서대로 포함된 레시피 전체를 읽습니다.
[
이제 벡터 RAG 방식으로 하는 것을 상상해 보십시오. 모든 페이지를 찢어내어 통 안에 섞어 넣은 다음, 질문과 단어가 가장 비슷해 보이는 낱개 페이지 5장을 뽑아내는 것입니다. 재료 목록은 잡을 수 있겠지만, 다음 페이지에 있는 오븐 온도는 놓칠 수도 있습니다. 실수로 치킨 스톡(stock) 레시피를 잡을 수도 있습니다. 그리고 어떤 페이지가 어느 장에서 왔는지 전혀 알 수 없게 됩니다.
그것이 차이점입니다. 인간은 **구조에 기반한 추론(reasoning over structure)**을 사용하지만, 벡터 RAG는 **잘게 쪼개진 텍스트에 대한 유사도(similarity over shredded text)**를 사용합니다. 목차는 장식이 아닙니다. 그것은 저자가 이미 당신을 위해 그려놓은 의미의 지도이며, 청킹(chunking)은 그 지도를 쓰레기통에 던져버리는 행위입니다.
그렇다면 검색(retrieval)이 그 지도를 유지한다면 어떨까요?
킬러를 소개합니다
그것의 이름은 PageIndex입니다.
청크(chunks)를 임베딩(embedding)하는 대신, PageIndex는 문서의 구조 — 장(chapters), 섹션(sections), 하위 섹션(sub-sections) — 로부터 목차(table of contents)를 반영하는 **트리(tree)**를 구축합니다. (목차가 없다면? LLM이 페이지 텍스트로부터 트리를 생성합니다.) 이는 요리책을 갈기갈기 찢는 대신 온전하게 유지합니다. 각 노드(node)는 자신이 무엇을 포함하고 있는지 알고 있습니다. 벡터(vectors)도, 코사인 유사도(cosine similarity)도, k값도 필요 없습니다.
쿼리(query) 시점에 LLM은 사람이 목차를 살펴보는 방식과 동일하게 트리를 탐색(walk)합니다. 루트(root)에서 시작하여 어떤 브랜치(branch)에 정답이 있을 가능성이 가장 높은지 추론하고, 내려가고, 재평가하며, 더 깊이 파고들거나 다시 되돌아옵니다. 올바른 노드에 도달하면, 헤더(headers), 표(tables), 문맥(context)이 여전히 붙어 있는 온전한 섹션 전체를 읽습니다.
이 방식에서는 두 가지 이점이 자동으로 따라옵니다:
1. 문맥(Context)이 온전하게 유지됩니다. 정답은 그것을 구성하는 섹션과 함께 전달됩니다. 앞서 언급된 고립된 "...425°F에서 40분 동안 굽기"라는 문구는 이제 공중에 떠다니는 대신, _로스트 치킨(Roast Chicken)_이라는 헤딩(heading)을 함께 갖게 됩니다.
2. 검색(Retrieval) 과정이 설명 가능합니다. 루트에서 정답 노드까지의 정확한 경로인 추론 흔적(reasoning trace)을 얻을 수 있습니다. 결과가 틀렸을 때, 어느 지점에서 잘못되었는지 확인할 수 있습니다. 코사인 점수(cosine score)를 디버깅하려고 시도해 보세요.
실제로 작동할까요?
PageIndex는 Vectify AI (docs)에 의해 구축되어 오픈 소스로 공개되었습니다. 그리고 구조화된 문서(structured docs)에 대한 수치는 반박하기 어렵습니다. 실제 금융 공시 자료에 대한 질의응답(QA) 벤치마크인 FinanceBench에서, PageIndex를 기반으로 구축된 Vectify의 Mafin 2.5는 다음과 같이 보고했습니다:
동일한 문서에 대해 **98.7%**의 정확도를 기록했습니다. 이는 검색 기능을 사용한 ChatGPT-4o의 31%, Perplexity의 **45%**와 대조적입니다.
솔직히 말하자면, 이는 독립적인 재현 결과가 아닌 벤더(vendor)가 보고한 수치입니다. 하지만 패턴은 유효합니다. 구조화된 문서(structured documents)는 유사도 검색(similarity search)이 가장 큰 타격을 입는 지점이자, 트리(tree) 방식이 가장 크게 승리하는 지점입니다.
Vector RAG는 정답처럼 보이는 텍스트를 검색합니다. PageIndex는 정답이 어디에 존재하는지에 대해 추론합니다.
같은 질문, 같은 문서
이론은 이쯤 해두죠. 다시 그 400페이지짜리 요리책으로 돌아가서, 실제 질문 하나를 던져보겠습니다: "닭을 몇 도에서 얼마나 오랫동안 구워야 하나요?"
Vector RAG는 유사도에 따라 상위 5개의 청크(chunk)를 반환합니다. 그중 4개는 닭 육수(stock), 닭 샐러드(salad), 닭 _볶음(stir-fry)_에 관한 내용입니다. 이들은 모두 "닭(chicken)"이라는 단어를 포함하고 있습니다. 하나는 "425°F에서 40분 동안"이라는 문구를 찾아냈지만, 청킹(chunking) 과정에서 제목을 놓쳤기 때문에 모델은 이것이 구운 닭에 대한 것인지, 아니면 두 페이지 뒤에 나오는 구운 _감자(potatoes)_에 대한 것인지 구분할 수 없습니다. 답변: 확신에 차서, 틀렸습니다.
PageIndex는 트리를 따라 이동합니다 — 루트(root) → 가금류(Poultry) → 구운 닭(Roast Chicken) → 손상되지 않은 레시피, 즉 온도와 시간이 고유 제목 아래에 있고 재료와 단계가 여전히 붙어 있는 상태로 도달합니다. 문맥 속에서 정답을 읽어내어 정확하게 맞춥니다.
같은 책입니다. 한 리트리버(retriever)는 표면적인 텍스트를 추측했고, 다른 하나는 당신이 하는 방식과 똑같이 정답이 실제로 존재하는 곳으로 찾아갔습니다.
모두가 놓치는 부분: 상호 참조(Cross-References)
실제 문서는 포인터(pointer)로 가득 차 있습니다. "12페이지에 설명된 대로 염지액(brine)을 준비하세요." "2장의 팬 시어링(pan-searing) 기술을 참조하세요." "위 레시피의 스파이스 블렌드(spice blend)를 사용하세요." 구운 닭 레시피는 세 장 전에 나온 염지액 설명과 함께 있어야만 비로소 의미를 갖습니다.
벡터 RAG (Vector RAG)는 구조적으로 이러한 부분에 대해 눈이 멀어 있습니다. 벡터 RAG는 쿼리(query)와
'닮아 보이는' 상위 k개의 청크(top-k chunks)를 가져오는데, 상호 참조(cross-reference)는 아무런 유사성도 보이지 않습니다. "먼저 닭을 염지하세요"라고 말하는 청크는 높은 점수를 받지만, 세 장 뒤에 떨어져서
"실제로 염지액을 만드는 법을 설명하는" 청크는 점수가 낮아 결과에 포함되지 않습니다. 결국 모델은 레시피의 절반만 답변하며, 나머지 절반이 존재한다는 사실조차 알지 못합니다.
PageIndex는 전체 트리(tree)를 한 번에 봅니다. 노드(node)가 위치한 곳이 다른 곳을 가리킬 때 — "12페이지의 염지액을 참조하세요" — 탐색하는 LLM은 해당 포인터(pointer)를 따라가서 참조된 노드까지 가져올 수 있습니다. 이는 전용 링크 해결사(link-resolver)가 아니라, 단절된 파편들의 주머니(bag of disconnected fragments) 대신 전체 구조를 바탕으로 추론(reasoning)함으로써 발생하는 부수적인 효과입니다. 하지만 중요한 것은 결과입니다. 의존 관계가 누락되지 않고 읽힌다는 점입니다.
청크(chunk)는 섬입니다. 트리 노드(tree node)는 자신의 이웃을 알고 있습니다.
하지만 나는 책 한 권이 아니라 서가 전체를 가지고 있다
맞는 말입니다. 실제 시스템은 요리책 한 권이 아니라 수천 권의 도서관입니다. 이때 본능적으로 다시 벡터(vector)를 찾게 됩니다. 모든 책을 임베딩(embed)하고, 유사도 검색(similarity search)을 통해 후보를 선별한 다음, 내부를 탐색하는 방식 말이죠. 물론 그렇게 할 수도 있습니다. 벡터를 통한 1차 통과로 서가를 좁힌 다음, PageIndex가 각 책을 훑으며 정확한 레시피를 찾아가는 방식입니다. 이는 빠르며, Vectify의 자체 API에서도 현재 기본값으로 사용되고 있습니다. 대부분의 이유는 LLM으로 거대한 트리를 탐색하는 것이 밀리초 단위의 벡터 조회(vector lookup)보다 더 많은 호출 비용을 발생시키기 때문입니다.
하지만 방금 무슨 일이 일어났는지 주목하십시오. 당신은 결코 못하지 않았던 단 '한 가지 작업' — "어느 책들이 대략 이탈리아 음식에 관한 것인가?"라는 거친 분류 작업을 위해 파쇄기(shredder)를 다시 파이프라인(pipeline)으로 끌어들였습니다. 그리고 이제 당신은 단 하나의 질문에 답하기 위해 임베딩 저장소(embedding store), 벡터 인덱스(vector index), 그리고 추론 계층(reasoning layer)을 모두 유지 관리해야 합니다.
더 깔끔한 아이디어는 **구조를 끝까지 확장하는 것(structure all the way up)**입니다. 즉, 선반 전체가 하나의 커다란 트리가 되는 것입니다. 요리(Cuisine) → 책(book) → 장(chapter) → 레시피(recipe)로 이어지는 하나의 계층 구조를 LLM이 하향식(top-down)으로 탐색합니다. 임베딩(embeddings), 별도의 벡터 저장소(vector store), 문서 변경 시의 재인덱싱(re-indexing)이 필요 없습니다. Vectify는 이를 코퍼스 규모(corpus-scale) 검색을 위한 자신들의 "파일 시스템(file system)" 계층으로 제공합니다. 이는 더 새로운 경로이며 아직 성숙해가는 단계이지만, 이 논쟁의 정직한 종착점입니다. 만약 문서
_내부_에서 구조가 유사성(similarity)을 이긴다면, 컬렉션
_전체_에서도 구조가 유사성을 이깁니다.
교훈은 "규모를 위해 벡터를 추가하라"가 아닙니다. 핵심은 반사적으로 모든 단계에서 유사성 인덱스(similarity index)를 찾으려 하지 마라는 것입니다. 그것은 아키텍처로 위장한 오버헤드(overhead)일 뿐입니다. 꼭 필요하다면 임시적인 속도 단축키로 사용하되, 구조가 그 역할을 수행할 수 있는 순간 즉시 버리십시오. 어느 쪽이든, 파쇄기는 다시 돌아오지 않습니다.
어떤 것을, 언제 사용할 것인가
PageIndex는 공짜가 아닙니다. 벡터 조회(vector lookup)는 약 50ms가 소요되며 비용이 몇 센트 수준이지만, LLM으로 트리를 탐색하는 것은 수 초, 때로는 수십 초가 걸리며 여러 번의 모델 호출(model calls)이 필요합니다. 여러분은 밀리초(ms)와 몇 센트의 비용을 정확도, 전체 문맥이 포함된 답변, 그리고 감사 추적(audit trail)과 맞바꾸는 것입니다. 이것이 가치가 있는지는 전적으로 워크로드(workload)에 달려 있습니다.
| 워크로드 | 선택할 것 | 이유 |
|---|---|---|
| 밀집된 서류, 계약서, SOP, 매뉴얼 | PageIndex | 탐색을 위한 실제 구조가 필요함; 답변에 섹션 전체의 문맥 + 감사 추적이 필요함 |
| ... |
데이터가 진정으로 평면적(flat)이고 지연 시간(latency)이 전부인 경우(채팅 로그, FAQ, 자동 완성 등)에는 여전히 벡터가 승리합니다. 하지만 문서에 구조가 생기는 순간, 그것이 한 권의 책이든 백만 권이든 PageIndex의 영역이 됩니다.
그래서 RAG는 정말 죽었는가?
죽은 것은 모든 검색 문제에 대해 벡터 전용 RAG를 기본값(default)으로 선택하려는 반사적 습관입니다. 그 기본값은 구조화된 문서에 결코 적합하지 않았습니다. 단지 우리가 그만한 노력을 들일 만한 더 나은 옵션을 가지고 있지 않았을 뿐입니다. 이제 우리는 가지고 있습니다.
검색(Retrieval)은 유사성 검색(similarity-search) 문제에서 추론(reasoning) 문제로 변하고 있습니다. 벡터는 모델이 작고 문맥(context)이 부족했을 때 우리에게 필요했던 해킹(hack)이었습니다. 이제 둘 다 더 이상 사실이 아닙니다.
일주일 내내 재청킹(re-chunking)을 반복해 온, 가장 성능이 안 나오는 RAG 쿼리를 대신 트리(tree) 구조에 적용해 보세요. 코드는 오픈 소스로 공개되어 있습니다: github.com/VectifyAI/PageIndex. 그러고 나서 무엇이 망가지는지 저에게 알려주세요. 제 생각을 바꿔보세요: 무엇이 PageIndex를 망가뜨릴까요?
만약 여러분이 실제 애플리케이션과 LLM 사이의 경계에서 무언가를 구축하고 있다면, 그것이 제가 여기서 주로 다루는 내용입니다. 여러분의 문제 유형과 맞다면 계속 지켜봐 주세요.
Raj Kumar는 토론토 기반의 테크 리드(tech lead)입니다. 2011년부터 소프트웨어를, 2014년(유행하기 전부터)부터 AI를 구축해 왔습니다. Ruby, Python, 그리고 시스템 아키텍처(system architecture)를 다루며, 현재는 프로덕션 애플리케이션과 LLM 에이전트(LLM agents) 사이의 경계에서 작업하고 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기
