본문으로 건너뛰기

© 2026 Molayo

Qiita헤드라인2026. 05. 31. 16:17

벡터 검색만 하는 RAG는 "정작 필요할 때 기억하지 못한다" — 하이브리드 검색 + 측정을 통해 recall을 0.2에서 1.0으로 올린

요약

RAG 시스템에서 발생하는 기억력 문제를 아키텍처 재설계가 아닌 검색(Retrieval) 품질의 문제로 정의하고 해결하는 과정을 다룹니다. 데이터 수집 파이프라인의 결함을 발견하고, 골든 세트를 활용한 재현율(Recall) 측정 및 하이브리드 검색 도입을 통해 성능을 개선하는 실무적 방법론을 제시합니다.

핵심 포인트

  • 설계 결함과 검색 약점을 구분하여 불필요한 재설계를 방지함
  • 데이터 수집 단계의 누락(Truncation)이 검색 실패의 근본 원인일 수 있음
  • 골든 세트를 활용해 감이 아닌 지표(Recall) 기반으로 검색을 개선함
  • 하이브리드 검색과 우선순위 교정을 통해 재현율을 0.2에서 1.0으로 향상함
  • 「파트너」로 설계한 RAG가 막상 사용해보니
    기억해내지 못하는 문제의 정체 - 그것이
    설계의 결함이 아니라 retrieval (검색)의 약함이라는 것을 구분하는 법 (재설계의 함정에 빠지지 않기 위해)
  • 검색을 개선하기 전에 해야 할
    "수집되지 않은 데이터"의 회수
  • golden set (골든 세트)로 recall (재현율)을 측정하며 검색을 수정해 나가는 방법 (감에 의존하지 않기)
  • 하이브리드 검색, 다양화, 우선순위 편향 교정을 통해 recall이 어떻게 변했는가

측정하여 "불채택"을 결정하는 것의 가치 (시도해보고 악화된다면 버릴 용기)

  • 마지막에 남는 "어휘의 벽"과 GPU 이야기, 그리고
    웃픈 결말 - 검색 개선이 사실은
    데이터 수집 · 평가 설계 · 운영 아키텍처 · 인프라 (GPU 분산) 설계까지 이어져 있었다는 것

  • 개인 RAG / PKM을
    실제로 사용하면서, 원하는 정보를 잘 가져오지 못한다고 느끼는 사람

  • RAG를 "일단 벡터 검색"으로 구성한 채 방치하고 있는 사람

  • 검색 개선을
    감으로 하다가 늪에 빠진 경험이 있는 사람

  • RAG 처리 분산 구상을 세웠음에도, 정작 중요한 하드웨어가 작동하지 않아 발만 동동 굴렀던 경험을 한 사람 (이번의 저처럼)

지난번, 저는 자작 개인 RAG를 "5년 동안 함께할 파트너"로 만들기 위한 설계 원칙에 도달했습니다. 생메모를 버리지 않고 쌓아두기, 증류(Distillation)는 대체가 아닌 보강, 그리고 매 턴 관련 기억을 주입하는 Activation 층.

🔗 지난 글: AI에게 5번 수정당한 밤 — 개인 RAG를 5년 동안 함께할 파트너로 만드는 설계 사상

설계 자체는 마음에 들었습니다.

그런데 실제로 매일 사용하다 보니, 어느 날 이런 생각이 들었습니다.

"정작 필요할 때 기억해내지 못하네?"

과거에 분명히 이야기했을 내용, 분명히 기록했을 교훈. 그것을 지금의 문맥에서 끌어와 주길 바라는 순간에, 파트너는 슬그머니 다른 것을 내놓습니다. 혹은 최근 이야기만 내놓고, 조금 전의 중요한 결론은 잊어버리고 있습니다.

설계는 분명 좋을 텐데, 경험은 "기억력이 나쁜 파트너"였습니다.

여기서 많은 사람이 저지르는 실수가 있습니다. "설계가 잘못되었구나"라고 생각하여 다시 만들기 시작하는 것입니다. 지난 글에서 저와 AI는 "재설계의 함정"을 실컷 겪었습니다. 그래서 이번에는 우선 그것을 의심했습니다.

이것은

아키텍처의 결함이 아니라, retrieval (검색)의 약함이 아닐까?

이층 구조도, Capture-first도, Activation도, 골격은 지난번에 정한 대로 타당합니다. 고쳐야 할 것은 검색의 질이지, 설계의 재구축이 아닙니다. 이 구분 작업이 이번의 출발점이었습니다.

문제가 발생하면, 다시 만들기 전에 "어느 층의 문제인지"를 파악한다. 개인용 RAG든, 업무에서 다루는 거대한 시스템이든, 우선 분리한 뒤 움직인다 — 이 순서는 변하지 않습니다.

검색을 개선하겠다고 의욕을 불태우던 찰나, 불길한 가능성을 깨달았습니다.

정보를 가져오지 못하는 것이 검색이 나빠서가 아니라, 애초에 데이터가 들어있지 않기 때문은 아닐까?

조사해 보니 맞았습니다. 대화를 수집하는 파이프라인이 긴 발언의 후반부를 조용히 잘라내고 있었습니다. 게다가 AI가 도구를 사용하며 여러 번에 걸쳐 답변할 경우, 두 번째 이후의 응답이 통째로 누락되고 있었습니다.

즉, 과거의 밀도 높은 토론일수록 중요한 부분이 인덱스(Index)에 존재하지 않았습니다. "검색해도 나오지 않는" 것은 당연하며, 그곳에는 처음부터 아무것도 없었던 것입니다.

수집 과정에서 일어난 일 (이미지)
긴 발언: ┌──────────────────────────────────┐
│ ●●●●●●●● 설계의 핵심은 사실 이 후반부에 │
...

이는 사소해 보이지만 결정적인 교훈이었습니다.

검색을 아무리 다듬어도 원본 데이터에 없는 것은 가져올 수 없습니다. retrieval (검색) 이전에 capture (수집)를 의심하십시오.

수집의 경계 판정을 수정하여 잘라내기를 중단하고, 모든 대화를 다시 넣었습니다. 사라졌던 과거의 주고받은 대화들이 눈에 띄게 돌아왔습니다. 검색 이야기는 이제부터가 진짜입니다.

이 부분이 이번에 가장 전달하고 싶은 내용입니다.

검색 개선은 감으로 하면 반드시 늪에 빠집니다. "왠지 좋아진 것 같다"는 느낌은 대개 착각이거나, 다른 무언가를 망가뜨리고 있는 것입니다.

그래서 작은 golden set ("이 질문에는 이 기록이 나왔으면 좋겠다"라는 정답 쌍의 집합)를 직접 만들었습니다. 그리고 변경할 때마다 **recall (상위 N개 안에 정답이 포함된 비율)**을 측정합니다. Before/After를 숫자로 확인합니다. 이것만으로도 세상이 바뀝니다.

처음 측정했던 순수 벡터 검색 (Vector Search) 스코어는 솔직히 형편없었습니다. 의미가 가까운 것은 찾아내더라도, 표현 방식이 조금만 달라도 놓쳐버립니다. recall@10이 0.2, 즉 10개를 출력했을 때 정답을 2할밖에 맞히지 못했습니다.

해결책은 RAG의 정석대로였지만, 하나씩 적용하고 하나씩 측정했습니다.

검색 파이프라인 (최종 형태의 이미지)
질문
│
...

하이브리드 검색 (Hybrid Search): 의미 기반의 벡터 검색에, 글자 그대로의 어휘 일치 (BM25 계열)를 병행시켜 양자의 순위를 융합합니다. 일본어의 어형 변화(예: 「遅い(느리다)」와 「遅すぎ(너무 느리다)」와 같은 변화)를 글자 기반 측면에서 잡아낼 수 있도록 고안했습니다. 이것이 가장 효과적이었습니다. -
다양화 (Diversification): 상위 결과가 '거의 동일한 내용'으로만 채워지지 않도록, 조금 떨어진 후보들도 일정 범위 내에서 섞어줍니다. 이전 글에서 썼던 "후지산 기슭 근처를 알려주는 설계"를 검색 측면에서도 지키기 위함입니다. -
우선순위 편중 교정: "절대로 잊으면 안 된다"라고 표시를 해둔 기록을 최상위에 고정해 두었는데, 이런 기록이 늘어나면 현재 질문과 정말 가까운 기록을 밀어내 버립니다. 고정을 해제하고, 순위를 조금 높여주는 정도로 균형을 맞췄습니다.

일본어의 "글자 그대로 잡는" 측면을 조금 더 구체화하자면, 단어를 그대로 보는 것이 아니라 문자를 1~2글자의 나열 (n-gram)로 쪼갠 뒤 일치 여부를 확인합니다. 이렇게 하면 어형이 바뀌더라도 부분적으로 글자가 겹치게 되어 BM25 측에서 잡아낼 수 있게 됩니다.

# 일본어의 어형 변화를 글자 기반으로 잡기: 문자를 1~2gram으로 쪼개어 BM25로 전달
def char_ngrams(text, n_min=1, n_max=2):
grams = []
...

결과적으로, golden set의 recall@10은 0.2에서 1.0까지 올라갔습니다. crowding (중요 표시가 된 기록이 상위를 점유하는 현상)도 실측 결과 크게 개선되었습니다.

내용을 조금만 공유하자면, 융합의 핵심은 스코어의 절대값이 아니라 "순위"로 섞는 것입니다 (거리와 BM25 스코어는 척도가 달라 더할 수 없지만, 순위라면 더할 수 있습니다).

# RRF: 두 개의 랭킹을 "순위"로 융합 (간략 버전, 값은 예시)
def rrf(rankings, k=60): # k는 안정화를 위한 상수
score = {}
...

그리고 "감으로 하지 않는다"의 핵심이 바로 이것입니다. 변경할 때마다 동일한 golden set을 흘려보내고, 숫자의 차이만을 봅니다.

# golden set으로 recall@N 측정 (간략 버전)
def recall_at_n(golden, search, n=10):
hit = 0
...

※ 게재된 코드는 개념을 보여주기 위한 간략 버전이며, 실제 파라미터나 구성은 조금 더 복잡합니다. 요점은 "순위로 융합", "숫자로 비교" 이 두 가지입니다.

깨달음: 검색 개선의 가치는 "시도한 방법"이 아니라 "측정된 차이"에 있다. recall을 보면서 수정하면, 효과가 있는 방법과 없는 방법을 한눈에 알 수 있다.

숫자가 있으면 논의가 "취향"에서 "사실"로 바뀝니다. 이는 설계 리뷰에서도 마찬가지였습니다.

측정 주도 (Measurement-driven) 방식의 가장 맛있는 부분은, 안 좋은 방법을 안 좋다고 확정할 수 있다는 점입니다.

기대했던 방법 중 하나는 "쿼리 확장 (Query Expansion)"이었습니다. 검색 전에 LLM을 사용하여 질문을 다른 표현으로 바꾸고, 여러 변형으로 검색하는 것입니다. 어휘의 차이에 강해질 것이라는—계획이었습니다.

시도해보고 측정했습니다. 악화되었습니다. 표현을 바꾸는 과정에서 의미가 미묘하게 어긋나며 (drift), 오히려 정답을 놓치게 됩니다. 게다가 느립니다. 로컬의 경량 LLM으로는 품질도 속도도 수지타산이 맞지 않았습니다.

또 다른 방법인 "더 강력한 임베딩 모델 (Embedding Model)로 교체"는 실제로 구성해서 돌려보았습니다. 그런데 수중에 있는 CPU에서는 5건 중 4건이 처리 실패했을 뿐만 아니라, 통과한 것도 건당 몇 초씩 걸렸습니다. 매번 검색할 때마다 그 비용이 추가된다면, 실시간으로 사용하는 파트너로서는 현실적이지 않았습니다.

두 방법 모두 측정 후에 불채택했습니다. 코드는 "측정 결과 net-negative · 실서비스 미연결"이라고 명시하여 남겨두었습니다 (이런 실패의 과정이야말로 나중에 다시 볼 가치가 있습니다).

여기서 운영상의 원칙 하나가 확립되었습니다. 기능의 좋고 나쁨이 아니라, 속도와 경험을 지키기 위한 판단 — 이른바 비기능 요구사항 (Non-functional requirements)의 설계입니다.

깨달음: 실시간으로 돌아가는 검색 (읽기) 경로에 무거운 처리를 두지 말 것. 무거운 것은 백그라운드 배치(Batch)로. 이를 어기면 품질이 올라가더라도 사용자 경험이 죽는다.

무거운 처리를 어느 경로에 둘 것인가
┌─ 라이브 경로 (읽기 · 실시간)─────────────────┐
│ 질문 → 가벼운 검색 → 즉시 반환 ← 여기에 무거운 처리를 두지 말 것 │
...

"시도했다 → 악화되었다 → 그래서 버렸다"라고 당당하게 말할 수 있는 이유는 측정하고 있기 때문입니다. 감에 의존하면 "뭔가 넣었더니 효과가 있는 것 같다"라는 상태가 영원히 남게 됩니다.

일반적인 질문에서는 recall(재현율)이 만점에 가깝게 나왔습니다. 하지만 일부러 어렵게 만든 golden set——질문과 가져오고 싶은 기록이 의미는 같지만 단어를 전혀 공유하지 않는 케이스에 대해서는 여전히 0.2에 머물렀습니다. 이것이 이 글에서 "어휘의 벽"이라고 부르는 것입니다.

예를 들어,

  • "의욕이 계속된다"라는 말을 듣고, "동기 부여 유지" 기록을 가져오고 싶을 때
  • "동료와 힘을 합친다"라는 말을 듣고, "팀워크" 기록을 가져오고 싶을 때

인간이라면 "같은 말을 하고 있다"는 것을 알 수 있습니다. 하지만 글자 그대로의 형태는 단 한 글자도 겹치지 않으며, 경량 임베딩 (Embedding)으로도 도달할 수 없습니다. 이곳이 로컬 환경에서 구축한 RAG의 마지막 벽이었습니다.

어휘의 벽: 의미는 같지만, 단어가 한 글자도 겹치지 않음
질문: "의욕이 계속된다"
│ 글자 형태의 중복 = 제로
...

대처법은 알고 있습니다. **cross-encoder 리랭커 (Reranker)**로 상위 항목을 다시 정렬하거나, **다국어에 강한 임베딩 (Embedding)**으로 교체하는 것입니다. 둘 다 효과가 있다는 것을 알고 있습니다.

하지만——둘 다 실용적인 속도로 동작시키려면 고성능 GPU가 필요합니다. CPU로는 제3막에서 실제로 측정한 것처럼 건당 몇 초씩 걸리고 맙니다. 매번 검색할 때마다 몇 초를 기다려야 한다면, 실시간으로 답변을 돌려주는 파트너 경로에는 태울 수 없습니다.

그리고 여기서부터 이야기가 조금 이상한 방향으로 흘러갑니다.

고성능 GPU가 필요합니다. 하지만 그것을 한 대에 전부 짊어지게 하는 것은 부담스럽습니다. 그렇다면 집에 있는 여러 대의 GPU 머신에 역할을 나누면 된다——그렇게 생각한 저는 기쁜 마음으로 그 분산 구성을 그리기 시작했습니다.

임베딩 (Embedding)이나 리랭커 (Reranker) 같은 무거운 처리를 집에 있는 여러 대의 GPU 머신에 역할 분담시키고, 노트북은 조작용 단말기로 전념하게 한다. LAN을 흐르는 것은 쿼리(Query)와 결과인 수 KB뿐이므로 병목 현상 (Bottleneck)이 되지 않는다——그림까지 그려가며 스스로 만족해하고 있었습니다.

의기양양하게 데스크톱의 전원을 켰습니다.

작동하지 않았습니다.

다른 한 대도 결과는 마찬가지였습니다. 오랫동안 전혀 손대지 않았던 데스크톱 두 대 모두, 전원을 넣어도 부팅되지 않고 비프음만 5번 울릴 뿐이었습니다. CPU 발열 문제일 것이라고 짐작하여 일체형 수냉 쿨러와 파워 서플라이를 새것으로 교체해 볼 생각이지만, 확정 진단은 아직입니다. 어찌 되었든 RAG 처리 분산 구상은 정작 중요한 머신 두 대가 모두 작동하지 않는다는 허무한 현실 앞에서 멈춰 섰습니다.

여기서 잠시, 손에 든 노트북에 희망을 걸었습니다.

확실히 이 노트북에도 RTX 2070이 탑재되어 있습니다. 이걸로 테스트하면 되지 않을까——순간 그렇게 생각했습니다.

하지만 곧 마음을 고쳐먹었습니다. 이 노트북은 제가 매일 VSCode와 Claude Code를 돌리고 있는 작업 기기 그 자체입니다. 본래의 분산 구성에서 노트북에 부여한 역할은 쿼리를 던지고 결과를 받는 쪽——즉 조작 역할이었습니다. 무거운 처리는 데스크톱 측 GPU에 맡기고, 손에 든 단말기는 가벼운 상태로 유지한다. 이것은 타협이 아니라 의도적으로 배치한 설계입니다.

평소 사용하는 작업 도구에 임베딩 서버나 리랭커를 상시 짊어지게 하면, 정작 중요한 작업 자체가 무거워집니다. 그러므로 이 벽에 진심으로 도전하는 것은 역시 데스크톱을 고쳐서 본래의 분산을 구축한 이후의 일입니다. 노트북으로도 시도할 수 있는 범위는 있지만, 리랭커를組み込み(組み込み, 내장)하는 등의 실제 구현은 아직 앞으로의 이야기입니다.

머릿속의 처리 분산 구상 (집에 있는 여러 머신에 역할을 분산)
[머신 A: Ollama (임베딩 + LLM) 담당]
┌──────────────────────────────────┐
...

만약을 위해 덧붙이자면, 이 배치는 즉흥적인 짜깁기가 아닙니다. 임베딩 (Embedding)도 LLM도 가벼운 모델로 충분하므로, 6GB라도 Turing 아키텍처로 빠른 RTX 2060으로. ChromaDB 본체 처리는 CPU 중심이므로, 남는 GPU에 리랭커 (Reranker)를 올릴 수 있는 1080Ti 머신으로. 노트북은 조작 역할에 전념하고, LAN을 흐르는 것은 쿼리와 결과인 수 KB뿐——각 하드웨어의 장단점에 맞춰 역할을 맞춘 엄연한 설계 판단입니다. RAG의 검색을 고치려던 것이 어느새 네트워크와 하드웨어 배치를 고민하는 인프라 설계가 되어 있었습니다.

그리고 이 "무거운 역할을 나누어, 특기에 맞는 하드웨어에 싣는다"라는 사고방식은, AI에 요구하는 성능이 높아질수록 효과를 발휘합니다. 규모가 바뀌면 스케일링(Scale)이나 오케스트레이션(Orchestration) 도구는 바뀌더라도, 설계의 사고방식은 그대로 유지됩니다. 책상 위에서 내린 판단은 거대한 시스템의 처리 분산(Processing Distribution)과 맞닿아 있었습니다.

그 첫걸음으로—임베딩(Embedding)의 접속 대상을 환경 변수로 추상화하는 것만큼은, 하드웨어 복구를 기다리지 않고 설계할 수 있습니다. 접속 대상을 하드코딩(Hard-coding)하지 않으면, 단일 장비든 분산 환경이든 "동일한 코드"를 유지한 채 목적지만 바꾸면 되기 때문입니다 (구현은 앞으로 진행할 예정입니다).

# 임베딩의 접속 대상을 환경 변수로 추상화 (설계 단계, 향후 구현 예정)
# 단일 장비든 분산 환경이든 "동일한 코드"로, 목적지만 교체
import os
...

몇 세대 전의 PC들을 모아 역할을 나누어 효율적으로 RAG를 돌리는—그 처리 분산 구상은 일단 봉인해 둡니다. 데스크톱 PC는 부품을 교체하면 작동할 것이므로, 수리한 후에 본래의 분산 구성을 구축할 예정입니다.

마지막으로, 환경에 대해 조금만 이야기하겠습니다.

로컬에서 GPU를 사용한 RAG 튜닝(Tuning)은 강력합니다. 클라우드로 보내지 않고, 수중에 있는 환경에서 몇 번이고 측정하며 돌릴 수 있습니다. 제2막~제4막은, 로컬에 어느 정도 성능이 되는 GPU가 있었기에 깊이 파고들 수 있었습니다.

이 GPU는 RAG를 위해 산 것이 아닙니다. 자동차를 좋아해서 본격적인 레이싱 시뮬레이터를 구축하고, 새로운 것을 좋아하는 성격 탓에 PC 연결형 VR(Oculus Rift 2)까지 손을 대는—그런 취미를 위해 장착해 두었던 것이, 우연히 이번 튜닝에 도움이 되었을 뿐입니다. 둘 다 어느 정도의 GPU가 없으면 쾌적하게 작동하지 않으니까요.

만약 수중에 잠자고 있는 게이밍 PC가 있다면, 그것은 훌륭한 실험 환경입니다. GPU가 있으면 로컬 LLM/RAG를 부담 없이 몇 번이고 시도할 수 있습니다. 실제로 제2막~제4막의 튜닝은 그 노트북 한 대로 진행했습니다.

화려한 이야기는 하나도 없었습니다. 제가 한 일을 나열하면 다음과 같습니다.

검색 전에 데이터 수집(Ingestion)을 의심했다 (원 데이터에 없는 것은 불러올 수 없다) -
골든 세트(Golden Set)를 만들어, 재현율(Recall)을 측정하며 수정했다 (감에 의존하지 않는다) -
효과가 있는 방법은 도입하고, 효과가 없는 방법은 측정하여 버렸다 (채택하지 않을 용기) -
무거운 처리는 실시간 경로에 두지 않았다 (품질보다 사용자 경험을 해치지 않는다) -
마지막에는 GPU의 벽에 부딪혀 본래 필요한 분산 구성을 그렸으나, 정작 중요한 데스크톱이 켜지지 않아 제자리걸음을 했다 (웃픈 에피소드)

이것들은 특정 도구가 무엇이든 간에 변하지 않는 원칙이라고 생각합니다. ChromaDB가 다른 벡터 DB로 바뀌어도, Ollama가 다른 런타임(Runtime)으로 바뀌어도, "측정하며 수정한다", "무거운 것은 라이브(Live) 환경에 두지 않는다", "무거운 처리는 적재적소의 하드웨어에 할당하고, 접속 대상을 추상화하여 단일 장비든 분산 환경이든 동일한 코드로 동작하게 한다", "할 수 있는 데까지는 수중에서 해결하고, 부족하면 솔직하게 증설한다"는 아마 계속 유효할 것입니다.

파트너(AI)는 처음부터 똑똑한 것이 아닙니다. 측정하고, 수정하고, 다시 측정한다. 그 지루한 왕복만이, 기억력이 나쁜 파트너를 조금씩 "기억해 주는 파트너"로 바꾸어 갑니다.

마지막으로 남은 "어휘의 벽"은 아직 넘지 못했습니다. 이를 넘으려면 강력한 임베딩과 리랭커(Reranker)를 제대로 구동할 수 있는 분산 구성이 필요합니다. 그것은 데스크톱을 수리하고 구축한 뒤의 일입니다. 당분간은 노트북으로 시도할 수 있는 범위부터 손을 대겠지만, 본격적인 작업은 그 이후—라는 것이 지금의 솔직한 현재 위치입니다.

로드맵 — 현재 위치
✅ 완료
├ 수집 누락 수정 (D1/D5) → 전체 대화 재투입
...

RAG는 한 번 튜닝하면 완성되는 것이 아닙니다. 검색이 잘 되지 않는 상황에 직면하면, 우선 무엇이 일어나고 있는지 조사하고 원인을 분석합니다. 그 후에 수정해야 할 곳을—때로는 설계 단계까지 파고들어—조치하고, 다시 재현율(Recall)을 측정합니다.

조사 → 분석 → 개선의 루프를 멈추지 않고 계속 돌리는 것이, 파트너를 진정으로 키워나가는 것이라고 생각합니다. 이번 어휘의 벽도 그 과정 중 하나일 뿐입니다. 하나를 넘으면 또 다음 벽이 보일 것입니다. 그때도 같은 방식으로 마주하겠습니다.

🔗 개인 블로그에도 동일한 내용의 기사와 관련 글이 있습니다: 벡터 검색만 하는 RAG는 "정작 필요할 때 기억하지 못한다" — 하이브리드 검색 + 측정을 통해 recall을 0.2에서 1.0으로 올린 이야기

AI 자동 생성 콘텐츠

본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0