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️⃣ 실습 ③ — 루프가 도는 것 확인」과 이어집니다.
# ==============================================================
# 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
✓ 무한루프에 빠지지 않고 안전하게 종료