RAG Python Agentic RAG Qdrant HyDE Multi-Source

[RAG Playground] Quest 7: Agentic RAG으로 검색 전략을 스스로 고르게 만들기

지난 Quest 6에서 Query RewritingQuery Rewriting검색 성능을 높이기 위해 사용자의 원본 질의를 LLM으로 변환하거나 확장하는 기법입니다. HyDE(가상 문서 생성)와 Multi-Query(대안 질의 생성) 등이 대표적입니다.을 붙이면서 검색 품질이 꽤 좋아진 것을 볼 수 있었습니다. 그런데 과연 질의마다 항상 같은 검색 전략을 쓰는 게 맞을까요????

예를 들어 "동래구 목욕탕"처럼 의도가 또렷한 질의는 그냥 Hybrid + Re-rank로도 충분할 걸로 보이는데요, 반면 "이번 주말에 아이들이랑 갈 만한 데" 같은 질의는 애초에 무엇을 찾고 있는지부터 해석해야 합니다. 어떤 질의는 Multi-Query가 낫고, 어떤 질의는 HyDEHyDEHypothetical Document Embeddings의 약자입니다. 사용자 질의에 대한 가상의 답변 문서를 LLM으로 생성한 뒤, 그 문서의 임베딩으로 검색하는 기법입니다. 실제 답변과 유사한 문서를 찾는 데 효과적입니다.가 낫고, 어떤 질의는 아예 다른 데이터 소스를 먼저 봐야 합니다.

그래서 이번 퀘스트에서는 Agentic RAGAgentic RAG질의 분석, 검색 전략 선택, 데이터 소스 라우팅, 재시도(fallback) 같은 retrieval 과정을 에이전트가 스스로 조정하는 RAG 방식입니다.을 붙였습니다. 질의를 보고 검색 전략과 데이터 소스를 스스로 선택하고, 결과가 부족하면 fallback까지 타는 실행 루프를 만들어봤습니다.

이번 퀘스트에서 만든 것

이번 Quest 7의 목표는 새로운 검색 기법 하나를 더 추가하는 게 야니라 에이전트가 스스로 판단핸서 언제, 어디에, 어떤 순서로 쓸지 결정하는 계층을 만드는 방향입니다.

핵심은 네 가지입니다.

  1. 질의의 모호함을 판단한다
  2. 데이터 소스를 선택한다
  3. 검색 전략을 선택한다
  4. 결과가 약하면 다른 전략으로 다시 시도한다

이번에 실제로 연결한 소스는 두 개입니다.

  • family_card: 부산 가족사랑카드 참여업체
  • library: 부산 도서관 정보 스냅샷

Step 1: 소스 카탈로그 만들기

기존에는 사실상 family_card_shops_hybrid 컬렉션 하나만 있다고 생각하고 하드 코딩을 해두었었는데요 ! 에이전트가 스스로 어떤 데이터 소스가 있는지 파악할 수 있으려면 하드 코딩을 제거하고, 카탈로그 형태로 AI에게 알려줄 필요가 있습니다.

그래서 SourceConfig를 만들고 소스별 메타데이터를 한 곳에 모아보도록 하겠습니다.

@dataclass(frozen=True, slots=True)
class SourceConfig:
    source_id: str               # 소스 식별자 (예: "family_card", "library")
    label: str                   # 사람이 읽기 좋은 소스 이름 (예: "가족사랑카드")
    description: str             # 소스에 대한 간략한 설명
    collection_name: str         # Qdrant 기본 컬렉션 이름 (벡터 검색용)
    hybrid_collection_name: str  # Qdrant 하이브리드 컬렉션 이름 (키워드+벡터 검색용)
    default_json_path: Path      # 로컬 스냅샷 JSON 파일 경로
    document_loader: DocumentLoader  # JSON → Document 변환 함수
    domain_hint: str             # Query Rewriting 프롬프트에 주입할 도메인 컨텍스트
    answer_hint: str             # 답변 생성 프롬프트에 주입할 도메인 힌트
    query_keywords: tuple[str, ...]  # 라우팅 시 이 소스를 선택하는 키워드 목록

그리고 카탈로그에는 기존에 사용하던 ‘가족사랑카드’ 소스에 ‘도서관’ 소스를 추가했습니다.

SOURCE_CATALOG = {
    "family_card": SourceConfig(
        source_id="family_card",
        label="가족사랑카드",
        hybrid_collection_name="family_card_shops_hybrid",
        query_keywords=("할인", "가맹점", "맛집", "미용실", "목욕탕", "가게"),
        ...
    ),
    "library": SourceConfig(
        source_id="library",
        label="도서관",
        hybrid_collection_name="busan_libraries_hybrid",
        query_keywords=("도서관", "책", "공부", "아이", "주말", "갈 만"),
        ...
    ),
}

Step 2: Query Rewriter를 소스별로 바꾸기

Quest 6 에서 만든 HyDE/Multi-Query 는 프롬프트가 "부산 가족사랑카드 참여업체"에 강하게 묶여 있었기 때문에 새롭게 다른 소스가 붙으면 이 프롬프트는 바로 사용이 불가능해집니다.

예를 들어 "아이들이랑 갈 만한 데"를 도서관에서 찾고 싶은데, HyDE가 계속 "업체명, 혜택, 할인" 같은 문서를 만들면 오히려 검색 노이즈가 되기 때문이에요 😮‍💨

그래서 generate_hypothetical_document()generate_multi_queries()domain_context를 추가했습니다.

def generate_hypothetical_document(
    query: str,
    domain_context: str = "부산광역시 가족사랑카드 참여업체 데이터베이스",
) -> str:
    ...

def generate_multi_queries(
    query: str,
    n: int = 3,
    domain_context: str = "부산광역시 가족사랑카드 참여업체 데이터베이스",
) -> list[str]:
    ...

덕분에 family_card에서는 할인/업체 중심으로, library에서는 도서관/문화공간 중심으로 재작성할 수 있게 됐습니다 👍


Step 3: Agent Planner 만들기

이번 Quest 에서 가장 핵심이 되는 부분은 바로 planner 입니다.

  • 이 질의가 구체적인가?
  • 모호한가?
  • 업체 데이터가 맞는가?
  • 도서관 데이터가 맞는가?
  • 첫 검색이 실패하면 무엇으로 fallback 할까?

사람으로 치면 동일한 질문이 들어왔을 때, 위와 같은 판단을 순차적으로 거칠 겁니다. LLM도 질의의 맥락을 보고 전략을 달리할 수 있기 때문에, 이 판단 흐름을 코드로 구조화해봤습니다.

현재는 LLM planner 대신 휴리스틱 기반 planner로 시작해 보겠습니다 :)

if score >= 3:
    ambiguity = "high"
    initial_mode = "multi_rerank"
    fallback_modes = ["hyde_rerank", "rerank"]
    sufficiency_threshold = 0.03
elif score == 2:
    ambiguity = "medium"
    initial_mode = "multi_rerank"
    fallback_modes = ["rerank", "hyde_rerank"]
    sufficiency_threshold = 0.15
else:
    ambiguity = "low"
    initial_mode = "rerank"
    fallback_modes = ["multi_rerank"]
    sufficiency_threshold = 0.55

여기서 포인트는 임계치를 질의 모호도에 따라 다르게 둔 것인데요 "동래구 목욕탕" 같은 명확한 질의는 relevance가 0.9 이상도 잘 나오기 때문에 0.55 정도로 잡아도 괜찮습니다.

그런데 "아이들이랑 갈 만한 데" 같은 탐색형 질의는 relevance가 구조적으로 낮게 형성됩니다. 이때도 같은 임계치를 쓰면 영원히 실패 판정만 날 거예요 🫣

그래서 high ambiguity 질의는 threshold를 과감하게 0.03까지 낮춰서 진행해 보겠습니다


Step 4: fallback이 있는 실행 루프

planner만 있으면 아직 Agentic이라고 부르기 애매하죠? 실제로는 검색을 돌려 보고, 결과가 약하면 전략을 바꾸는 루프가 있어야 하거든요.

실제로 아래와 같은 워크플로우가 필요하다고 판단하여 구현해 보았습니다.

질의 입력
  → planner가 소스/전략 선택
  → 1차 검색 실행
  → top relevance 확인
  → 부족하면 fallback 전략 실행
  → 필요하면 단일 소스 → 멀티 소스로 확장
  → 원본 질의 기준으로 답변 생성

특히 소스를 탐색할 때, 처음부터 모든 소스를 다 때리지 않고 우선 library 하나만 조회합니다. 그리고 결과가 부족하다고 에이전트가 판단하면 그때 library + family_card로 확장합니다.

처음부터 모든 소스를 다 열면 검색 비용도 커지고, 관련 없는 후보가 너무 많이 섞입니다. 반대로 source expansion을 fallback으로 미루면, 좀 더 “에이전트가 판단해서 행동을 넓혀 가는” 형태가 될 수 있죠 💪


Step 5: 실제 실행 결과

케이스 1. 구체적인 질의

"동래구 목욕탕"은 planner가 이렇게 판단했습니다.

  • source: family_card
  • initial mode: rerank
  • fallback: 사용 안 함

실제로 relevance는 0.99 / 0.98 / 0.90 수준으로 나왔고, 바로 종료됐습니다. 이런 질의는 굳이 에이전트가 과하게 복잡할 필요가 없다는 걸 확인할 수 있었습니다.

케이스 2. 탐색형 질의

"이번 주말에 아이들이랑 갈 만한 데"는 이렇게 흘렀습니다.

  1. 처음에는 library 소스만 선택
  2. multi_rerank로 1차 검색
  3. 결과가 약해서 fallback 발동
  4. hyde_rerank로 재검색
  5. 이때 library + family_card로 소스 확장

최종적으로 답변에는 이런 후보들이 섞여 나왔습니다.

  • 부산아쿠아리움
  • 키자니아 부산
  • 푸른누리작은도서관
  • 연제도서관

즉, 처음에는 도서관부터 보다가, 결과가 충분하지 않다고 판단하자 체험형 장소가 들어 있는 가족사랑카드 소스까지 확장한 겁니다. 완벽하다고 하긴 어렵지만, “정답이 하나로 고정되지 않은 질의”를 다루는 방식은 확실히 이전보다 한 단계 나아갔습니다.

Rerank(기준) vs Agentic — 질의 유형별 Top-1 Relevance 비교

명확한 질의에서는 기존 Rerank와 동등하고, 탐색형 질의에서는 Agentic이 눈에 띄게 개선됩니다.

질의 유형별 Top-1 Relevance Score 비교

명확한 질의(동래구 목욕탕, 부산진구 한식)는 Agentic이 불필요한 전략을 추가하지 않고 Rerank만으로 종료해 기존과 동등한 품질을 유지합니다. 반면 탐색형 질의(해운대 양식 할인, 아이 주말 나들이, 갈 만한 곳)에서는 fallback과 멀티 소스 확장 덕분에 relevance가 크게 올라갑니다.

탐색형 질의 fallback 단계별 개선

"이번 주말에 아이들이랑 갈 만한 데" 질의에서 fallback이 실제로 얼마나 결과를 끌어올렸는지 단계별로 확인할 수 있습니다.

탐색형 질의 — Fallback 단계별 Top-1 Relevance

1차 검색(library 단독 + multi_rerank)에서 relevance 0.18로 임계치 0.03을 넘기긴 했지만, planner가 medium 이상 전략을 선택한 질의이므로 더 나은 결과를 기대할 수 있었습니다. fallback으로 소스를 확장하고 hyde_rerank를 적용하자 0.39까지 올라가며 부산아쿠아리움, 키자니아 부산 등이 최종 후보로 진입했습니다.


이번에 느낀 점

1. Agentic RAG는 검색 기법보다 오케스트레이션 문제다

Quest 3~6까지는 “어떤 검색을 더 붙일까?”의 문제였는데, Quest 7에 와서는 “언제 어떤 검색을 쓸까?”의 문제가 됐습니다. 같은 HyDE라도 언제 쓰느냐에 따라 가치가 완전히 달라집니다.

-> RAG 쉽지 않다 ^^;

2. 멀티 소스는 붙이는 것보다 라우팅이 더 어렵다

도서관 소스를 붙이는 것 자체는 어렵지 않았습니다. 진짜 어려운 건 "이 질의가 어느 소스에 더 가깝냐"를 판단하는 부분이었습니다. 잘못 열면 노이즈가 급격히 늘어나고, 너무 보수적으로 열면 recall이 떨어집니다.

-> 나중엔 소스 자체도 RAG 를 적용해야 한다 !

3. relevance threshold는 질의 난이도에 따라 달라져야 한다

이건 생각보다 중요했습니다. 명확한 질의와 탐색형 질의를 같은 임계치로 재단하면 broad query는 무조건 실패합니다. 결국 planner는 전략뿐 아니라 성공 판정 기준도 같이 가져야 한다는 걸 배웠습니다. 😮‍💨

4. 실제 서비스라면 다음 단계는 LLM planner다

지금은 휴리스틱으로 시작했지만, 이 구조는 결국 LLM planner나 tool-calling agent로 가기 위한 발판입니다. 지금 단계에서는 오버엔지니어링을 피하고 싶어서 휴리스틱으로 끝냈지만, 다음에 데이터 소스가 더 늘어나면 규칙 기반만으로는 한계가 올 것 같다는 생각이 듭니다.


다음 퀘스트

Quest 7까지 오면서 드디어 “검색을 좀 영리하게 운영한다”는 느낌을 얼추 만들었습니다. 하지만 아직도 한 가지가 부족합니다. 이게 실제로 얼마나 좋아졌는지를 숫자로 측정하지 않았다는 점입니다.

다음 퀘스트에서는 본격적으로 RAG Evaluation을 붙여서, 각 모드와 Agentic RAG의 효과를 NDCGNDCGNormalized Discounted Cumulative Gain의 약자입니다. 검색 결과의 순위와 관련도를 함께 반영하는 랭킹 평가 지표로, 관련 문서가 상위에 올수록 점수가 높습니다., MRRMRRMean Reciprocal Rank의 약자입니다. 첫 번째 정답 문서가 검색 결과에서 얼마나 앞에 나타나는지를 측정하는 평가 지표로, 정답이 더 높은 순위에 있을수록 점수가 높습니다.로 정량 비교해보려고 합니다.

이제야 정말 “감”이 아니라 “지표”로 얘기를 해보겠습니다. 시리즈 마무리도 코앞이네요 홧팅 💪