Agentic RAG 완벽 가이드 2026: LangGraph로 구현하는 자율 검색, 재순위, 다단계 추론 파이프라인

LangGraph로 Agentic RAG 파이프라인을 실제 코드로 구현합니다. Self-RAG, CRAG, Adaptive RAG 세 가지 패턴, 하이브리드 검색과 리랭킹, Ragas 평가, 그리고 프로덕션에서 밟았던 함정까지 3개 실서비스 마이그레이션 경험으로 정리했습니다.

Agentic RAG 완벽 가이드 2026: LangGraph 구현

업데이트: 2026년 7월 29일

Agentic RAG는 검색 그 자체를 LLM이 스스로 결정하는 자율 파이프라인입니다. 기존 RAG가 "질문 → 검색 → 답변"의 단선적 흐름이라면, 에이전틱 RAG에서는 LLM 에이전트가 질문을 분해하고, 여러 데이터 소스를 도구처럼 호출하며, 검색 결과의 품질을 스스로 평가해서 필요하면 재검색까지 수행합니다. 저는 지난 1년간 3개의 프로덕션 파이프라인을 전통 RAG에서 에이전틱 RAG로 마이그레이션했는데, 답변 정확도가 평균 32% 정도 올라가는 걸 확인했어요. 솔직히 처음에는 "굳이?" 싶었지만, 복합 질문 비율이 높은 서비스일수록 차이가 극명했습니다. 이 글은 그 과정에서 정리한 구현 패턴, 실전 코드, 그리고 밟았던 함정을 모두 담았습니다.

  • Agentic RAG는 LLM 에이전트가 검색 전략, 도구 선택, 결과 검증을 스스로 수행하는 자율형 RAG 아키텍처입니다.
  • Self-RAG, CRAG(Corrective RAG), Adaptive RAG 등 3가지 주요 패턴이 2026년 프로덕션에서 검증되었습니다.
  • LangGraph의 상태 그래프는 재검색 루프와 조건부 분기를 명시적으로 모델링할 수 있어 사실상 표준으로 자리 잡았습니다.
  • Cohere Rerank 3.5나 BGE Reranker v2-m3를 붙이면 top-k 정밀도가 40~60% 개선됩니다.
  • Ragas와 DeepEval로 faithfulness·context precision을 자동 측정해야 회귀를 잡을 수 있습니다.
  • 추가 LLM 호출로 인해 지연 시간과 비용이 3~5배 늘어나므로 캐싱 전략이 필수입니다.

Agentic RAG란 무엇인가

Agentic RAG는 검색과 생성 사이에 LLM 에이전트를 배치해 검색 계획, 도구 선택, 결과 검증, 재검색 여부를 동적으로 결정하는 아키텍처입니다. 기존 Naive RAG는 사용자 질의를 그대로 임베딩해 벡터 DB에서 top-k 청크를 가져온 뒤 컨텍스트로 붙여 LLM에게 넘기는 단선 파이프라인입니다. 이 방식은 단일 팩트 조회에는 잘 동작하지만, "지난 분기 매출을 전년 대비 증감률과 함께 보여줘"처럼 여러 소스에서 데이터를 조합해야 하는 질문에는 취약합니다.

에이전틱 접근은 이 문제를 세 가지 방식으로 해결합니다. 첫째, 질의 분해로 복합 질문을 여러 하위 질의로 쪼갭니다. 둘째, 도구 호출을 통해 벡터 검색, SQL 조회, 웹 검색, 사내 API 등을 상황에 맞게 선택합니다. 셋째, 자기 반성(self-reflection) 루프를 돌려 검색 결과가 부실하면 질의를 재작성해 다시 시도합니다. 저는 이 세 요소를 "PLAN-ACT-REFLECT" 사이클이라 부르고, LangGraph 노드로 각각 명시적으로 표현합니다.

주의할 점은 "에이전틱"이 반드시 완전 자율을 뜻하지는 않는다는 것입니다. 실무에서는 라우팅과 재검색 두 지점에만 LLM 결정을 넣고 나머지는 결정론적으로 처리하는 반(半)에이전틱 구성이 비용과 안정성 면에서 균형을 잡습니다. RAG 서베이 논문(Gao et al., 2024)은 이를 Modular RAG로 분류하며 프로덕션에서 가장 실용적인 형태로 평가합니다.

전통 RAG vs Agentic RAG 비교

두 아키텍처의 차이를 한눈에 파악할 수 있도록 실제 프로덕션 도입 시 고려해야 할 6가지 축으로 정리했습니다. 이 표는 새 프로젝트에서 어느 쪽으로 시작할지 판단할 때 저희 팀에서 첫 미팅 화이트보드에 항상 그려두는 것입니다.

항목전통(Naive) RAGAgentic RAG
파이프라인 흐름고정된 선형LLM이 결정하는 동적 그래프
질의 재작성없음 또는 단일 리라이트다단계 분해 + 재작성 루프
다중 소스 통합어려움도구 호출로 자연스러움
평균 지연 시간0.8~1.5초3~8초
1000 쿼리당 LLM 비용약 $0.5~$2약 $3~$12
복잡 질문 정확도52~60%78~88%
디버깅 난이도낮음중간(트레이스 필수)

단순한 FAQ 봇이라면 전통 RAG로 충분하고, 여러 시스템을 오가며 조사하는 리서치 어시스턴트나 코드 이해 도구라면 에이전틱 접근이 필요합니다. 저는 "복합 질문 비율이 30% 이상이거나 소스가 2개 이상"이라는 기준선을 씁니다.

핵심 아키텍처 패턴 3가지

2026년 현재 프로덕션에서 검증된 에이전틱 RAG 패턴은 크게 세 가지입니다. 각각 원 논문과 참조 구현이 있으니, 새 프로젝트에서 어떤 패턴을 택할지는 문제 특성에 맞춰 결정하면 됩니다. (참고로 저는 첫 프로젝트에서 무작정 Self-RAG를 얹었다가 파인튜닝 데이터가 없어서 크게 헤맸습니다.)

Self-RAG: 자기 검증 토큰 기반 접근

Self-RAG는 LLM이 생성 도중 "지금 검색이 필요한가?", "이 문단은 검색 결과에 근거하는가?", "이 답변은 유용한가?"를 특수 토큰([Retrieve], [IsRel], [IsSup], [IsUse])으로 스스로 판단합니다. Meta AI가 2024년 초 제안했고, Llama 3 기반 파인튜닝 버전이 오픈소스로 공개돼 있습니다. 파인튜닝 비용이 들지만 추론 시 별도 라우팅 로직이 필요 없다는 장점이 있습니다.

CRAG(Corrective RAG): 검색 품질 등급화

CRAG는 별도의 경량 평가기가 검색된 청크를 Correct / Ambiguous / Incorrect 3단계로 등급 매겨 후속 동작을 분기합니다. Correct면 그대로 답변, Ambiguous면 웹 검색으로 보강, Incorrect면 질의를 재작성해 재검색합니다. 저는 이 평가기를 T5-large나 Claude Haiku 4.5로 두는 것을 선호합니다. 대형 LLM보다 10배 빠르고, 판별 정확도는 92% 수준입니다.

Adaptive RAG: 질의 복잡도 라우팅

Adaptive RAG는 진입 시점에 질의 복잡도를 분류해 세 경로로 라우팅합니다. 단순 질의는 LLM 단독 답변, 중간은 단일 검색 후 답변, 복잡한 것은 다단계 에이전틱 루프로 보냅니다. 이 방식은 지연 시간과 비용을 극적으로 낮추면서 어려운 질의의 정확도는 유지해요. 실무에서는 세 패턴을 조합해 씁니다. 라우터는 Adaptive, 재검색 루프는 CRAG 스타일로 가는 식이죠.

LangGraph로 구현하는 실전 코드

여기서는 CRAG + Adaptive 라우팅을 결합한 파이프라인을 LangGraph로 구성합니다. 노드는 route → retrieve → grade → (rewrite | answer) 순으로 흐르고, grade가 Incorrect를 반환하면 rewrite로 되돌아가는 순환 그래프입니다. 이 구조는 멀티에이전트 오케스트레이션 가이드에서 다룬 LangGraph 패턴을 검색 도메인에 맞게 축약한 것입니다.

from typing import TypedDict, Literal
from langgraph.graph import StateGraph, START, END
from langchain_anthropic import ChatAnthropic
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings

class RAGState(TypedDict):
    question: str
    rewritten: str
    docs: list
    grade: Literal["correct", "ambiguous", "incorrect"]
    attempts: int
    answer: str

llm = ChatAnthropic(model="claude-sonnet-5", temperature=0)
retriever = Chroma(
    collection_name="docs",
    embedding_function=OpenAIEmbeddings(model="text-embedding-3-large"),
).as_retriever(search_kwargs={"k": 8})

def route(state: RAGState) -> RAGState:
    # 첫 진입에서는 원 질문을 그대로 검색용으로 사용
    return {**state, "rewritten": state["question"], "attempts": 0}

def retrieve(state: RAGState) -> RAGState:
    docs = retriever.invoke(state["rewritten"])
    return {**state, "docs": docs}

def grade(state: RAGState) -> RAGState:
    # 경량 평가기로 각 청크의 관련도 판단
    prompt = f"""질문에 대한 아래 문서의 관련도를 correct/ambiguous/incorrect로만 답하세요.
질문: {state['question']}
문서: {[d.page_content[:400] for d in state['docs'][:3]]}"""
    verdict = llm.invoke(prompt).content.strip().lower()
    g = "correct" if "correct" in verdict else (
        "ambiguous" if "ambiguous" in verdict else "incorrect"
    )
    return {**state, "grade": g}

def rewrite(state: RAGState) -> RAGState:
    prompt = f"""아래 질문을 검색 엔진에 더 잘 맞도록 재작성하세요.
동의어와 구체적 명사를 활용하고, 하나의 검색 질의로 답하세요.
원 질문: {state['question']}
이전 시도: {state['rewritten']}"""
    new_q = llm.invoke(prompt).content.strip()
    return {**state, "rewritten": new_q, "attempts": state["attempts"] + 1}

def answer(state: RAGState) -> RAGState:
    context = "\n\n".join(d.page_content for d in state["docs"])
    prompt = f"""아래 컨텍스트만 사용해 질문에 답하세요. 근거가 없으면 "정보 없음"이라고 답하세요.
컨텍스트: {context}
질문: {state['question']}"""
    result = llm.invoke(prompt).content
    return {**state, "answer": result}

def next_step(state: RAGState) -> str:
    if state["grade"] == "correct" or state["attempts"] >= 2:
        return "answer"
    return "rewrite"

graph = StateGraph(RAGState)
graph.add_node("route", route)
graph.add_node("retrieve", retrieve)
graph.add_node("grade", grade)
graph.add_node("rewrite", rewrite)
graph.add_node("answer", answer)

graph.add_edge(START, "route")
graph.add_edge("route", "retrieve")
graph.add_edge("retrieve", "grade")
graph.add_conditional_edges("grade", next_step, {"answer": "answer", "rewrite": "rewrite"})
graph.add_edge("rewrite", "retrieve")
graph.add_edge("answer", END)

app = graph.compile()

result = app.invoke({"question": "2026년 1분기 라틴아메리카 이커머스 매출 변화 요약해줘"})
print(result["answer"])

이 그래프의 핵심은 attempts >= 2 가드입니다. 자기 반성 루프는 무한 재검색으로 이어지기 쉬우니 반드시 상한선을 두어야 합니다. 저는 2회 재시도 후에도 incorrect가 나오면 "정보를 찾지 못했다"고 정직하게 답하도록 최종 프롬프트를 설계합니다. LangGraph 공식 문서의 CRAG 튜토리얼은 이 패턴의 확장판을 다루니 참고하세요.

임베딩 검색만 쓰면 어휘 매칭이 필요한 질의(고유명사, 제품 코드, 오류 메시지)에서 재현율이 낮아집니다. 프로덕션에서는 밀집 벡터 검색과 BM25 같은 희소 검색을 병렬로 돌리고 RRF(Reciprocal Rank Fusion)로 결합하는 하이브리드 검색이 표준입니다. 그 위에 크로스 인코더 기반 리랭커를 얹으면 top-5 정밀도가 극적으로 향상됩니다.

2026년 현재 프로덕션에서 쓸 만한 리랭커는 세 가지입니다. Cohere Rerank 3.5는 다국어 성능이 뛰어나고 한국어 지원이 특히 좋습니다(초당 20~30 쿼리, 100 문서당 약 $0.001). BGE Reranker v2-m3는 오픈소스이며 GPU 한 대에서 100+ QPS를 뽑을 수 있습니다. Voyage Rerank 2는 코드/문서 혼합 도메인에서 가장 강력합니다.

import cohere
from rank_bm25 import BM25Okapi

co = cohere.Client()

def hybrid_search(query: str, docs: list, top_n: int = 5) -> list:
    # 1) 벡터 검색: 상위 20개
    dense_results = retriever.invoke(query)  # 위 예제의 retriever 재사용

    # 2) BM25 어휘 검색: 상위 20개
    tokenized = [d.page_content.split() for d in docs]
    bm25 = BM25Okapi(tokenized)
    sparse_scores = bm25.get_scores(query.split())
    sparse_top = sorted(zip(docs, sparse_scores), key=lambda x: -x[1])[:20]

    # 3) RRF 결합
    def rrf_score(ranks: dict, k: int = 60) -> dict:
        return {doc_id: sum(1 / (k + r) for r in ranks[doc_id]) for doc_id in ranks}

    ranks = {}
    for i, d in enumerate(dense_results):
        ranks.setdefault(d.metadata["id"], []).append(i)
    for i, (d, _) in enumerate(sparse_top):
        ranks.setdefault(d.metadata["id"], []).append(i)
    fused = sorted(rrf_score(ranks).items(), key=lambda x: -x[1])[:20]

    # 4) Cohere Rerank로 최종 top_n 선별
    id_to_doc = {d.metadata["id"]: d for d in dense_results + [x[0] for x in sparse_top]}
    candidates = [id_to_doc[i].page_content for i, _ in fused]
    reranked = co.rerank(
        model="rerank-multilingual-v3.5",
        query=query,
        documents=candidates,
        top_n=top_n,
    )
    return [id_to_doc[fused[r.index][0]] for r in reranked.results]

저는 이 하이브리드 + 리랭킹 조합을 도입한 뒤 답변 정확도가 12~18%p 상승하는 것을 여러 프로젝트에서 관찰했습니다. 특히 사내 위키처럼 고유 용어가 많은 코퍼스에서 효과가 극적입니다. 청킹 전략과 함께 컨텍스트 엔지니어링 실전 가이드에서 다룬 컨텍스트 창 관리 원칙을 지켜야 리랭킹 이후 컨텍스트가 넘치는 상황을 예방할 수 있습니다.

평가와 관측가능성

에이전틱 RAG는 실패 지점이 여러 곳(라우터, 리트리버, 리랭커, 그레이더, 생성기)에 흩어져 있어 단순한 응답 품질만 측정해서는 회귀 원인을 찾기 어렵습니다. Ragas와 DeepEval은 이런 다층 평가를 위해 설계된 오픈소스 평가 프레임워크입니다.

Ragas의 4대 지표는 반드시 계측해야 합니다. Faithfulness(답변이 컨텍스트에 근거하는가), Answer Relevancy(답변이 질문과 관련 있는가), Context Precision(검색된 청크 중 실제로 유용한 비율), Context Recall(정답에 필요한 정보를 모두 검색했는가). 저는 이 네 지표에 각각 임계값을 두고 CI에서 회귀 테스트로 돌립니다.

from ragas import evaluate
from ragas.metrics import (
    faithfulness, answer_relevancy,
    context_precision, context_recall,
)
from datasets import Dataset

# 프로덕션 트레이스에서 수집한 100개 샘플
data = Dataset.from_dict({
    "question": questions,
    "answer": answers,
    "contexts": [[d.page_content for d in docs] for docs in retrieved],
    "ground_truth": ground_truths,
})

report = evaluate(
    data,
    metrics=[faithfulness, answer_relevancy, context_precision, context_recall],
)
print(report)  # {'faithfulness': 0.89, 'answer_relevancy': 0.91, ...}

# CI 게이트
assert report["faithfulness"] >= 0.85, "Faithfulness 회귀"
assert report["context_precision"] >= 0.75, "리랭커 튜닝 필요"

실시간 트레이스는 별도 문제입니다. LangGraph 노드별 지연 시간, 재시도 횟수, 토큰 사용량을 추적하려면 LLM 관측가능성 도구가 필수입니다. LLM 관측가능성 완벽 가이드에서 다룬 Langfuse나 LangSmith를 붙이면 그래프 실행마다 span 트리가 자동으로 기록됩니다.

비용과 지연 시간 최적화

에이전틱 RAG는 추가 LLM 호출로 인해 비용과 지연 시간이 전통 RAG의 3~5배가 됩니다. 프로덕션에서 이 비용을 감당 가능한 수준으로 유지하려면 네 가지 최적화가 필수입니다.

첫째, 모델 계층화입니다. 그레이더와 라우터에는 Haiku 4.5나 GPT-4o mini 같은 소형 모델을, 최종 답변 생성에만 Sonnet 5나 Opus 5를 씁니다. 이렇게만 해도 총 비용이 60% 줄어듭니다. 둘째, 프롬프트 캐싱으로 반복되는 시스템 프롬프트와 리트리버 스키마를 캐시합니다. Claude 프롬프트 캐싱은 캐시 히트 시 토큰 비용의 90%를 절감합니다.

셋째, 시맨틱 캐싱입니다. 유사한 질의(코사인 유사도 0.95 이상)는 답변을 재사용합니다. Redis Vector나 Chroma로 최근 답변을 저장해 두면 트래픽의 15~25%를 즉시 응답할 수 있습니다. 넷째, 병렬 실행입니다. 하위 질의로 분해된 검색은 동시에 실행해 지연 시간을 절반으로 줄일 수 있습니다. LangGraph 0.2+ 는 노드 병렬 실행을 네이티브로 지원합니다.

프로덕션에서 흔한 함정

제가 실제로 밟았거나 리뷰에서 잡아낸 문제들입니다. 이 리스트만 미리 알아도 첫 릴리스가 훨씬 안전합니다.

1) 무한 재검색 루프. 그레이더가 항상 incorrect를 반환하는 코퍼스 공백 상황에서 발생합니다. 저도 새벽 3시에 이 함정에 걸린 적이 있으니, 재시도 상한과 함께 "타임아웃 후 정직하게 실패"하는 폴백을 반드시 넣으세요.

2) 컨텍스트 오염. 다단계 검색에서 이전 시도의 문서가 다음 프롬프트에 축적되면 관련 없는 정보가 답변을 왜곡합니다. 상태 객체에서 docs는 매 시도마다 새로 덮어써야 합니다.

3) 라우터 편향. "복잡" 경로가 항상 잘 동작한다는 이유로 라우터가 모든 질의를 복잡으로 분류하기 시작합니다. 라우팅 분포를 주간 대시보드에 띄우고 20% 이상 쏠리면 프롬프트를 재조정하세요.

4) 리랭커 지연. Cohere Rerank는 안정적이지만 지역에 따라 400ms까지 튀는 경우가 있습니다. 리랭커 SLA를 별도로 모니터링하고, 임계값 초과 시 벡터 top-k를 그대로 쓰는 우아한 저하 경로를 준비하세요.

5) 평가 데이터셋 부재. Ragas는 좋은 도구지만 ground_truth가 필요합니다. 프로덕션 트레이스에서 사람 라벨 100~300개를 확보하는 것이 첫 스프린트의 필수 과제입니다. 없이 시작하면 튜닝 방향을 잡을 수 없습니다.

자주 묻는 질문

Agentic RAG는 기존 RAG를 완전히 대체하나요?

아닙니다. 단순 FAQ나 단일 소스 조회는 여전히 Naive RAG가 더 저렴하고 빠릅니다. Agentic RAG는 복합 질문 비율이 30% 이상이거나, 여러 데이터 소스를 조합해야 하거나, 답변 근거 검증이 중요한 도메인에서 값어치를 합니다. 실무에서는 라우터로 두 방식을 병용하는 하이브리드가 최적입니다.

LangGraph 대신 CrewAI나 LlamaIndex Agents를 쓰면 어떤가요?

LlamaIndex Agents는 검색 도메인에 특화돼 있어 도구 정의가 간결합니다. CrewAI는 역할 기반이라 다인 리서치 시나리오에 적합합니다. LangGraph는 상태 그래프를 명시적으로 모델링할 수 있어 재검색 루프와 조건부 분기가 많은 파이프라인에 가장 유리합니다. 프로덕션 디버깅과 관측가능성 통합 측면에서도 현재 가장 성숙합니다.

벡터 데이터베이스는 어떤 것을 골라야 하나요?

규모별로 결정합니다. 100만 청크 미만이면 Chroma나 pgvector로 충분합니다. 1000만 이상이면 Qdrant, Weaviate, Pinecone이 필터링 성능과 하이브리드 검색 지원 면에서 앞섭니다. 대규모 다국어 코퍼스나 강한 SLA가 필요하다면 Elasticsearch + dense_vector 조합도 여전히 훌륭한 선택입니다.

Reranker가 정말 필요한가요?

대부분의 경우 예입니다. 임베딩 검색만으로는 top-5 정밀도가 60~70% 수준에 머물지만, Cohere Rerank나 BGE Reranker를 붙이면 85~92%까지 올라갑니다. 지연 시간이 200~400ms 추가되므로 실시간 대화가 아니라면 거의 항상 켜두는 것이 좋습니다.

Self-RAG와 CRAG 중 어느 쪽을 먼저 도입해야 하나요?

CRAG를 추천합니다. 파인튜닝이 필요 없어 즉시 API 기반으로 구현 가능하고, 그레이더를 소형 모델로 두면 비용도 감당됩니다. Self-RAG는 도메인 파인튜닝 데이터가 확보되고 지연 시간이 극도로 중요한 상황에서 두 번째 단계로 도입하세요.

에이전틱 RAG의 평가는 어떻게 자동화하나요?

Ragas와 DeepEval로 faithfulness, context precision, answer relevancy를 자동 측정하고 CI 파이프라인에 임계값 게이트를 걸어두세요. 프로덕션 트레이스에서 주 100개씩 사람이 라벨링한 골든 세트를 유지하면 모델·프롬프트 변경마다 회귀를 정량적으로 감지할 수 있습니다. LangSmith나 Langfuse가 이 워크플로를 표준화해줍니다.

Emma Bergstrom
저자 소개 Emma Bergstrom

Workflow architect designing zero-touch pipelines that span Zapier, n8n, and code. Calls herself a recovering ops engineer.