본문 바로가기

RAG

쿼리 재작성, 반복 검색, 멀티소스 라우팅 — Agentic RAG 동작 원리와 동적 검색 전략 완전 정리

반응형

RAG 시스템을 만들고 나면 이런 한계가 생겨요.

"단순한 질문은 잘 답하는데, '2024년 실적을 바탕으로 2025년 전략을 분석해줘' 같은 복잡한 질문은 엉뚱한 답이 나온다."

이건 일반 RAG의 구조적 한계예요. 한 번 검색하고 끝나는 구조로는 복잡한 다단계 질문을 처리할 수가 없어요. 이걸 해결하는 게 Agentic RAG입니다.


일반 RAG의 한계

일반 RAG의 파이프라인은 고정돼 있어요.

질문 → 벡터 검색 → 상위 K개 문서 → LLM → 답변

단순하고 빠른데, 세 가지 문제가 있어요.

한 번밖에 검색 못 해요. 검색 결과가 별로여도 그냥 그걸로 답해요. "부족하다"는 판단을 못 해요.

쿼리 복잡도를 무시해요. "파이썬이 뭐야?" 같은 단순 질문과 "2024년 AI 트렌드를 분석하고 우리 회사 전략에 어떻게 적용할지 알려줘" 같은 복잡한 질문에 똑같은 방식으로 검색해요.

소스가 하나예요. 벡터 DB 하나만 봐요. 필요에 따라 웹 검색, SQL DB, 외부 API를 골라서 쓰는 게 불가능해요.


Agentic RAG란

Agentic RAG는 검색 자체를 에이전트가 동적으로 제어하는 구조예요.

질문
  │
  ▼
쿼리 분석 (단순 / 복잡 판단)
  │
  ▼
검색 전략 선택 (벡터 / 키워드 / 웹 / SQL)
  │
  ▼
검색 실행
  │
  ▼
결과 평가 "충분한가?"
  ├─ 충분 → 답변 생성
  └─ 부족 → 쿼리 재작성 후 재검색 (루프)

진짜 Agentic RAG의 세 가지 조건이 있어요.

자율 전략 선택 — 언제, 어떻게, 어디서 검색할지 LLM이 스스로 결정해요. 외부 규칙이나 분류기가 대신 결정하지 않아요.

반복 실행 — 중간 결과를 보고 라운드 수를 동적으로 결정해요. 한 번으로 충분하면 한 번, 세 번이 필요하면 세 번 검색해요.

툴 기반 검색 — 검색 자체가 LLM이 호출하는 툴이에요. ReAct 패턴으로 Thought → Action(검색) → Observation(결과) 사이클을 돌아요.


핵심 전략 4가지

전략 1: 쿼리 재작성 (Query Rewriting)

사용자 쿼리를 그대로 검색하지 않아요. 검색에 최적화된 형태로 바꿔요.

원본 쿼리: "아인슈타인이 양자역학에 대해 어떻게 생각했어?"

↓ 에이전트가 재작성

검색 쿼리 1: "아인슈타인 양자역학 비판 EPR 역설"
검색 쿼리 2: "아인슈타인 보어 논쟁 코펜하겐 해석"
검색 쿼리 3: "Einstein quantum mechanics published papers"

한 질문을 여러 각도로 쪼개서 검색하는 거예요. 각 검색 결과를 합쳐서 더 완전한 답을 만들 수 있어요.

전략 2: 반복 검색 (Iterative Retrieval)

검색 결과를 평가하고, 부족하면 다시 검색해요.

def agentic_retrieval(query: str, max_iterations: int = 3):
    context = []
    iteration = 0

    while iteration < max_iterations:
        # 현재 컨텍스트로 충분한지 LLM이 판단
        assessment = llm.assess(
            query=query,
            context=context,
            prompt="현재 컨텍스트로 질문에 답할 수 있나요? 부족하다면 어떤 정보가 더 필요한가요?"
        )

        if assessment.is_sufficient:
            break

        # 부족한 정보를 채우는 새 쿼리 생성
        new_query = assessment.additional_query
        new_docs = vector_search(new_query)
        context.extend(new_docs)
        iteration += 1

    return generate_answer(query, context)

실제 실패 케이스를 보면 일반 RAG는 "정보 부족"을 못 느끼고 그냥 할루시네이션으로 답해요. Agentic RAG는 부족하다고 판단하면 재검색을 해요.

전략 3: 쿼리 라우팅 (Query Routing)

질문의 성격에 따라 어디서 검색할지 동적으로 결정해요.

tools = [
    {
        "name": "vector_search",
        "description": "의미 기반 문서 검색. 개념 설명, 관련 문서 찾기에 적합"
    },
    {
        "name": "sql_search",
        "description": "정형 데이터 조회. 매출, 통계, 날짜 기반 데이터에 적합"
    },
    {
        "name": "web_search",
        "description": "실시간 웹 검색. 최신 뉴스, 현재 가격, 오늘 날씨에 적합"
    },
    {
        "name": "code_search",
        "description": "코드베이스 검색. 함수, 클래스, 구현 방법 찾기에 적합"
    }
]

# "오늘 애플 주가는?" → web_search 선택
# "작년 4분기 매출은?" → sql_search 선택
# "벡터 검색 원리는?" → vector_search 선택
# "search 함수 어디 있어?" → code_search 선택

LlamaIndex가 auto_routed 모드에서 이 방식을 써요. 경량 에이전트가 쿼리를 분석해서 최적의 검색 모드를 선택해요.

전략 4: Self-RAG (자기 평가)

검색 결과가 실제로 유용한지 LLM이 스스로 평가해요. 세 가지를 체크해요.

검색 필요 여부 판단
  → "이 질문은 외부 검색이 필요한가, 아니면 내 지식으로 충분한가?"

검색 결과 관련성 평가
  → "이 문서가 질문과 관련이 있나? (완전 관련 / 부분 관련 / 무관)"

답변 신뢰도 평가
  → "생성된 답변이 검색 결과로 뒷받침되는가?"

관련 없는 문서를 그냥 LLM에 넣으면 오히려 답변 품질이 떨어져요. Self-RAG는 이걸 걸러냅니다.


일반 RAG vs Agentic RAG 비교

구분 일반 RAG Agentic RAG

검색 횟수 1회 고정 동적 (필요한 만큼)
쿼리 처리 원본 그대로 재작성 / 분해
검색 소스 단일 벡터 DB 멀티소스 동적 선택
결과 평가 없음 충분성 자체 평가
복잡한 질문 취약 다단계 처리 가능
비용 낮음 높음 (반복 호출)
지연 시간 빠름 느림

언제 Agentic RAG가 필요한가

모든 경우에 Agentic RAG가 좋은 건 아니에요. 비용과 지연 시간이 늘어나거든요.

일반 RAG로 충분한 경우

  • 단순 FAQ, 문서 요약
  • 응답 속도가 중요한 실시간 챗봇
  • 검색 소스가 하나인 경우

Agentic RAG가 필요한 경우

  • 다단계 추론이 필요한 복잡한 질문
  • 여러 소스를 교차 참조해야 하는 경우
  • "최신 정보 + 내부 문서 + DB 데이터"를 종합해야 하는 경우
  • 검색 품질이 답변 품질에 직결되는 도메인 (의료, 법률, 금융)

실전 구현 패턴

LangGraph로 간단한 Agentic RAG 구조를 만들면 이렇게 돼요.

from langgraph.graph import StateGraph, END
from typing import TypedDict, Annotated
from langchain_core.messages import BaseMessage

class AgenticRAGState(TypedDict):
    query: str
    context: list[str]
    iteration: int
    is_sufficient: bool
    answer: str

def retrieval_node(state: AgenticRAGState):
    # 쿼리 재작성 후 검색
    rewritten_query = rewrite_query(state["query"], state["context"])
    docs = vector_search(rewritten_query)
    return {"context": state["context"] + docs}

def assessment_node(state: AgenticRAGState):
    # 현재 컨텍스트가 충분한지 평가
    is_sufficient = assess_sufficiency(state["query"], state["context"])
    return {
        "is_sufficient": is_sufficient,
        "iteration": state["iteration"] + 1
    }

def answer_node(state: AgenticRAGState):
    answer = generate_answer(state["query"], state["context"])
    return {"answer": answer}

def should_continue(state: AgenticRAGState) -> str:
    if state["is_sufficient"] or state["iteration"] >= 3:
        return "answer"
    return "retrieve"

# 그래프 구성
workflow = StateGraph(AgenticRAGState)
workflow.add_node("retrieve", retrieval_node)
workflow.add_node("assess", assessment_node)
workflow.add_node("answer", answer_node)

workflow.set_entry_point("retrieve")
workflow.add_edge("retrieve", "assess")
workflow.add_conditional_edges("assess", should_continue, {
    "retrieve": "retrieve",
    "answer": "answer"
})
workflow.add_edge("answer", END)

app = workflow.compile()

마무리

Agentic RAG는 일반 RAG의 "한 번 검색하고 끝"이라는 한계를 극복한 구조예요.

쿼리 재작성, 반복 검색, 멀티소스 라우팅, 자기 평가를 조합해서 복잡한 질문도 처리할 수 있어요. 단 비용과 지연 시간이 늘어나기 때문에 단순한 질문에는 오버엔지니어링이에요.

"RAG는 죽지 않았다. 한 번 검색하는 파이프라인에서 스스로 검색 전략을 짜는 에이전트로 진화했을 뿐이다."

😄


 

반응형