Agentic RAG로 “스스로 찾고, 평가하고, 다시 찾는” 자율 정보검색 에이전트 만들기
Naive RAG(질문 → retrieval 1회 → 답변)는 질문이 모호하거나(“성능이 왜 느려?”), 다단계 원인 추적이 필요하거나(장애 분석), 문서가 방대하고 중복이 많은(사내 위키/코드베이스) 상황에서 쉽게 무너집니다.
들어가며
Naive RAG(질문 → retrieval 1회 → 답변)는 질문이 모호하거나(“성능이 왜 느려?”), 다단계 원인 추적이 필요하거나(장애 분석), 문서가 방대하고 중복이 많은(사내 위키/코드베이스) 상황에서 쉽게 무너집니다. 보통 “검색 쿼리가 나쁘다 → 엉뚱한 chunk가 붙는다 → 모델이 그럴듯하게 메운다(=hallucination)” 패턴이죠.
Agentic RAG는 이 병목을 “에이전트 루프”로 해결합니다. 즉, 모델이 한 번에 답을 내는 게 아니라:
- 검색이 필요한지 판단하고
- 검색했다면 근거가 충분한지(grade/evidence gating) 평가하고
- 부족하면 질문/쿼리를 rewrite해서 다시 검색하거나
- 경우에 따라 다른 tool(웹/DB/코드검색)로 경로를 바꾸는 식입니다.
언제 쓰면 좋나?
- 사용자 질문이 불완전/모호하고 clarifying 또는 재검색이 자주 필요한 경우
- 근거(출처) 기반 답변이 필수인 프로덕션(지원/보안/컴플라이언스/사내지식)
- retrieval 품질이 들쭉날쭉해서 self-correcting loop가 비용 대비 이득인 경우 (LangGraph가 이런 “상태/조건 분기”를 그래프로 다루는 튜토리얼을 제공합니다.1)
언제 쓰면 안 되나?
- FAQ 수준 단답, 검색 corpus가 작고 정제되어 Naive RAG로도 회수율/정확도가 충분할 때
- 지연(latency) 1~2초가 KPI인 UX에서, 반복 루프가 체감 성능을 망칠 때
- 운영팀이 관측성/평가/가드레일까지 같이 가져갈 준비가 없을 때(Agentic RAG는 “실패 모드”가 더 다양해집니다)
🔧 핵심 개념
주요 개념 정의
- Agentic RAG: “retrieval + generation”을 고정 파이프라인으로 두지 않고, LLM이 도구 호출(tool use)과 분기/루프를 통해 retrieval을 능동적으로 조절하는 패턴. LangGraph 문서에서도 “벡터스토어를 검색할지 vs 바로 답할지”를 결정하는 retrieval agent 패턴을 다룹니다.1
- Stateful orchestration: 현재까지의 질문, 검색 쿼리, 후보 문서, 채점 결과, 시도 횟수 등을 상태(state)로 들고 다니며 다음 스텝을 결정.
- Evidence gating / grading: 가져온 문서가 답변에 충분한 근거인지 판정하고, 불충분하면 재검색/재작성으로 되돌리는 장치(최근 LangGraph 기반 레시피/논문에서도 “evidence gating”을 핵심 구성요소로 강조).2
- Query rewrite: “원 질문”을 그대로 검색하지 않고, corpus에 맞는 형태(키워드/약어/로그 패턴/파일경로 등)로 검색용 쿼리로 변환.
- Tool calling & bounded loops: 루프/분기/병렬 호출이 필요하면 “모델-툴-모델”을 여러 번 왕복하거나, SDK가 제공하는 실행 하네스에서 제어. OpenAI Agents SDK는 tool 호출과 실행 제어(guardrails, timeouts, concurrency 등) 개념을 명시하고, bounded workflow에서 루프/분기/병렬이 유용하다고 설명합니다.3
내부 작동 방식(구조/흐름)
실무적으로는 아래 6개 블록으로 나누면 설계가 쉬워집니다.
1) Router(Need retrieval?)
- 입력 질문을 보고 “내 지식/대화 메모리만으로 답해도 되나?” vs “근거 검색이 필요한가?”를 결정
- 잘못 라우팅되면 비용 폭증 또는 근거 부족이 나옵니다. 그래서 보통 “불확실/정책/사내정보”는 retrieval 강제.
2) Planner(검색 전략)
- 단일 쿼리로 끝낼지, 서브쿼리로 쪼갤지(예: “장애 원인 + 최근 배포 + 관련 설정 변경”) 결정
- 여기서 병렬 검색(여러 인덱스/여러 retriever)도 고려.
3) Retriever tool 호출
- 벡터 검색 + (가능하면) keyword/regex/메타데이터 필터를 혼합
- “코드베이스”는 경로/모듈/커밋 메타데이터를 같이 넣어야 chunk 품질이 올라갑니다.
4) Grader(Evidence gating)
- 회수한 chunk가 질문에 답할 “직접 근거”인지, 아니면 주변 정보인지 평가
- 통과 기준을 못 맞으면 rewrite로 루프.
5) Query rewrite
- 예: “로그인 느림” → “p95 latency auth service redis timeout”처럼 corpus 용어로 재표현
- 이때 “다음 검색은 무엇을 확인해야 하나”를 상태에 남겨 재시도 비용을 줄입니다.
6) Answer synthesis
- 최종 답변은 “결론 + 근거 요약 + 인용 + 불확실성” 구조로 고정하는 게 안전합니다.
LangGraph는 이런 노드/엣지/조건분기 형태를 “그래프”로 구성하는 방식(상태, 노드, conditional edge)을 공식 튜토리얼로 제공합니다.1
그리고 “local agentic RAG workflow” 사례(예: Ollama+ChromaDB)에서도 Naive RAG의 한계를 “stateful decision loops + relevance grading + query rewrite”로 해결한다고 정리합니다.4
다른 접근과의 차이점
- Naive RAG: 단순/저렴/빠름. 하지만 질문이 나쁘면 그대로 실패.
- Re-rank 강화 RAG: retrieval 후보를 더 잘 고르는 데 집중. “재검색”은 덜 함.
- Agentic RAG: retrieval 자체를 “반복적으로 개선”하고, 필요 시 다른 도구로 갈아탐. 대신 비용/지연/운영 복잡도가 증가.
💻 실전 코드
아래 예제는 “사내 운영 Runbook + 장애 포스트모템” 문서를 가진 팀이, SRE/온콜 질문에 답하는 Agentic RAG를 만드는 현실적인 시나리오입니다.
- 벡터스토어: Chroma(로컬/온프렘)
- 오케스트레이션: LangGraph(StateGraph)
- 핵심:
retrieve → grade → (rewrite → retrieve)* → answer루프 + 최대 반복 횟수, 근거 부족 시 안전한 실패(“확실한 근거 없음”)
1) 초기 셋업
1
pip install -U langgraph langchain chromadb sentence-transformers pydantic
LLM은 조직 표준(OpenAI/로컬/사내 호스팅 등)으로 교체 가능합니다. 아래 코드는 “구조”가 핵심이라 LLM 어댑터 부분은 여러분 환경에 맞게 바꾸세요.
2) 인덱싱 + Retriever tool
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# agentic_rag_sre.py
from __future__ import annotations
import os
from typing import List, TypedDict, Optional
from pydantic import BaseModel, Field
from langchain_core.documents import Document
from langchain_core.tools import tool
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import HuggingFaceEmbeddings
from langgraph.graph import StateGraph, END
# ---------- (A) Corpus: 예시로 로컬 텍스트 파일들을 로드한다고 가정 ----------
def load_runbooks() -> List[Document]:
# 현실에서는: Confluence export, Git repo markdown, Notion dump 등
paths = [
"./docs/runbook_auth.md",
"./docs/postmortem_2026-07-redis-timeout.md",
"./docs/runbook_k8s_hpa.md",
]
docs: List[Document] = []
for p in paths:
with open(p, "r", encoding="utf-8") as f:
docs.append(Document(page_content=f.read(), metadata={"source": p}))
return docs
def build_vectorstore() -> Chroma:
raw_docs = load_runbooks()
splitter = RecursiveCharacterTextSplitter(chunk_size=900, chunk_overlap=150)
chunks = splitter.split_documents(raw_docs)
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
vs = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_sre",
collection_name="sre_runbooks",
)
return vs
VS = build_vectorstore()
RETRIEVER = VS.as_retriever(search_kwargs={"k": 6})
class RetrieveArgs(BaseModel):
query: str = Field(..., description="Search query optimized for internal SRE runbooks/postmortems")
@tool("retrieve_sre_docs", args_schema=RetrieveArgs)
def retrieve_sre_docs(query: str) -> List[dict]:
"""Retrieve internal SRE docs (runbooks/postmortems). Returns list of {content, source}."""
docs = RETRIEVER.get_relevant_documents(query)
return [{"content": d.page_content, "source": d.metadata.get("source")} for d in docs]
3) Agentic loop: grade + rewrite + answer
아래는 LangGraph로 “상태 기반” 루프를 구성합니다(노드/엣지/조건분기).1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# 계속: agentic_rag_sre.py
# ---------- (B) LLM 어댑터(여기만 여러분 모델로 교체) ----------
class SimpleLLM:
"""
데모용 인터페이스.
실전에서는 OpenAI/사내 모델 SDK로:
- structured output(grade JSON)
- temperature/timeout
- retry
를 붙이세요.
"""
def complete(self, prompt: str) -> str:
raise NotImplementedError("Plug your LLM here")
LLM = SimpleLLM()
# ---------- (C) Graph State ----------
class RAGState(TypedDict):
question: str
query: str
tries: int
retrieved: List[dict] # {content, source}
evidence_ok: bool
answer: Optional[str]
MAX_TRIES = 3
def node_seed(state: RAGState) -> RAGState:
return {
**state,
"query": state["question"],
"tries": 0,
"retrieved": [],
"evidence_ok": False,
"answer": None,
}
def node_retrieve(state: RAGState) -> RAGState:
results = retrieve_sre_docs.invoke({"query": state["query"]})
return {**state, "retrieved": results, "tries": state["tries"] + 1}
def node_grade(state: RAGState) -> RAGState:
# 핵심: “근거 충분성”을 엄격하게 본다. (없으면 rewrite로 루프)
excerpts = "\n\n".join(
[f"[{i}] source={r['source']}\n{r['content'][:700]}" for i, r in enumerate(state["retrieved"])]
)
prompt = f"""
You are an SRE assistant. Decide if the retrieved excerpts contain DIRECT evidence to answer the question.
Question:
{state['question']}
Retrieved excerpts:
{excerpts}
Return ONLY one line:
EVIDENCE_OK=true|false
"""
out = LLM.complete(prompt).strip()
ok = "true" in out.lower()
return {**state, "evidence_ok": ok}
def node_rewrite(state: RAGState) -> RAGState:
# rewrite는 “검색용 쿼리”를 만드는 작업: 서비스명/지표/에러코드/컴포넌트 후보를 넣는다.
prompt = f"""
Rewrite the question into a high-recall internal search query for SRE runbooks/postmortems.
Include likely keywords: service names, error strings, metrics (p95/p99), dependencies (redis, db), k8s terms.
Return ONLY the rewritten query.
Question: {state['question']}
Previous query: {state['query']}
"""
new_query = LLM.complete(prompt).strip()
return {**state, "query": new_query}
def node_answer(state: RAGState) -> RAGState:
citations = "\n".join([f"- {r['source']}" for r in state["retrieved"]])
context = "\n\n".join([r["content"][:1200] for r in state["retrieved"]])
prompt = f"""
You are an SRE assistant. Answer using ONLY the provided context.
If context is insufficient, say so explicitly and propose what to check next.
Question:
{state['question']}
Context:
{context}
In the final answer:
1) Give a concrete diagnosis/next steps
2) Mention key evidence snippets (paraphrase)
3) List sources
Sources:
{citations}
"""
ans = LLM.complete(prompt).strip()
return {**state, "answer": ans}
def route_after_grade(state: RAGState) -> str:
if state["evidence_ok"]:
return "answer"
if state["tries"] >= MAX_TRIES:
return "answer" # 근거 부족해도 안전하게 종료(답변에서 불충분 명시)
return "rewrite"
def build_graph():
g = StateGraph(RAGState)
g.add_node("seed", node_seed)
g.add_node("retrieve", node_retrieve)
g.add_node("grade", node_grade)
g.add_node("rewrite", node_rewrite)
g.add_node("answer", node_answer)
g.set_entry_point("seed")
g.add_edge("seed", "retrieve")
g.add_edge("retrieve", "grade")
g.add_conditional_edges("grade", route_after_grade, {"rewrite": "rewrite", "answer": "answer"})
g.add_edge("rewrite", "retrieve")
g.add_edge("answer", END)
return g.compile()
if __name__ == "__main__":
app = build_graph()
final = app.invoke({"question": "지난주부터 auth 서비스 로그인 p95가 급증했는데 redis timeout이 의심돼. 어디부터 확인?"})
print(final["answer"])
예상 출력(형태)
- 근거가 충분하면: “redis timeout 패턴 + 관련 설정/대응 runbook”을 근거로 즉시 점검 체크리스트가 나옵니다.
- 근거가 부족하면: “현재 문서로는 직접 근거가 부족”을 명시하고, 추가로 수집할 로그/메트릭/대시보드를 제안(이걸 숨기면 사고 납니다).
⚡ 실전 팁 & 함정
Best Practice (2~3개)
1) Evidence gating을 “관대하게” 두지 마세요
Agentic RAG의 목적은 “그럴듯한 답”이 아니라 “근거 기반 루프”입니다. grade 기준을 빡빡하게 두고, 부족하면 rewrite/재검색을 강제하세요. (evidence gating 자체가 패턴으로 강조됩니다.2)
2) 루프는 반드시 bounded(최대 시도/예산)로
MAX_TRIES, 토큰/시간 budget, tool timeout을 고정하세요. OpenAI Agents SDK도 tool 실행에 guardrails/timeout/concurrency 같은 운영 제어를 강조합니다.3
3) 관측성(Tracing) 없으면 운영 못 합니다
“어떤 쿼리로 검색했고”, “무슨 근거로 grade가 false였고”, “rewrite가 어떻게 변했는지”가 남아야 디버깅이 됩니다. LangGraph/LangChain 쪽은 LangSmith 기반 tracing을 공식 문서에서 안내합니다.1
흔한 함정/안티패턴
- rewrite가 원 질문을 왜곡: 검색 회수율을 올리려다 의미가 바뀌는 경우가 많습니다. 해결: rewrite에 “변경 금지 제약(핵심 엔티티/기간)”을 명시하고, 상태에 entity를 구조화해서 고정.
- retrieval 결과를 그대로 답변에 과다 주입: 컨텍스트가 길면 비용/지연이 폭증하고, 오히려 정답률이 떨어집니다. 해결: “chunk → 요약/정규화 → answer” 중간 노드를 추가.
- grade 모델과 answer 모델을 동일 프롬프트/온도로 운용: grade는 결정적/보수적으로, answer는 설명력이 필요합니다. 분리하세요.
비용/성능/안정성 트레이드오프
- 정확도 vs 지연: 루프가 늘수록 지연이 선형(혹은 그 이상)으로 증가합니다. 실무에선 “1회 검색 + rerank”로 해결되는 문제인지 먼저 보세요.
- 비용 vs 회복탄력성: 장애/보안/법무처럼 틀리면 큰일 나는 영역은 Agentic RAG가 맞고, 단순 Q&A는 과합니다.
- 안정성: tool 실패(벡터스토어 다운), 쿼리 폭주, 무한 루프를 가정하고 “fallback answer + 다음 행동 제안”을 설계해야 합니다.
🚀 마무리
Agentic RAG는 “RAG를 똑똑하게”가 아니라, 정보 검색을 자율적으로 운영 가능하게(루프/분기/평가/재시도) 만드는 패턴입니다. 2026년 기준 생태계에서는 LangGraph처럼 stateful graph orchestration으로 retrieval 루프를 명시적으로 모델링하는 방식이 널리 쓰이고, 공식 튜토리얼도 “검색 vs 즉답”, “grade”, “rewrite”, “그래프 조립” 흐름을 제공합니다.1
도입 판단 기준(실무 체크리스트):
- 질문이 자주 모호하고, 재검색/근거 검증이 실제로 필요한가?
- 정확도 향상이 “한 번 더 검색”으로 해결되는가, 아니면 루프/분기 설계가 필요한가?
- MAX_TRIES/예산/timeout/트레이싱/평가까지 운영 패키지로 가져갈 준비가 되었나?
다음 학습 추천: