RAG2026-5일27차시 · 기본 Modular RAG 구현9 / 18← 이전목차다음 →MetaContents

27차시. 기본 Modular RAG 구현

6. 실습 ③ — 루프가 도는 것 확인

출처: 원본 PDF 27차시_기본ModularRAG구현.pdf 13~14쪽 · 쪽 그림 13쪽 14쪽

재시도가 실제로 발생하는 상황을 만들어 확인합니다.

📝 stream으로 추적

출력
from graph import app
from state import init_state
q = "환불 방법과 수수료를 알려주세요"   # 근거가 부분적인 질문
for step in app.stream(init_state(q), {"recursion_limit": 25}):
    for node, update in step.items():
        # "log" 키는 빼고 나머지 키만 모으기
        keys = []
        for k in update.keys():
            if k != "log":
                keys.append(k)
        print(f"[{node}] {keys}")
        for line in update.get("log", []):
            print(f"        {line}")

출력
─────────────────────────────────────────────
─────────
[retrieve] ['documents', 'scores', 'retrieval_ok', 'fail_reason']
        검색(similarity,k=5): 2/5건 통과 최고=0.68
[generate] ['answer', 'insufficient', 'has_citation']
        생성: 74자, 인용 1건 정상
[verify]   ['grade', 'reason']
        검증(LLM): g=True r=False -> research
[bump]     ['retries']
        재시도 1회차 진입
[generate] ['answer', 'insufficient', 'has_citation']
        생성: 96자, 인용 2건 정상          ← 피드백 반영됨
[verify]   ['grade', 'reason']
        검증(LLM): g=True r=True -> pass    ← 통과!

🎯 재시도가 의미 있게 작동했습니다
1회차 — 관련성 부족(relevant=False)으로 반려
reason: "수수료 부분을 다루지 않음"
2회차 — 25차시 피드백 프롬프트가 작동
이유가 프롬프트에 들어가 개선된 답변 생성
→ 통과
25차시에서 피드백을 넣어두지 않았다면
같은 답변이 반복되어 상한까지 갔을 것입니다.

실습 코드 · 27/test_retry.py

실습 코드: 실습/src/27/test_retry.py · 위의 원본 PDF 27차시 13쪽 「6️⃣ 실습 ③ — 루프가 도는 것 확인」과 이어집니다.

Python
# ==============================================================
# 27차시. PPT 27차시_기본ModularRAG구현. 코드 폴더는 src/27
# PPT: 27차시 재생성 루프
# 역할: graph27을 stream으로 돌려 노드 이름과 그 노드가 남긴 log를 순서대로 출력합니다.
# 읽는 키: question으로 만든 초기 State
# 쓰는 키: 화면 출력만. State를 바꾸지 않습니다.
# 그래프:
#   START --> retrieve --> generate --> verify
#     |-- pass --> END
#     +-- retry --> bump --> generate --> verify ...
# --------------------------------------------------------------
# 파이썬만 아는 분을 위한 말
# State는 노드들이 함께 쓰는 딕셔너리입니다. 함수는 state를 읽고, 바꿀 키만 return 합니다.
# return에 적지 않은 키는 이전 값이 남습니다.
# log와 tried_queries는 리스트라서, 새로 return한 항목이 뒤에 이어 붙습니다.
# Retriever: 검색 담당. 질문과 비슷한 문서 조각만 고릅니다. 답 문장은 만들지 않습니다.
# Generator: 고른 문서를 근거로 답 문장을 만듭니다. 문서가 없으면 안내 문장만 돌려줍니다.
# Verifier: 답이 근거와 맞는지 검사하고 grade를 적습니다.
#   grade=pass     답을 확정하고 그래프를 끝냅니다.
#   grade=retry    같은 문서로 답 문장만 다시 씁니다. retries가 1 늘어납니다.
#   grade=research 문서가 부족합니다. 검색어를 바꾼 뒤 다시 찾습니다. rewrites가 늘어납니다.
#   grade=giveup   더 진행하지 않고 fallback으로 안내 문장만 돌려줍니다.
# fallback: 실패했을 때 답을 지어내지 않고 안내 문장을 반환하는 노드입니다.
# intent: 질문 종류. greeting 인사, calc 계산, scope 문서 밖, document 문서 검색.
# rewrite: 사용자가 친 question은 유지하고, 검색에 쓰는 query만 바꿉니다.
# safe_node: 노드를 try로 감쌉니다. 오류가 나도 그래프는 멈추지 않고 node_error에 적습니다.
# app.invoke는 마지막 State만 줍니다. app.stream은 노드가 끝날 때마다 그 노드의 return만 줍니다.
# 그래서 이 파일은 중간 log를 볼 수 있습니다.
# ==============================================================

# 이 파일은 27차시 RAG 그래프의 실행 과정을 확인합니다.
# 각 노드가 어떤 순서로 실행되었는지와 로그 내용을 출력합니다.

import os
import sys

# ============================================================
# 필요한 모듈 경로 등록
# ============================================================

# 현재 파일이 있는 폴더의 경로입니다.
CURRENT_DIR = os.path.dirname(__file__)

# 18차시의 State 모듈을 Python 검색 경로에 추가합니다.
STATE_DIR = os.path.join(CURRENT_DIR, "..", "18")
sys.path.insert(0, STATE_DIR)

# ============================================================
# 그래프와 초기 State 함수 가져오기
# ============================================================

# 27차시에서 완성한 LangGraph를 가져옵니다.
from graph27 import app

# 질문을 초기 State로 변환하는 함수를 가져옵니다.
from graph_state2 import make_initial_state

# ============================================================
# 그래프 실행 및 로그 출력
# ============================================================

def trace_graph(question):
    """질문을 그래프에 전달하고 노드 실행 과정을 출력합니다."""

    # 질문을 LangGraph가 사용할 초기 State로 변환합니다.
    initial_state = make_initial_state(question)

    # 그래프를 실행하면서 각 노드의 결과를 순서대로 받습니다.
    steps = app.stream(
        initial_state,
        {
            "recursion_limit": 25
        },
    )

    # 실행된 노드와 각 노드의 결과를 하나씩 확인합니다.
    for step in steps:

        # step에는 현재 실행된 노드 이름과 결과가 들어 있습니다.
        for node_name, update in step.items():

            # 현재 실행된 노드 이름을 출력합니다.
            print()
            print(f"[{node_name}]")

            # 해당 노드가 남긴 로그를 출력합니다.
            logs = update.get("log", [])

            for log in logs:
                print(f"    {log}")

# ============================================================
# 프로그램 시작점
# ============================================================

if __name__ == "__main__":

    # 재시도 상황을 확인하기 위한 질문입니다.
    question = "환불 방법과 대표이사를 알려주세요"

    # 질문을 그래프에 전달하고 전체 실행 과정을 출력합니다.
    trace_graph(question)

실행 결과 샘플 (교안 기록. API 키가 필요한 코드는 키 없이 맞춘 예시이고, 모델의 답은 실행할 때마다 달라집니다)

출력
===== 실행 결과 샘플 =====
경로: retrieve → generate → verify → END
예시 출력 (API 키 필요, 실행하지 않음)
이 파일이 던지는 질문 '환불 방법과 대표이사를 알려주세요'는 기록된 로그에 없습니다.
노드별 로그 문장과 점수는 적지 않습니다.
같은 그래프 계열에서 기록된 질문:
환불은 며칠 이내에 신청해야 하나요? grade=pass retries=0 rewrites=0 elapsed=6.69
대표이사가 누구인가요? grade=giveup rewrites=2 elapsed=3.41 fail_reason=low_score top_score=-0.219

코드 해설

graph27을 stream으로 돌려 노드 이름과 그 노드가 남긴 log를 순서대로 출력합니다.

코드 앞에서 고정해 두는 말입니다.

  • 읽는 키: question으로 만든 초기 State
  • 쓰는 키: 화면 출력만. State를 바꾸지 않습니다.
  • 그래프:
  • START --> retrieve --> generate --> verify
  • |-- pass --> END
  • +-- retry --> bump --> generate --> verify ...
  • 파이썬만 아는 분을 위한 말
  • State는 노드들이 함께 쓰는 딕셔너리입니다. 함수는 state를 읽고, 바꿀 키만 return 합니다.

함수와 클래스로 나누어 읽습니다.

  • trace_graph: 질문을 그래프에 전달하고 노드 실행 과정을 출력합니다.

파일 맨 아래 실행 결과 샘플 주석은 실행되지 않습니다. 화면 출력은 다음 절에 다시 적어 두었습니다.

6-1) 상한 도달 케이스

출처: 원본 PDF 14쪽

출력
■ 검증이 계속 실패하는 경우

[retrieve] ... 3건 통과
[generate] ... 생성
[verify]   ... -> retry
[bump]     ... 재시도 1회차
[generate] ... 생성
[verify]   ... -> retry
[bump]     ... 재시도 2회차
[generate] ... 생성
[verify]   ... -> retry
→ route: retries(2) >= MAX_RETRY(2) → giveup → END

✓ 무한루프에 빠지지 않고 안전하게 종료
← 이전 절27차시 · 5. 실습 ② — ask 함수와 실행 추적다음 절 →27차시 · 7. 실습 ④ — 개선 효과 측정