Joyuno's Dev Blog

자율적으로 “찾고-검증하고-다시 찾는” Agentic RAG 구현법 (2026년 6월 실전 패턴)

AI Agent

들어가며

전통적인 RAG는 보통 (1) 한 번 검색 → (2) 컨텍스트 주입 → (3) 답변 생성으로 끝납니다. 하지만 실무 질문은 대개 “검색 쿼리를 어떻게 쪼개야 하지?”, “지금 가져온 근거가 부족한데?”, “서로 충돌하는 문서가 있는데?” 같은 후속 의사결정이 필요합니다. 이 지점에서 Agentic RAG(=RAG를 tool call로 만들고, 모델이 반복/수정/중단을 결정하는 구조)가 힘을 발휘합니다. LangGraph 기반으로 iterative retrieval + self-correction + answer grading을 그래프 루프로 엮는 패턴이 2026년 상반기 실전 레퍼런스로 많이 공유되고 있습니다. (docs.langchain.com)

언제 쓰면 좋은가

  • 질문이 모호하거나(“최근 정책 변경 요약해줘”), 검색 결과 품질 편차가 크고 재질의/재탐색이 자주 필요한 도메인
  • “근거 충분성(evidence sufficiency)”이나 “충돌 해결”처럼 답을 만들기 전의 검증 루프가 필요한 경우(기술 문서/규정/법무/금융/헬스케어) (arxiv.org)
  • 운영 단계에서 관측/트레이싱/평가를 붙여 “싼 경로(단발 RAG)”와 “비싼 경로(Agentic)”를 라우팅하고 싶은 경우 (callsphere.ai)

언제 쓰면 안 되는가

  • FAQ처럼 검색 난이도가 낮고 정답 근거가 명확한 케이스(Agentic 루프가 비용만 증가)
  • SLA가 빡빡한 온라인 트래픽에서 “최악의 경우 반복 3~5회” 같은 변동성을 감당 못 하는 경우
  • 보안/거버넌스가 준비되지 않은 상태에서 툴을 마구 붙이는 경우(자율 에이전트는 공격 표면이 급증한다는 경고가 2026년에 강하게 나옵니다) (techradar.com)

🔧 핵심 개념

주요 개념 정의

  • Agentic RAG: retrieval을 “전처리 단계”가 아니라 에이전트가 호출하는 Tool로 노출하고, 검색→판정→재검색→정리→답변을 반복 루프로 구성하는 RAG 패턴 (detached-node.dev)
  • Self-correction / Query rewrite: 검색 결과가 빈약하거나 엉뚱하면, 에이전트가 쿼리를 재작성해 재시도 (callsphere.ai)
  • Retrieval grading / Evidence sufficiency: “가져온 근거로 답이 가능한가?”를 점수화/판정하고 부족하면 루프를 계속(최근 연구에선 evidence sufficiency를 정교하게 스코어링하는 프레임도 제안) (arxiv.org)
  • Bounded autonomy(자율성 경계): 모델이 무한 루프를 돌지 않도록 max_iterations, timeout, budget을 런타임이 강제
  • Hosted retrieval vs Bring-your-own retrieval: OpenAI Responses API의 file_search처럼 “벡터스토어+검색을 호스팅”에 맡길지, Qdrant/pgvector/Elastic 등 “직접 운영”할지 (platform.openai.com)

내부 작동 방식(구조/흐름)

2026년 실전에서 많이 쓰는 구조는 “그래프 기반 상태 머신”입니다(LangGraph가 대표적). 핵심은 노드 간 전이가 ‘모델 판단’ + ‘하드 가드레일’의 합으로 결정된다는 점입니다. (docs.langchain.com)

대표 플로우(단일 에이전트, iterative retrieval):

1) Planner(옵션): 질문을 sub-queries로 분해하거나, 어떤 retriever를 쓸지(내부 KB vs 웹) 결정
2) Retrieve(tool): vector search / keyword search / hybrid search 실행
3) Grade Retrieval: (a) 관련성, (b) 다양성, (c) 최신성, (d) 상충 여부 등을 평가
4) Rewrite Query: 부족하면 쿼리 수정 후 2)로 회귀
5) Synthesize Answer: 근거를 인용/요약하며 답변 생성
6) Answer Check: 근거 대비 주장 과대/환각 여부를 간단히 재평가 후 반환

여기서 중요한 차별점은 “retrieve-then-generate”가 아니라 retrieve가 ‘행동(act)’이 되어 ReAct 루프에 들어간다는 겁니다. (detached-node.dev)
또한 최신 연구들은 retrieval 인터페이스 자체를 계층화하거나(A-RAG), 그래프/인용 검증 루프까지 포함(TechGraphRAG)하는 방향으로 확장 중입니다. (arxiv.org)

다른 접근과의 차이점

  • Vanilla RAG: 단발성이라 싸고 단순하지만, 실패 시 “그냥 틀린 답”이 나오기 쉬움
  • Multi-query RAG(비에이전트): 여러 쿼리로 한 번에 많이 가져오지만, 여전히 “부족하면 재시도” 같은 동적 제어가 약함
  • Agentic RAG: 성공률이 올라가지만(특히 어려운 질문), 토큰/지연/보안 리스크가 증가하며 운영 난이도가 높음. 엔터프라이즈가 파일럿에서 못 넘어가는 이유로 운영성/ROI가 자주 지목됩니다. (itpro.com)

💻 실전 코드

아래 예시는 “사내 기술문서 KB + 장애 티켓 요약” 같은 현실 시나리오를 가정합니다.

  • 목표: “장애 현상”을 입력하면
    1) KB에서 관련 Runbook/ADR을 찾고
    2) 부족하면 쿼리를 재작성해 재검색하고
    3) 충분한 근거가 모이면 “원인/조치/검증 체크리스트” 형태로 답을 생성
  • 스택: LangGraph(오케스트레이션) + OpenAI Responses API의 file_search(호스팅 retrieval)
    • file_search는 벡터스토어에 업로드된 파일을 대상으로 검색하고, 결과를 모델 컨텍스트에 주입하는 managed primitive로 소개됩니다. (platform.openai.com)
    • LangGraph는 “retriever tool을 언제 쓸지/루프를 어떻게 돌지”를 그래프로 구성하는 문서를 제공합니다. (docs.langchain.com)

1) 셋업 (의존성/환경)

python -m venv .venv
source .venv/bin/activate

pip install -U langgraph langchain-openai langchain-core pydantic python-dotenv
export OPENAI_API_KEY="..."
# 사전에 OpenAI Vector Store를 만들고 문서(런북/ADR/티켓 가이드)를 업로드해둔다.
export OPENAI_VECTOR_STORE_ID="vs_..."

2) 그래프: retrieve → grade → (rewrite|answer)

# agentic_rag_incident.py
from __future__ import annotations

import os
from typing import List, Literal, Optional, TypedDict

from pydantic import BaseModel, Field

from langgraph.graph import StateGraph, END
from langchain_openai import ChatOpenAI

# 주의: 아래 코드는 "구조"가 핵심이며,
# OpenAI file_search tool 연결은 프로젝트에 맞게 감싸는 형태로 쓰는 걸 권장합니다.
# (Responses API의 file_search 개념/설정은 공식 가이드를 참고)  ([platform.openai.com](https://platform.openai.com/docs/guides/tools-file-search/?utm_source=openai))


class RetrievedChunk(BaseModel):
    doc_id: str
    title: str
    snippet: str


class AgentState(TypedDict):
    incident: str
    query: str
    iter: int
    retrieved: List[RetrievedChunk]
    decision: Optional[str]
    answer: Optional[str]


class RetrievalGrade(BaseModel):
    verdict: Literal["good", "bad"] = Field(..., description="검색 결과가 답변에 충분하면 good")
    reason: str
    improved_query: Optional[str] = Field(None, description="bad일 때 재검색용 쿼리")


llm = ChatOpenAI(model="gpt-4.1-mini", temperature=0)

VECTOR_STORE_ID = os.environ["OPENAI_VECTOR_STORE_ID"]


def file_search(query: str, k: int = 6) -> List[RetrievedChunk]:
    """
    실제 운영에서는 OpenAI Responses API의 file_search를 호출해 결과를 받아오세요.
    file_search는 벡터스토어 기반 검색을 managed로 제공하는 방식입니다. ([platform.openai.com](https://platform.openai.com/docs/guides/tools-file-search/?utm_source=openai))
    여기서는 '도메인 어댑터' 위치만 보여주기 위해 더미 형태로 둡니다.
    """
    raise NotImplementedError("Connect to OpenAI Responses API file_search here.")


def node_retrieve(state: AgentState) -> AgentState:
    chunks = file_search(state["query"], k=6)
    state["retrieved"] = chunks
    state["iter"] += 1
    return state


def node_grade(state: AgentState) -> AgentState:
    # retrieved를 너무 길게 넣지 말고 "제목+스니펫" 위주로.
    context = "\n\n".join(
        [f"[{c.doc_id}] {c.title}\n{c.snippet}" for c in state["retrieved"]]
    )[:8000]

    prompt = f"""
You are grading whether the retrieved context is sufficient to answer an incident question.

Incident:
{state["incident"]}

Search query used:
{state["query"]}

Retrieved context:
{context}

Return JSON with:
- verdict: good|bad
- reason
- improved_query (only if bad): rewrite query with missing constraints, components, error codes, and synonyms.
"""
    grade = llm.with_structured_output(RetrievalGrade).invoke(prompt)
    state["decision"] = grade.verdict
    if grade.verdict == "bad" and grade.improved_query:
        state["query"] = grade.improved_query
    return state


def node_answer(state: AgentState) -> AgentState:
    context = "\n\n".join(
        [f"[{c.doc_id}] {c.title}\n{c.snippet}" for c in state["retrieved"]]
    )[:12000]

    prompt = f"""
You are an SRE assistant. Answer using ONLY the retrieved context.
If context is insufficient, say what is missing and propose next retrieval targets.

Incident:
{state["incident"]}

Context:
{context}

Write:
1) Likely root cause (with doc refs)
2) Immediate mitigation steps
3) Verification checklist
4) What evidence is missing (if any)
"""
    state["answer"] = llm.invoke(prompt).content
    return state


def route_after_grade(state: AgentState) -> str:
    # 하드 가드레일: 최대 3회까지만 루프
    if state["decision"] == "good":
        return "answer"
    if state["iter"] >= 3:
        return "answer"
    return "retrieve"


def build_graph():
    g = StateGraph(AgentState)
    g.add_node("retrieve", node_retrieve)
    g.add_node("grade", node_grade)
    g.add_node("answer", node_answer)

    g.set_entry_point("retrieve")
    g.add_edge("retrieve", "grade")
    g.add_conditional_edges("grade", route_after_grade, {
        "retrieve": "retrieve",
        "answer": "answer",
    })
    g.add_edge("answer", END)
    return g.compile()


if __name__ == "__main__":
    app = build_graph()

    incident = (
        "배포 이후 결제 API에서 502가 급증했고, 로그에 upstream reset이 보입니다. "
        "Envoy/Ingress 경로에서 발생하는 것 같고, 특정 리전에서만 재현됩니다."
    )

    init: AgentState = {
        "incident": incident,
        "query": "payment api 502 upstream reset envoy ingress regional",
        "iter": 0,
        "retrieved": [],
        "decision": None,
        "answer": None,
    }

    out = app.invoke(init)
    print(out["answer"])

예상 출력(형태)

  • 근거 문서 ID를 인용하며 “가능성 높은 원인 2~3개 + 즉시 조치 + 검증 체크리스트”
  • 3회 이내로 근거가 부족하면 “부족한 증거(예: Envoy upstream cluster 설정/배포 diff/특정 리전 LB 로그)”를 명시하고, 다음 검색 타깃을 제안

3) 확장: “cheap path vs agentic path” 라우팅

실무에서는 모든 요청을 Agentic 루프로 보내면 비용이 터집니다. LangGraph 기반 사례에서도 cheap path(단발 RAG)와 agentic path(반복/검증)를 나눠 라우팅하며 비용 분석을 강조합니다. (callsphere.ai)
간단한 휴리스틱은:

  • 질문 길이/불확실성(“같은데/같습니다/아마”) + 과거 실패율 + 도메인(규정/보안/법무)을 feature로 점수화
  • 점수 낮으면 단발 RAG, 높으면 Agentic

⚡ 실전 팁 & 함정

Best Practice

1) 자율성 예산을 코드로 강제하라

  • max_iterations, timeout, max_tool_calls, token_budget를 런타임에서 하드하게 제한
  • 자율 에이전트는 운영 관점에서 “무한 루프/비용 폭주”가 제일 먼저 터집니다.

2) Retrieval 품질을 “점수/판정”으로 외부화하라

  • “그럴듯한 답변”이 아니라, 근거 충분성을 먼저 통과시키는 게 Agentic RAG의 핵심입니다. 최근 TechGraphRAG처럼 evidence sufficiency 스코어링을 체계화하는 시도도 이 흐름입니다. (arxiv.org)

3) 관측(Observability)을 처음부터

  • 최소한: 각 iteration의 query, top-k 문서, grade verdict, 최종 답변, 비용/지연을 로그로 남기세요.
  • 에이전트는 “디버깅 불가능하면 운영 불가능”입니다(파일럿에서 못 넘어가는 원인 중 하나). (itpro.com)

흔한 함정/안티패턴

  • 에이전트에게 ‘판단’을 다 맡김: “좋을 때까지 찾아”는 위험합니다. 종료 조건을 런타임이 가져가야 합니다.
  • 컨텍스트 무한 비대화: iteration이 늘수록 retrieved를 누적하면 토큰이 폭발합니다. “요약/압축/중복 제거” 전략이 필요하며, 커뮤니티에서도 context compression을 주요 개선점으로 공유합니다. (reddit.com)
  • 보안 없이 툴 확장: MCP/내부 API/사내 위키를 도구로 붙일수록 공격 표면이 늘고, 프롬프트 인젝션/데이터 유출 리스크가 커집니다. 2026년엔 “자율 에이전트가 보안 위기”라는 톤의 경고가 뚜렷합니다. (techradar.com)

비용/성능/안정성 트레이드오프

  • 비용: (iteration 수) × (retrieval + grade + answer)로 선형 증가
  • 지연: 검색이 네트워크/스토리지 지연을 포함하므로 tail latency가 커짐
  • 안정성: 성공률은 올라가지만, 실패 모드가 복잡(잘못된 재질의/잘못된 중단/근거 과신)
  • 따라서 “항상 Agentic”가 아니라, 라우팅 + 예산 + 관측 + 평가가 한 세트입니다. (callsphere.ai)

🚀 마무리

2026년 6월 기준 Agentic RAG의 실전 구현은 크게 한 줄로 요약됩니다:

  • RAG를 ‘파이프라인’이 아니라 ‘에이전트가 쓰는 Tool’로 격상하고,
  • LangGraph 같은 state machine/graph로 retrieve→grade→rewrite→answer 루프를 만들며, (docs.langchain.com)
  • max_iterations/budget 같은 bounded autonomy로 운영 가능한 형태로 “가둬서” 쓰는 것.

도입 판단 기준(현실적인 체크리스트):

  • (필수) 단발 RAG 실패율이 눈에 띄게 높다 / 재질의 니즈가 반복된다
  • (필수) 비용·지연 예산과 관측 체계를 갖췄다(트레이싱/로그/평가)
  • (권장) “cheap path vs agentic path” 라우팅으로 ROI를 관리할 수 있다 (callsphere.ai)

다음 학습 추천:

  • LangGraph의 agentic RAG 공식 가이드로 그래프 패턴을 몸에 익히고 (docs.langchain.com)
  • OpenAI Responses API의 file_search/hosted retrieval 옵션을 비교해 “직접 운영 vs 매니지드” 결정을 하며 (platform.openai.com)
  • 연구 트렌드는 A-RAG/TechGraphRAG처럼 “retrieval 인터페이스 계층화 + 증거 충분성/인용 검증”으로 확장되는 방향을 훑어보면 좋습니다. (arxiv.org)