본문 바로가기
AI System/Agentic RAG: Agent 시스템을 위한 Vector RAG 설계

d02 - 9. CRAG & Self-RAG

by Toddler_AD 2026. 6. 16.

S8 · CRAG & Self-RAG — 스스로 평가하고 교정하는 RAG

Agentic RAG 실전 (14시간) · Day 2

검색이 빗나가거나 답이 근거를 벗어나도(환각) 그대로 진행하던 RAG에 자기 점검과 되돌아가기를 넣는다. S4의 조건부 엣지·루프가 본격적으로 쓰인다. S0 .venv/.env 로 실행.

목표

  • CRAG: 검색 문서의 관련성을 채점하고, 나쁘면 질의를 고쳐 재검색한다.
  • Self-RAG: 답의 근거 충실성을 채점하고, 미흡하면 재생성한다.
  • 무한 루프를 막는 재시도 상한을 둔다.
 

1. 왜 자기 교정인가

RAG가 틀리는 두 지점:

  • 검색 빗나감: 무관한 청크를 근거로 답하면 결과가 엉터리가 된다.
  • 환각: 검색은 적절한데 모델이 근거에 없는 내용을 지어낸다.

자기 교정은 이 둘을 각각 채점·교정한다.

 

 

 

2. 두 가지 교정 패턴

CRAG — 검색 교정: 검색 → 관련성 채점 → 관련 있으면 생성, 없으면 질의 재작성 후 재검색.

Self-RAG — 답변 교정: 생성 → 충실성 채점 → 충실하면 종료, 미흡하면 재생성.

둘을 한 그래프에 합치면 검색과 답을 모두 점검하는 견고한 RAG가 된다.

 

 

3. LLM-as-judge + 루프 안전장치

채점도 LLM에 맡기되, 분기에 쓰려면 명확한 형식이 필요하다. S7의 구조화 출력으로 불리언을 받는다.

  • 문서 채점: relevant(관련 있음)
  • 답변 채점: grounded(근거에 충실함)

"나쁘면 다시" 루프는 무한 반복될 수 있으므로 재시도 상한(MAX_RETRIES) 을 두고, 도달하면 현재까지의 최선으로 마무리한다. (LangGraph에도 recursion_limit 안전장치가 있어 과도 반복 시 GraphRecursionError 가 난다.)

 
 

4. 그래프 구조

  START → [retrieve] → [grade_docs: 문서 관련성]
                              │
              ┌───────────────┼────────────────────┐
          관련 O           관련 X & 재시도            관련 X & 상한도달
              │               │                       │
          [generate]      [rewrite] ─▶ (retrieve 루프)   [generate] (최선)
              │
       [grade_answer: 근거 충실성]
              │
        충실 O → END        충실 X → [regenerate] → END

부품은 모두 S4~S7의 것이다: 노드 + 조건부 엣지 + 루프 + 구조화 출력.

 

 

 

5. 실행 준비

S0 .venv 커널. .env 키 로드.

import os
from getpass import getpass

# S0 프로젝트 루트의 .env 에서 키 로드 (없으면 입력으로 폴백)
try:
    from dotenv import load_dotenv
    load_dotenv()
except ImportError:
    pass
for name in ["OPENAI_API_KEY", "PINECONE_API_KEY"]:
    if not os.environ.get(name):
        os.environ[name] = getpass(f"{name}: ")
print("키:", {k: bool(os.environ.get(k)) for k in ["OPENAI_API_KEY", "PINECONE_API_KEY"]})
키: {'OPENAI_API_KEY': True, 'PINECONE_API_KEY': True}

 

 

 

6. 인덱스 연결

import os
from pinecone import Pinecone
from langchain_pinecone import PineconeVectorStore, PineconeEmbeddings
from langchain_openai import ChatOpenAI

embeddings = PineconeEmbeddings(model="multilingual-e5-large")   # 한국어 임베딩(1024차원)
pc = Pinecone(api_key=os.environ["PINECONE_API_KEY"])
index = pc.Index("agentic-rag-lab")                              # S2/S3에서 색인한 인덱스
# ★ 네임스페이스 미지정 시 빈 기본 네임스페이스를 조회한다. S3에서 색인한 chunk_500 을 메인 코퍼스로 사용.
NAMESPACE = "chunk_500"
vector_store = PineconeVectorStore(index=index, embedding=embeddings, namespace=NAMESPACE)
try:
    _stats = index.describe_index_stats()
    _ns = getattr(_stats, "namespaces", None) or (_stats.get("namespaces") if isinstance(_stats, dict) else {}) or {}
    _cnt = {k: getattr(v, "vector_count", v.get("vector_count") if isinstance(v, dict) else None) for k, v in _ns.items()}
    print("네임스페이스별 벡터 수:", _cnt)
    if not _cnt.get(NAMESPACE):
        print(f"⚠️ '{NAMESPACE}' 네임스페이스에 데이터가 없습니다. S3의 색인 셀을 먼저 실행하세요.")
except Exception as _e:
    print("인덱스 통계 확인 생략:", _e)
llm = ChatOpenAI(model="gpt-4.1-mini", temperature=0)            # 기본 LLM
print("연결 완료")
네임스페이스별 벡터 수: {'demo': 3, 'chunk_200': 89, 'chunk_1500': 12, 'chunk_500': 34, 'chunk_1000': 18}
연결 완료

 

 

 

7. 채점관 정의 — 구조화 출력

문서 관련성과 답변 충실성을 각각 불리언으로 채점하는 두 채점관을 만든다.

from pydantic import BaseModel, Field
from langchain_core.messages import SystemMessage, HumanMessage

# 채점 결과의 '틀' — 각각 불리언 한 개
class DocGrade(BaseModel):
    """검색된 문서가 질문에 답하는 데 관련이 있는지 채점한다."""
    relevant: bool = Field(description="검색 문서가 질문과 관련 있으면 True, 아니면 False")

class AnswerGrade(BaseModel):
    """생성된 답변이 근거 문서에 충실한지(환각이 없는지) 채점한다."""
    grounded: bool = Field(description="답변이 근거에 충실하고 지어낸 내용이 없으면 True")

doc_grader = llm.with_structured_output(DocGrade)        # 문서 채점관
answer_grader = llm.with_structured_output(AnswerGrade)  # 답변 채점관
print("채점관 준비 완료")
채점관 준비 완료

 

 

 

8. 노드 정의

준비·검색·재작성·생성·재생성 노드. 각 노드는 상태의 변경분만 반환한다.

def retrieve_node(state):
    sq = state.get("search_query") or state["question"]     # 재작성된 검색어가 있으면 그걸로
    docs = vector_store.similarity_search(sq, k=3)
    context = "\n\n".join(f"[{d.metadata['title']}] {d.page_content}" for d in docs)
    return {"documents": context}

def grade_docs_node(state):
    # 검색된 문서가 질문에 관련 있는지 채점
    result = doc_grader.invoke([
        SystemMessage(content="검색된 문서가 질문에 답하는 데 관련 있는지 채점하세요."),
        HumanMessage(content=f"[질문]\n{state['question']}\n\n[검색 문서]\n{state['documents']}"),
    ])
    print(f"  · 문서 관련성: {result.relevant}")
    return {"docs_relevant": result.relevant}

def rewrite_node(state):
    # 검색이 부실했으니 질의를 재작성한다. 단, '이미 시도한 검색어'와는 다른 표현·핵심어를 쓰게 해
    # 매 재시도가 같은 검색어로 반복되는 것을 막는다(루프가 헛돌지 않도록).
    tried = state.get("tried") or [state.get("search_query") or state["question"]]
    avoid = " / ".join(tried)
    prompt = ("이전 검색이 부실했습니다. 아래 질문을 검색이 잘 되도록 한 줄로 재작성하되, "
              f"다음 '이미 시도한 검색어'와는 다른 표현·다른 핵심어를 사용하세요: {avoid}\n"
              f"질문: {state['question']}\n새 검색어:")
    new_q = llm.invoke(prompt).content.strip()
    print(f"  · 재작성: {new_q}")
    return {"search_query": new_q,
            "retries": state.get("retries", 0) + 1,
            "tried": tried + [new_q]}

def generate_node(state):
    # 관련 근거를 확보한 경우: 근거만 사용해 답하고 참고 문서 제목을 표기.
    # 관련 근거를 못 찾은 경우(상한 도달): 없는 내용을 지어내지 말고, 참고 문서도 표기하지 않는다.
    if state.get("docs_relevant"):
        prompt = ("다음 [근거]만 사용해 한국어로 답하고, 끝에 참고한 문서 제목을 표기하세요.\n\n"
                  f"[근거]\n{state['documents']}\n\n[질문]\n{state['question']}")
    else:
        prompt = ("아래 [근거]에는 질문에 답할 내용이 없을 수 있습니다. 근거에 명확한 답이 없으면 "
                  "'제공된 사내 문서에서 해당 내용을 찾지 못했습니다'라고만 답하고 참고 문서 제목은 표기하지 마세요. "
                  "근거에 답이 있으면 그 내용만 사용해 답하고 참고 문서 제목을 표기하세요.\n\n"
                  f"[근거]\n{state['documents']}\n\n[질문]\n{state['question']}")
    return {"answer": llm.invoke(prompt).content}

def grade_answer_node(state):
    # 답이 근거에 충실한지(환각 없는지) 채점
    result = answer_grader.invoke([
        SystemMessage(content="답변이 근거 문서에 충실하고 지어낸 내용이 없는지 채점하세요."),
        HumanMessage(content=f"[근거]\n{state['documents']}\n\n[답변]\n{state['answer']}"),
    ])
    print(f"  · 답변 충실성: {result.grounded}")
    return {"grounded": result.grounded}

 

 

 

9. 분기 함수 + 그래프 조립

채점 결과와 재시도 횟수로 다음 노드를 정한다. 재시도 상한을 여기서 적용한다.

from typing import TypedDict
from langgraph.graph import StateGraph, START, END

MAX_RETRIES = 2     # 무한 루프 방지: 최대 2회까지만 재시도

class CragState(TypedDict):
    question: str        # 원본 질문
    search_query: str    # 재작성된 검색어
    documents: str       # 검색된 근거
    docs_relevant: bool  # 문서 채점 결과
    answer: str          # 생성된 답
    grounded: bool       # 답변 채점 결과
    retries: int         # 재시도 횟수
    tried: list          # 이미 시도한 검색어 목록(재작성 다양화용)

def decide_after_docs(state):
    if state["docs_relevant"]:                       # 문서가 관련 있으면
        return "generate"                            #   답 생성으로
    if state["retries"] < MAX_RETRIES:               # 관련 없고 재시도 여유 있으면
        return "rewrite"                             #   재작성 → 재검색
    return "generate"                                # 상한 도달 → 있는 자료로 최선

def decide_after_answer(state):
    if state["grounded"]:                            # 답이 근거에 충실하면
        return "end"                                 #   종료
    if state["retries"] < MAX_RETRIES:               # 미흡 + 재시도 여유
        return "rewrite"                             #   재작성 → 재검색 → 재생성
    return "end"                                     # 상한 도달 → 종료

g = StateGraph(CragState)
g.add_node("retrieve", retrieve_node)
g.add_node("grade_docs", grade_docs_node)
g.add_node("rewrite", rewrite_node)
g.add_node("generate", generate_node)
g.add_node("grade_answer", grade_answer_node)

g.add_edge(START, "retrieve")
g.add_edge("retrieve", "grade_docs")
g.add_conditional_edges("grade_docs", decide_after_docs,
                        {"generate": "generate", "rewrite": "rewrite"})
g.add_edge("rewrite", "retrieve")                    # 재작성 → 다시 검색 (CRAG 루프)
g.add_edge("generate", "grade_answer")
g.add_conditional_edges("grade_answer", decide_after_answer,
                        {"end": END, "rewrite": "rewrite"})  # Self-RAG 루프

app = g.compile()
print("CRAG + Self-RAG 그래프 컴파일 완료")
CRAG + Self-RAG 그래프 컴파일 완료

 

from IPython.display import Image, display
try:
    display(Image(app.get_graph().draw_mermaid_png()))
except Exception:
    print(app.get_graph().draw_mermaid())

 

 

10. 실행 — 채점·교정 과정

노드가 채점 결과를 출력하므로 검색·답변이 어떻게 점검·교정되는지 보인다.

def run(q):
    print("Q:", q)
    out = app.invoke({"question": q, "search_query": q, "retries": 0, "tried": [q]})
    print("A:", out["answer"][:160], "...")
    print("재시도 횟수:", out.get("retries", 0), "\n")

run("연차는 며칠이고 이월 규정은 어떻게 되나요?")   # 정상 검색 → 충실 답 기대
run("주차장 정책 알려줘")                          # 코퍼스에 없음 → 재작성/교정 루프 유도
Q: 연차는 며칠이고 이월 규정은 어떻게 되나요?
  · 문서 관련성: True
  · 답변 충실성: True
A: 연차는 입사 1년 차에 15일이 부여되며, 근속 2년마다 1일씩 가산되어 최대 25일까지 늘어납니다. 입사 첫해에는 1년을 채우지 못한 경우 매월 만근 시 1일씩 비례하여 부여받습니다. 연차는 회계연도(1월~12월) 기준으로 산정하며, 회계연도 중 입사한 직원은 입사일을 기준으로 비례  ...
재시도 횟수: 0 

Q: 주차장 정책 알려줘
  · 문서 관련성: False
  · 재작성: 주차장 운영 규정과 관리 방침 안내
  · 문서 관련성: False
  · 재작성: 주차장 관리 지침 및 운영 방안 안내
  · 문서 관련성: False
  · 답변 충실성: True
A: 제공된 사내 문서에서 해당 내용을 찾지 못했습니다. ...
재시도 횟수: 2 

 

🔍 관찰 포인트

  • 각 실행의 문서 관련성/답변 충실성 채점과 재작성 로그를 따라가면, 나쁜 검색이 감지될 때 재작성→재검색 루프가 도는 것이 보인다.
  • "주차장 정책" 처럼 코퍼스에 없는 질문은 상한(MAX_RETRIES)에서 멈추고 최선으로 마무리한다 → 무한 루프 없음.
  • 채점관이 "근거 없는 단정" 을 한 번 걸러 환각을 줄인다.

 

⚠️ 비용·지연

채점·재작성마다 LLM 호출이 늘어 지연·비용이 증가한다. 정확성이 중요한 곳에 선택 적용한다.

 

✏️ 미니 실습

  1. MAX_RETRIES 를 0으로 바꿔 교정 루프를 끄고 답 품질을 비교한다.
  2. 답변 채점 프롬프트를 더 엄격하게(예: 근거에 없는 수치는 환각으로 간주) 바꾼다.
  3. 문서 관련성이 낮을 때 웹 검색으로 폴백하는 분기를 설계한다(개념만이라도).
# 1) MAX_RETRIES 를 0으로 바꿔 교정 루프를 끄고 답 품질을 비교한다

def build_crag_app(max_retries, grade_answer_fn=grade_answer_node, extra_nodes=None, after_docs_fn=None):
    # 실습용 비교 그래프를 빠르게 다시 조립하는 헬퍼.
    # max_retries 를 바꾸면 같은 노드 구성을 유지한 채 루프 정책만 비교할 수 있다.
    extra_nodes = extra_nodes or {}
    after_docs_fn = after_docs_fn or (lambda state: "generate" if state["docs_relevant"] else ("rewrite" if state["retries"] < max_retries else "generate"))

    def after_answer_fn(state):
        if state["grounded"]:
            return "end"
        if state["retries"] < max_retries:
            return "rewrite"
        return "end"

    graph = StateGraph(CragState)
    graph.add_node("retrieve", retrieve_node)
    graph.add_node("grade_docs", grade_docs_node)
    graph.add_node("rewrite", rewrite_node)
    graph.add_node("generate", generate_node)
    graph.add_node("grade_answer", grade_answer_fn)
    for node_name, node_fn in extra_nodes.items():
        graph.add_node(node_name, node_fn)

    graph.add_edge(START, "retrieve")
    graph.add_edge("retrieve", "grade_docs")
    graph.add_conditional_edges(
        "grade_docs",
        after_docs_fn,
        {"generate": "generate", "rewrite": "rewrite"} | ({name: name for name in extra_nodes} if extra_nodes else {}),
    )
    graph.add_edge("rewrite", "retrieve")
    graph.add_edge("generate", "grade_answer")
    graph.add_conditional_edges("grade_answer", after_answer_fn, {"end": END, "rewrite": "rewrite"})
    return graph.compile()

app_no_retry = build_crag_app(max_retries=0)

def compare_retry_policy(question):
    print("Q:", question)
    base_out = app.invoke({"question": question, "search_query": question, "retries": 0, "tried": [question]})
    no_retry_out = app_no_retry.invoke({"question": question, "search_query": question, "retries": 0, "tried": [question]})
    print("[기본 설정: MAX_RETRIES=2]")
    print("  재시도 횟수:", base_out.get("retries", 0))
    print("  답변:", base_out["answer"][:140], "...")
    print("[교정 루프 끔: MAX_RETRIES=0]")
    print("  재시도 횟수:", no_retry_out.get("retries", 0))
    print("  답변:", no_retry_out["answer"][:140], "...")
    print()

compare_retry_policy("주차장 정책 알려줘")
 
Q: 주차장 정책 알려줘
  · 문서 관련성: False
  · 재작성: 주차장 운영 규정과 관리 방침 안내
  · 문서 관련성: False
  · 재작성: 주차장 관리 지침과 운영 방안 안내
  · 문서 관련성: False
  · 답변 충실성: True
  · 문서 관련성: False
  · 답변 충실성: True
[기본 설정: MAX_RETRIES=2]
  재시도 횟수: 2
  답변: 제공된 사내 문서에서 해당 내용을 찾지 못했습니다. ...
[교정 루프 끔: MAX_RETRIES=0]
  재시도 횟수: 0
  답변: 제공된 사내 문서에서 해당 내용을 찾지 못했습니다. ...

실행 결과 해설

  • 이 셀은 MAX_RETRIES를 줄였을 때 최종 답이 어떻게 바뀌는지보다, 중간의 교정 과정이 얼마나 달라지는지를 보여준다. 현재 예시에서는 주차장 정책이 코퍼스에 없어서 재시도를 해도 결국 같은 결론으로 수렴한다.
  • 하지만 로그를 보면 차이는 분명하다. 기본 설정에서는 문서 관련성: False가 반복되면서 검색어가 두 번 재작성되고, MAX_RETRIES=0에서는 그 과정을 생략한 채 바로 답을 마무리한다. 즉, 루프를 끄면 비용과 지연은 줄지만 복구 기회도 사라진다.
  • 실무에서는 이런 비교로 재시도 횟수를 정한다. 질문이 조금만 다듬어져도 회복되는 코퍼스라면 재시도가 가치가 있지만, 지금처럼 애초에 자료가 없는 질문에서는 여러 번 돌려도 이득이 거의 없다. 교정 루프는 많을수록 좋은 것이 아니라, 추가 호출 비용 대비 회복 효과가 있을 때만 의미가 있다.

 

 

 

# 2) 답변 채점 프롬프트를 더 엄격하게 바꾼다

def grade_answer_node_strict(state):
    # 숫자, 기간, 횟수처럼 사실 오류가 나기 쉬운 표현은 특히 엄격하게 본다.
    # 근거에 없는 수치가 한 개라도 섞이면 grounded=False 로 판정하게 유도한다.
    result = answer_grader.invoke([
        SystemMessage(
            content=(
                "답변이 근거 문서에 충실한지 매우 엄격하게 채점하세요. "
                "특히 근거에 없는 수치, 날짜, 기간, 횟수, 비율, 고유명사가 들어가면 환각으로 간주하세요."
            )
        ),
        HumanMessage(content=f"[근거]\n{state['documents']}\n\n[답변]\n{state['answer']}"),
    ])
    print(f"  · 엄격 답변 충실성: {result.grounded}")
    return {"grounded": result.grounded}

app_strict_answer = build_crag_app(max_retries=2, grade_answer_fn=grade_answer_node_strict)

strict_question = "연차는 며칠이고 이월 규정은 어떻게 되나요?"
print("Q:", strict_question)
strict_out = app_strict_answer.invoke({
    "question": strict_question,
    "search_query": strict_question,
    "retries": 0,
    "tried": [strict_question],
})
print("A:", strict_out["answer"][:160], "...")
print("재시도 횟수:", strict_out.get("retries", 0))
Q: 연차는 며칠이고 이월 규정은 어떻게 되나요?
  · 문서 관련성: True
  · 엄격 답변 충실성: True
A: 연차는 입사 1년 차에 15일이 부여되며, 근속 2년마다 1일씩 가산되어 최대 25일까지 늘어납니다. 입사 첫해에는 1년을 채우지 못한 경우 매월 만근 시 1일씩 비례하여 부여받습니다. 연차는 회계연도(1월~12월) 기준으로 산정하며, 회계연도 중 입사한 직원은 입사일을 기준으로 비례  ...
재시도 횟수: 0

실행 결과 해설

  • 이 셀은 답변 채점 기준을 더 엄격하게 바꿨을 때, Self-RAG 루프가 실제로 더 자주 발동하는지 확인하는 실험이다. 특히 숫자, 기간, 횟수처럼 틀리기 쉬운 정보를 더 엄격하게 보겠다고 명시한 점이 핵심이다.
  • 현재 실행에서는 엄격 답변 충실성: True가 나왔고 재시도도 발생하지 않았다. 즉, 연차는 며칠이고 이월 규정은 어떻게 되나요?에 대한 답이 적어도 현재 근거 문서 범위 안에서는 무리 없이 방어된다는 뜻이다. 엄격한 채점 프롬프트에서도 통과했다면 비교적 안정적인 답으로 볼 수 있다.
  • 물론 기준을 더 세게 주면 항상 품질이 좋아지는 것은 아니다. 다른 질문에서는 False가 더 자주 나와 불필요한 재생성이 늘 수 있다. 그래서 채점 프롬프트는 환각 억제와 응답 지연 사이에서 균형을 잡는 장치로 봐야 한다.

 

 

 

# 3) 문서 관련성이 낮을 때 웹 검색으로 폴백하는 분기를 설계한다

def web_node(state):
    # 이 교시에서는 실제 웹 검색을 붙이지 않고, 어떤 상황에서 web 경로가 필요한지만 설명한다.
    return {
        "answer": (
            "사내 문서 관련성이 낮아 웹 검색 폴백 경로로 분기되었습니다. "
            "실서비스에서는 여기서 웹 검색 Application Programming Interface(API)를 호출하고, "
            "가져온 결과를 다시 근거로 요약·답변하면 됩니다."
        ),
        "grounded": True,  # 데모 셀에서는 설명 문자열 자체를 최종 답으로 사용한다.
    }

def decide_after_docs_with_web(state):
    if state["docs_relevant"]:
        return "generate"
    if state["retries"] < 1:
        # 한 번은 사내 문서 검색어를 고쳐 보게 하고, 그래도 낮으면 web 으로 보낸다.
        return "rewrite"
    return "web"

app_with_web_fallback = build_crag_app(
    max_retries=1,
    extra_nodes={"web": web_node},
    after_docs_fn=decide_after_docs_with_web,
)

web_question = "오늘 반도체 업계 주요 뉴스 요약해줘"
print("Q:", web_question)
web_out = app_with_web_fallback.invoke({
    "question": web_question,
    "search_query": web_question,
    "retries": 0,
    "tried": [web_question],
})
print("A:", web_out["answer"])
Q: 오늘 반도체 업계 주요 뉴스 요약해줘
  · 문서 관련성: False
  · 재작성: 최신 반도체 산업 동향 및 주요 소식 정리해줘
  · 문서 관련성: False
A: 사내 문서 관련성이 낮아 웹 검색 폴백 경로로 분기되었습니다. 실서비스에서는 여기서 웹 검색 Application Programming Interface(API)를 호출하고, 가져온 결과를 다시 근거로 요약·답변하면 됩니다.

실행 결과 해설

  • 이 셀의 핵심은 관련성 낮음이 반복될 때 검색어만 계속 고치는 대신, 정보원 자체를 바꾸는 분기를 두는 데 있다. 즉, 어떤 질문은 재작성 실패가 아니라 애초에 사내 코퍼스 바깥 질문일 수 있다고 판단하는 것이다.
  • 현재 실행에서도 오늘 반도체 업계 주요 뉴스 요약해줘는 한 번 재작성된 뒤 다시 문서 관련성: False가 나왔고, 그 다음 web 경로로 넘어간다. 이 흐름은 최신 뉴스처럼 시간 민감한 질문은 사내 문서 검색으로 해결되지 않는다는 점을 잘 보여준다.
  • 실서비스에서는 이 web 노드 뒤에 웹 검색 도구(Web search tool), 뉴스 Application Programming Interface(API), 또는 외부 검색 결과 요약 단계를 붙이면 된다. 핵심은 검색 실패를 단순 오류로 끝내지 않고, 질문 유형에 맞는 정보원으로 우회하는 그래프를 설계하는 데 있다.
 

 

 

정리

  • CRAG(검색 채점→재검색)와 Self-RAG(답변 채점→재생성)를 한 그래프에 합쳐 자기 교정 RAG를 만들었다.
  • 핵심은 구조화 출력 채점관 + 조건부 엣지 루프 + 재시도 상한.
  • 다음(S9): 검색 정밀도를 끌어올리는 재순위·하이브리드 검색.

'AI System > Agentic RAG: Agent 시스템을 위한 Vector RAG 설계' 카테고리의 다른 글

d01 - 11. 평가  (0) 2026.06.16
d02 - 10. Rerank  (0) 2026.06.16
d02 - 8. Adaptive RAG  (0) 2026.06.16
d02 - 7. create_agent  (0) 2026.06.16
d01 - 6. AgenticRAG 실습  (0) 2026.06.15