RAG 성능 고도화 — Self-RAG: 스스로 검열하는 RAG
이 글은 Self-RAG를 정리한 기록이다. 검색·생성·평가의 각 단계에 LLM이 개입해 스스로 검열하는 구조를, 라이브러리 없이 작은 루프로 직접 구현해보며 짚는다. 책과 Gemini의 도움을 빌려 궁금증을 해소해가며 쌓았다.
일반 RAG의 맹점 — 관련 없어도 일단 답을 만든다
검색 알고리즘에 리랭킹까지 얹으면 RAG 성능은 확실히 올라간다. 거기까지는 앞에서 정리한 그대로다. 그런데 이 모든 걸 다 적용해도 끝까지 남는 구조적인 약점이 하나 있다. 전통적인 RAG는 사용자가 문서와 아무 상관 없는 질문을 던져도, 어떻게든 뭔가를 찾아서 답을 만들어내려고 한다는 점이다.
일단 가장 비슷해 보이는 문서를 끌어오고, 생성 모델은 그 엉뚱한 자료를 재료 삼아 그럴듯한 문장을 짜낸다. 검색해온 정보를 제대로 활용하지 못하거나, 답을 만드는 동안 원래 질문의 맥락을 잃어버리는 경우도 흔하다. 결국 이 모든 길이 한 곳으로 모인다. '할루시네이션', 즉 모델이 사실처럼 지어낸 거짓 답변이다.
그러니까 검색을 더 잘하게 만드는 것만으로는 부족하다.
- 검색이 필요한지 아닌지를 먼저 판단하고
- 가져온 자료가 쓸 만한지 따지고
- 자기가 쓴 답이 근거가 있는지까지 스스로 점검하는 단계가 있어야 한다.
그 발상을 시스템으로 구현한 게 Self-RAG다.
Self-RAG — 모델이 자기 작업을 검열하게 만든다
Self-RAG(Self-Reflective Retrieval-Augmented Generation)는 이름 그대로 '자기 반성적' RAG다. 핵심은 LLM이 RAG의 각 단계에 직접 개입해서 정교하게 제어한다는 데 있다. 그냥 검색해서 답하는 게 아니라, 단계마다 모델이 특수한 토큰을 뱉어 흐름을 스스로 조종한다. 검색이 필요한지 아닌지를 판단하고(조건), 검색해온 정보가 쓸 만한지 평가하고(도구), 최종 응답의 품질까지 관리한다.
이렇게 하면 무엇이 달라지는가. 필요할 때만 문서를 검색하고, 가져온 정보를 검증한 뒤에야 답을 만들기 때문에, 앞서 말한 '관련 없어도 억지로 답하는' 문제가 근본부터 줄어든다. 신뢰할 수 있는 답변만 사용자에게 내보낼 수 있게 되는 것이다.
전체 동작은 크게 세 단계로 굴러간다. 검색, 생성, 평가다. 아래 그림이 그 흐름을 한눈에 보여준다.
이제 세 단계를 하나씩 풀어보겠다.
1단계 검색 — "이걸 굳이 찾아봐야 하나?"부터 묻는다
Self-RAG의 검색 단계는 단순히 데이터를 가져오는 일이 아니다. 그 앞에 '지능적인 판단'이 한 겹 깔린다. 일반적인 상황에서 RAG는 질문이 들어오면 묻지도 따지지도 않고 DB를 뒤지지만, Self-RAG는 그 전에 "내가 이걸 검색해야 할까?"를 먼저 고민한다.
이 판단을 가능하게 하는 게 일명 반성 토큰(Reflection Tokens)이다. 모델이 학습 단계에서 익히는 특수 토큰들인데, 그중 검색 여부를 결정하는 첫 번째 토큰이 [Retrieve]다. 모델은 들어온 질문을 보고 "내 내부 지식만으로 충분한가?"를 스스로 평가한 뒤, 셋 중 하나를 뱉는다. 외부 지식이 꼭 필요하면 [Retrieve]로 검색을 요청하고, 상식 수준이면 [No Retrieval]로 바로 답하며, 흐름상 검색 없이 이어가도 되면 [Continue]를 낸다.
이 분석 단계가 왜 중요할까. 모든 질문에 무조건 검색을 거는 건 비효율적인 데다 위험하기 때문이다. "안녕?", "오늘 날씨 어때?" 같은 단순한 대화에 굳이 어려운 기술 문서를 끌어다 붙이면 오히려 답변이 꼬인다. 불필요한 노이즈가 환각을 유발하는 것이다. 게다가 임베딩과 벡터 검색은 앞서 봤듯 자원을 잡아먹는 작업이라, 꼭 필요할 때만 검색하면 그만큼 추론 속도도 빨라지고 비용도 아낀다.
비유하자면 이렇다. 일반 RAG는 오픈북 시험에서 모든 문제를 일일이 교과서에서 찾아보는 학생이다. 아는 문제까지 굳이 펼쳐본다. 반면 Self-RAG는 아는 문제는 바로 풀고, 모르는 문제만 표시해뒀다가 나중에 사전을 찾는 똑똑한 학생이다.
여기서 한 가지 자연스러운 의문이 생긴다. 검색이 필요한지 아닌지를 가르는 이 판단을, 프롬프트로 LLM에게 시키는 것인지, 아니면 Self-RAG 전용 모델이 따로 있는 것인지 하는 점이다.
전용 모델이냐, 프롬프트냐 — 두 갈래 길
결론부터 말하면 둘 다 가능하다. 다만 원래 논문에서의 Self-RAG는 특정하게 훈련된 '전용 모델'을 쓰는 방식이다. selfrag/selfrag-llama-7b 같은 모델이 그것인데, 답변을 생성하기 전에 스스로 [Retrieve]라는 토큰을 뱉도록 아예 훈련되어 있다. 모델의 '뇌' 자체가 검색 타이밍을 알고 있는 셈이라, 별도의 복잡한 프롬프트 없이도 문맥에 따라 토큰을 알아서 낸다.
하지만 실무에서는 전용 모델을 직접 가져다 쓰는 게 번거롭다. 그래서 GPT-4 같은 일반 LLM에게 "이 질문이 검색이 필요한지 Yes/No로 답해줘"라고 판사 역할을 시키는 방식을 더 많이 쓴다. 이른바 모듈형(Modular RAG) 접근이고, LangGraph 같은 프레임워크에서 주로 이렇게 짠다. 모델을 새로 훈련시킬 필요 없이, 프롬프트만 잘 짜면 어떤 고성능 LLM이든 Self-RAG의 '판단관'으로 세울 수 있다는 게 장점이다.
이 자기반성 루프 전체를 코드로 직접 돌려보면 감이 확실히 잡힌다. 외부 라이브러리나 전용 모델 없이, Self-RAG의 판단들 — 검색 여부, 관련성(IsRel), 근거(IsSup), 유용성(IsUse), 질문 재작성 — 을 규칙 기반 함수로 흉내 내 '고객지원 FAQ'에 적용한 예제다. 질문 두 개를 넣어, 하나는 단순 인사라 검색을 건너뛰고([No Retrieval]), 다른 하나는 검색 → 관련 없는 문서 폐기 → 생성 → 자기 채점까지 밟는 과정을 단계별로 찍어준다. 아래 코드로 전체 흐름을 먼저 훑은 뒤, 이어서 생성·평가 단계를 하나씩 자세히 보자.
"""
Self-RAG 개념 예제 — 고객지원(Customer Support) FAQ 도메인
핵심 흐름: 질문 → [Retrieve]/[No Retrieval] 판단 → 문서 관련성 평가(IsRel)
→ 답변 생성 → 근거 확인(IsSup) + 유용성 확인(IsUse)
→ 미달 시 질문 재작성 후 재검색
모든 LLM 판단 함수는 규칙/키워드 기반으로 흉내 냄 (실제로는 LLM 판단)
"""
import re
# ─────────────────────────────────────────────────────────────
# 1. 사내 고객지원 FAQ 지식베이스 (인메모리 벡터DB 대역)
# ─────────────────────────────────────────────────────────────
FAQ_DOCS = [
{
"id": "doc-01",
"title": "배송 정책",
"content": (
"주문 후 영업일 기준 2~3일 내 배송됩니다. "
"제주·도서산간 지역은 추가 1~2일이 소요됩니다. "
"오후 2시 이전 결제 완료 건은 당일 출고합니다."
),
},
{
"id": "doc-02",
"title": "반품 및 환불 정책",
"content": (
"상품 수령 후 7일 이내에 반품을 신청할 수 있습니다. "
"단순 변심 반품 시 왕복 배송비(6,000원)가 고객 부담입니다. "
"불량·오배송의 경우 전액 환불이 가능합니다."
),
},
{
"id": "doc-03",
"title": "회원 포인트 적립 규정",
"content": (
"구매 금액의 1%가 포인트로 적립됩니다. "
"포인트는 적립일로부터 1년간 유효하며, "
"5,000포인트 이상 보유 시 결제에 사용할 수 있습니다."
),
},
{
"id": "doc-04",
"title": "고객센터 운영 시간",
"content": (
"고객센터는 평일 오전 9시부터 오후 6시까지 운영합니다. "
"주말 및 공휴일은 휴무이며, 카카오톡 채널로 문의하면 "
"다음 영업일에 순차 답변드립니다."
),
},
# 질문 B 첫 번째 검색 시 의도적으로 낮은 관련성 문서가 섞이도록 추가
{
"id": "doc-05",
"title": "이벤트 안내",
"content": "여름 할인 이벤트: 7월 한 달간 전 상품 10% 할인 적용.",
},
]
# ─────────────────────────────────────────────────────────────
# 2. 검색기 (키워드 기반 유사도 흉내)
# ─────────────────────────────────────────────────────────────
def retrieve_docs(query: str, top_k: int = 3) -> list:
"""
실제로는 임베딩 벡터 유사도 검색(FAISS, Chroma 등)을 수행.
여기서는 키워드 겹침 개수로 단순 근사.
"""
query_tokens = set(re.sub(r"[^\w]", " ", query).split())
scored = []
for doc in FAQ_DOCS:
doc_tokens = set(re.sub(r"[^\w]", " ", doc["title"] + " " + doc["content"]).split())
overlap = len(query_tokens & doc_tokens)
scored.append((overlap, doc))
# 점수 내림차순 정렬 후 top_k 반환
scored.sort(key=lambda x: x[0], reverse=True)
return [doc for _, doc in scored[:top_k]]
# ─────────────────────────────────────────────────────────────
# 3. Self-RAG 판단 함수 (실제로는 LLM 판단 — 여기서는 규칙 기반)
# ─────────────────────────────────────────────────────────────
# 단순 인사/감사 패턴 — 검색 불필요 신호
_NO_RETRIEVAL_PATTERNS = re.compile(
r"^(안녕|고마워|감사|ㅎㅇ|잘있어|반가워|수고|안녕하세요|괜찮으세요|어떻게 지내)", re.IGNORECASE
)
def decide_retrieval(query: str) -> str:
"""
[Retrieve] / [No Retrieval] 결정.
실제로는 LLM이 '내부 지식으로 충분한가?'를 판단.
여기서는 인사·감사 패턴이면 [No Retrieval], 그 외는 [Retrieve].
"""
if _NO_RETRIEVAL_PATTERNS.match(query.strip()):
return "No Retrieval"
return "Retrieve"
def grade_relevance(query: str, doc: dict) -> bool:
"""
IsRel: 검색된 문서가 질문과 관련 있는지 평가.
실제로는 LLM이 '이 문서가 질문 해결에 도움이 되는가?'를 판단.
여기서는 질문 키워드가 문서에 1개 이상 포함되면 관련 있다고 판단.
"""
query_tokens = set(re.sub(r"[^\w]", " ", query).split())
doc_text = doc["title"] + " " + doc["content"]
doc_tokens = set(re.sub(r"[^\w]", " ", doc_text).split())
return len(query_tokens & doc_tokens) >= 1
def generate_answer(query: str, docs: list) -> str:
"""
답변 생성 단계.
실제로는 LLM이 관련 문서를 컨텍스트로 삼아 답변을 생성.
여기서는 관련 문서 내용을 단순 연결.
"""
if not docs:
return "죄송합니다. 관련 정보를 찾지 못했습니다."
context = " / ".join(doc["content"] for doc in docs)
return f"[생성된 답변] 질문 '{query}'에 대한 안내: {context}"
def check_is_sup(answer: str, docs: list) -> bool:
"""
IsSup: 답변 내용이 검색 문서에 근거하는지 확인 (환각 방지).
실제로는 LLM이 '답변 문장이 컨텍스트 문서에 실제로 있는 내용인가?'를 판단.
여기서는 답변에 문서 내용 일부가 포함되어 있으면 Supported로 간주.
"""
for doc in docs:
# 문서 내용의 첫 15자 이상이 답변에 포함되면 근거 있다고 판단
snippet = doc["content"][:15]
if snippet in answer:
return True
return False
def check_is_use(query: str, answer: str) -> bool:
"""
IsUse: 답변이 질문에 실제로 유용한지 확인 (동문서답 방지).
실제로는 LLM이 1~5점 척도로 유용성을 채점.
여기서는 질문 키워드가 답변에 1개 이상 포함되면 유용하다고 판단.
"""
query_tokens = set(re.sub(r"[^\w]", " ", query).split())
answer_tokens = set(re.sub(r"[^\w]", " ", answer).split())
return len(query_tokens & answer_tokens) >= 1
def rewrite_query(original_query: str) -> str:
"""
질문 재작성기 (Query Rewriter).
실제로는 LLM이 검색에 더 적합한 형태로 질문을 변환.
여기서는 "정책 안내" 키워드를 추가하는 단순 규칙.
"""
return original_query.strip() + " 정책 안내"
# ─────────────────────────────────────────────────────────────
# 4. Self-RAG 메인 루프
# ─────────────────────────────────────────────────────────────
def self_rag(query: str, max_retry: int = 1) -> str:
"""
Self-RAG 자기반성 루프:
질문 → 검색 필요 여부 판단 → 검색 → IsRel 필터
→ 답변 생성 → IsSup + IsUse 채점 → 미달 시 재작성 후 재검색
"""
print(f"\n{'='*60}")
print(f"[질문] {query}")
print(f"{'='*60}")
# ── 단계 1: 검색 필요 여부 판단 ──────────────────────────
retrieval_decision = decide_retrieval(query)
print(f"\n[단계 1 · Retrieve 판단] -> {retrieval_decision}")
if retrieval_decision == "No Retrieval":
# 검색 불필요: 내부 지식(또는 고정 인삿말)으로 바로 답변
answer = "안녕하세요! 고객지원 챗봇입니다. 무엇을 도와드릴까요?"
print(f"[최종 답변] {answer}")
return answer
# ── 단계 2~6: 검색 → 평가 → 생성 → 채점 루프 ─────────────
for attempt in range(max_retry + 1):
current_query = query if attempt == 0 else rewrite_query(query)
if attempt > 0:
print(f"\n[재검색 · 질문 재작성] -> '{current_query}'")
raw_docs = retrieve_docs(current_query)
print(f"\n[단계 2 · 검색 결과] {len(raw_docs)}건 수신")
for d in raw_docs:
print(f" · {d['id']}: {d['title']}")
# ── 단계 3: 관련성 평가 (IsRel) ──────────────────────
relevant_docs = []
print("\n[단계 3 · IsRel 관련성 평가]")
for doc in raw_docs:
is_rel = grade_relevance(current_query, doc)
label = "관련 있음 ✓" if is_rel else "관련 없음 ✗ (폐기)"
print(f" · {doc['id']} ({doc['title']}): {label}")
if is_rel:
relevant_docs.append(doc)
if not relevant_docs:
print(" -> 관련 문서 없음. 재검색 시도 또는 내부 지식으로 전환.")
if attempt == max_retry:
answer = "죄송합니다. 해당 질문에 대한 관련 정보를 찾을 수 없습니다."
print(f"\n[최종 답변] {answer}")
return answer
continue # 재검색 루프
# ── 단계 4: 답변 생성 ────────────────────────────────
answer = generate_answer(current_query, relevant_docs)
print(f"\n[단계 4 · 답변 생성]\n {answer}")
# ── 단계 5: IsSup 근거 확인 ──────────────────────────
is_sup = check_is_sup(answer, relevant_docs)
print(f"\n[단계 5 · IsSup 근거 확인] -> {'Supported ✓' if is_sup else 'Not Supported ✗'}")
# ── 단계 6: IsUse 유용성 확인 ────────────────────────
is_use = check_is_use(current_query, answer)
print(f"[단계 6 · IsUse 유용성 확인] -> {'Useful ✓' if is_use else 'Not Useful ✗'}")
if is_sup and is_use:
print(f"\n[최종 답변] 채점 통과 -> 답변 확정")
print(f" {answer}")
return answer
else:
print(" -> 채점 미달. 질문 재작성 후 재검색합니다.")
if attempt == max_retry:
print(" -> 최대 재시도 횟수 도달. 현재 답변을 반환합니다.")
print(f"\n[최종 답변] {answer}")
return answer
return answer
# ─────────────────────────────────────────────────────────────
# 5. 데모 실행
# ─────────────────────────────────────────────────────────────
if __name__ == "__main__":
# ── 데모 A: 일상 인사 -> [No Retrieval]
self_rag("안녕하세요, 잘 지내셨나요?")
# ── 데모 B: 지식 질문 -> 검색 -> 관련성 평가 -> 생성 -> 채점
# '환불' 키워드: doc-02(반품·환불 정책)와 관련 있고,
# doc-01(배송), doc-03(포인트)은 IsRel 필터에서 폐기됨.
self_rag("환불 신청은 어떻게 하나요? 기간이 있나요?")
2단계 생성 — 답을 쓰면서 동시에 채점한다
생성 단계는 그냥 답변을 써 내려가는 과정이 아니다. 가져온 정보가 진짜 쓸모 있는지, 그리고 자기가 쓴 글이 팩트인지를 스스로 채점하는 '자기 검열' 과정이다. 여기서 세 가지 평가 지표가 등장하는데, 하나씩 보면 각각이 다른 함정을 막고 있다.
첫 번째는 **관련성 평가(IsRel, Is Relevant)**다. 검색기가 가져온 문서가 항상 정답은 아니다. 질문과 전혀 상관없는 '쓰레기 데이터'를 끌어오는 일이 비일비재하다. 그래서 모델이 문서를 하나씩 훑으며 "이게 질문에 답하는 데 도움이 되는가?"를 판별한다. 관련 있으면 답변 재료로 쓰고, 관련 없으면 과감히 버린다. 만약 모든 문서가 관련 없으면, 검색 결과에 매달리지 않고 내부 지식으로 답하거나 "모르겠습니다"라고 정직하게 말한다. 논문 기준으로는 이걸 사전 학습된 모델이 자동으로 처리하지만, 실무에서는 LLM을 채점관으로 세워 프롬프트로 처리한다.
문서를 추렸으면 이제 답을 쓰는데, 여기서도 문장마다 두 가지 토큰이 꼬치꼬치 따져 묻는다. 하나는 **근거 확인(IsSup, Is Supported)**이다. "내가 지금 쓴 답변이 방금 읽은 문서에 진짜로 적혀 있는 내용인가?"를 묻는다. 모델이 문서를 읽어놓고 제 마음대로 내용을 지어내는 걸 막는, 곧 할루시네이션 방지 장치다. 문서에 없는 내용을 말하면 감점하거나 해당 문장을 수정한다. 다른 하나는 **유용성 확인(IsUse, Is Useful)**이다. "이 답변이 사용자 질문에 정말 도움이 되는가?"를 본다. 팩트는 맞아도 질문과 핀트가 어긋난 동문서답을 걸러내는 역할이다.
여기서 전용 모델과 일반 LLM의 차이가 또 갈린다. 도메인에 특화된 Self-RAG 전용 모델을 쓰면 이 일련의 과정을 내부에서 알아서 처리한다. 실제로 selfrag/selfrag-llama 같은 모델은 답변을 생성할 때 텍스트만 뱉는 게 아니라, 문장 사이사이에 반성 토큰을 섞어서 내놓는다. 출력이 이런 식이다.
[Retrieve] [IsRel:Relevant] 삼성전자는 2026년 6G 상용화를 목표로... [IsSup:Supported] [IsUse:5]
우리가 "이게 관련 있어?"라고 따로 묻지 않아도, 모델이 문장을 쓰면서 동시에 [IsRel:Relevant]라는 판단을 스스로 박아 넣는다. 채점용 프롬프트 여러 개를 따로 관리할 필요 없이, 모델 하나가 생성과 검증을 한 번의 흐름 안에서 끝낸다는 게 핵심이다. 대신 이런 모델을 쓰려면 사전 훈련 비용이 든다. 일반 LLM은 편하지만 그 대가로 모든 검증 과정을 일일이 프롬프트로 짜야 한다. 어느 쪽이든 공짜는 없다는 뜻이다.
3단계 평가 — 자기 답을 검토하고 퇴고한다
마지막 평가 단계는 이 시스템이 왜 '자기 반성적'이라 불리는지를 가장 잘 보여주는 대목이다. 답을 한 번 내놓고 끝내는 게 아니라, 자신이 쓴 글을 검토하고 퇴고하는 과정이기 때문이다. 여기서는 앞서 나온 IsSup와 IsUse가 다시 등판해 더 엄격한 기준으로 작동한다.
**근거 평가(IsSup)**는 팩트 체크다. 모델이 아무리 유창하게 답해도 그 내용이 검색된 문서에 없으면 무효다. 생성된 답변의 각 문장이 문서에 근거하는지를 따져, 모든 문장이 근거를 갖추면 통과시키고(Fully Supported), 일부만 맞으면 수정 대상으로 돌리며(Partially Supported), 문서와 무관한 소설을 썼으면 답변을 폐기하고 다시 생성하게 한다(No Support).
**유용성 평가(IsUse)**는 품질 체크다. 팩트가 맞더라도 질문의 정답이 아닐 수 있으니, 답변이 질문을 정말로 해결해주는지를 본다. 보통 1~5점 척도로 매기는데, 완벽하게 답하면 5점, 팩트는 맞아도 딴소리면 1점이다. 점수가 임계값(예컨대 3점)보다 낮으면 검색 쿼리를 고쳐서 다시 검색 단계로 되돌아간다.
그래서 Self-RAG는 답을 하나만 만들지 않는다. 검색된 여러 문서 뭉치를 바탕으로 답변 초안을 여러 버전 만들고, 각 초안에 IsSup와 IsUse 점수를 매긴 뒤, 두 점수를 곱한 값(IsSup × IsUse)이 가장 높은 답변을 골라 사용자에게 보낸다. 만약 모든 후보의 점수가 낮으면? 질문을 다시 써서(Query Rewriting) 재검색하거나, 검색 문서가 아예 쓸모없다면 모델 내부 지식만으로 답을 재구성한다. 이 '평가 → 실패 시 되돌아가기'가 바로 Self-RAG를 순환 구조로 만드는 지점이다.
직접 구현하려면 채점단을 통째로 짜야 한다
여기까지 보면 자연스럽게 드는 생각이 있다. LLM으로 Self-RAG를 구현하려면 직접 만들어야 할 단계가 꽤 많겠다는 것이다. 정확한 통찰이다. 일반 RAG가 시작에서 끝까지 쭉 가는 '직진 코스'라면, Self-RAG는 갈림길과 유턴이 가득한 '복잡한 회로도'에 가깝다. 직접 구현하다 보면 "생각보다 노가다가 심하네?" 싶은 게 지극히 정상이다.
전용 모델 없이 일반 LLM으로 이 시스템을 세우려면, 최소한 다음 다섯 개의 독립적인 프롬프트(또는 체인)를 각각 만들어야 한다.
채점단역할
| 라우터(Router) | 질문을 보고 검색할지 말지 결정 |
| 관련성 채점기(Doc Grader) | 가져온 문서가 진짜 쓸모 있는지 판별 |
| 할루시네이션 채점기(Hallucination Grader) | 답변이 문서에 근거하는지 확인 |
| 답변 유용성 채점기(Answer Grader) | 질문에 제대로 답했는지 확인 |
| 질문 재작성기(Question Rewriter) | 검색 실패 시 질문을 어떻게 바꿀지 결정 |
이 다섯을 따로 만드는 것까지는 어떻게든 한다. 진짜 문제는 이것들을 엮는 흐름에 순환이 들어간다는 점이다. 답변이 통과 못 하면 다시 생성으로, 딴소리면 질문 재작성으로 되돌아가야 하는데, 일반적인 선형 체인으로는 이 유턴을 표현하기가 까다롭다.
그래서 LangGraph를 쓴다. LangChain 진영도 이 문제가 만만치 않다는 걸 알고 내놓은 도구가 LangGraph인데, Self-RAG처럼 순환이 필요한 구조를 짤 때 전 세계 개발자들이 가장 많이 집어 드는 선택지다. 검색·생성·채점을 노드(함수)로 정의하고, 채점 결과에 따라 흐름이 갈라지는 조건부 엣지(conditional edge)를 걸면 된다. 채점에 통과하면 종료, 할루시네이션이면 다시 생성, 딴소리면 질문 재작성으로 — 이렇게 갈림길을 코드로 깔끔하게 표현할 수 있다. 앞서 돌려본 예제가 이 순환을 if와 재시도 루프로 직접 구현한 버전이라면, LangGraph는 같은 갈림길과 유턴을 그래프로 선언해 관리해주는 도구인 셈이다.
고생한 만큼 통제권을 얻는다
직접 짤 단계가 많다는 건 뒤집어 보면 우리가 시스템을 그만큼 완벽하게 통제할 수 있다는 뜻이다. 손이 많이 가는 대가로 얻는 게 분명하다는 얘기다.
가장 큰 보상은 신뢰도다. 단계마다 검증을 거치니 사용자가 가짜 답변이나 엉뚱한 대답을 받을 확률이 0에 수렴한다. 비용도 아낀다. 검색이 필요 없는 질문을 앞단에서 걸러주므로 불필요한 API 호출이 줄어든다. 그리고 무엇보다, 단순히 챗봇 하나 만드는 수준을 넘어 기업용 '지식 엔진'에 가까운 품질이 나온다. Self-RAG가 손이 많이 가면서도 끝까지 거론되는 이유가 여기에 있다고 생각한다.
[참고자료]
RAG 마스터 레시피 - 브라이스 유 , 조경아 , 박수진 , 김재웅
https://product.kyobobook.co.kr/detail/S000216240484
테디노트의 랭체인을 활용한 RAG 비법노트
https://product.kyobobook.co.kr/detail/S000216574552
출처: https://kimback03.tistory.com/196 [DevCode : IT 개발 기술 아카이브:티스토리]