22차시. RAG를 Graph로 바꾸기
7. 실습 ⑤ — 흐름 확인
출처: 원본 PDF 22차시_RAG를Graph로바꾸기.pdf 12쪽 · 쪽 그림 12쪽
stream으로 실제 경로를 확인해 그래프가 의도대로 도는지 봅니다.
from graph import app
from state import init_state
for q in ["환불은 며칠 이내인가요?", "대표이사가 누구인가요?"]:
print(f"\n{'='*55}\nQ: {q}")
for step in app.stream(init_state(q)):
for node, update in step.items():
keys = list(update.keys())
print(f" [{node}] {keys}")
═════════════════════════════════════════════
══════════
Q: 환불은 며칠 이내인가요?
[retrieve] ['documents', 'scores', 'retrieval_ok', 'log']
[generate] ['answer', 'insufficient', 'has_citation', 'log']
═════════════════════════════════════════════
══════════
Q: 대표이사가 누구인가요?
[retrieve] ['documents', 'scores', 'retrieval_ok', 'log']
[fallback] ['answer', 'grade', 'log']🎯 두 경로가 다르게 흘렀습니다
정상 질문 → retrieve → generate
없는 질문 → retrieve → fallback
조건부 Edge가 제대로 판단하고 있다는 뜻입니다.
15차시 버전에서는 이 흐름이 함수 안에 숨어 있어
눈으로 확인할 수 없었습니다.
실습 코드 · 22/test_migration_stream.py
실습 코드: 실습/src/22/test_migration_stream.py · 위의 원본 PDF 22차시 12쪽 「7️⃣ 실습 ⑤ — 흐름 확인」과 이어집니다.
# ============================================================
# 22차시 | RAG를 Graph로 바꾸기
# 교안: 22차시_RAG를Graph로바꾸기.pptx
# 역할: stream 으로 노드가 끝날 때마다 갱신된 State 키를 출력한다.
# 입력: QUESTIONS 두 개. make_initial_state 로 첫 노트를 만든다.
# 출력: 노드 이름, 그 노드가 돌려준 키 목록. retrieve 이면 점수와 성공 여부.
#
# State = graph.py 가 compile 해 둔 앱의 RAGState.
# Node = retrieve, generate, fallback.
# stream = invoke 처럼 끝까지 모으지 않는다.
# 노드 하나가 끝나면 {노드이름: 그 노드가 return 한 칸} 을 하나씩 준다.
# 조건부 엣지 = retrieval_ok 이면 generate, 아니면 fallback.
#
# 흐름
# START → retrieve
# ├─ generate → END
# └─ fallback → END
# ============================================================
# 이 파일은 LangGraph가 실제로 어떤 경로로 실행되는지 확인합니다.
# 질문을 입력하면 retrieve, generate, fallback 노드의 실행 순서를 화면에 출력합니다.
from graph import app
from graph_state2 import make_initial_state
import warnings
warnings.filterwarnings("ignore")
# ==================================================
# 실행 경로를 확인할 질문 목록
# ==================================================
QUESTIONS = [
# 검색 결과가 있을 것으로 예상되는 질문입니다.
"환불은 며칠 이내인가요?",
# 검색 결과가 없을 것으로 예상되는 질문입니다.
"대표이사가 누구인가요?",
]
# ==================================================
# 하나의 질문에 대해 그래프 실행 과정을 출력하는 함수
# ==================================================
def show_graph_process(question):
"""
질문 하나를 그래프에 전달하고 실행 과정을 출력합니다.
"""
# 질문 제목을 출력합니다.
print()
print("=" * 55)
print("Q:", question)
print("=" * 55)
# 질문을 그래프가 사용할 초기 상태로 만듭니다.
initial_state = make_initial_state(question)
# 그래프를 실행하면서 각 노드의 결과를 하나씩 받습니다.
# stream: 노드가 끝날 때마다 {노드이름: return 한 칸} 을 하나씩 준다.
for step in app.stream(initial_state):
# 현재 실행된 노드와 상태 변경 내용을 확인합니다.
for node, update in step.items():
# 어떤 노드가 실행되었는지 출력합니다.
print(f" [{node}] {list(update.keys())}")
# 검색 노드가 실행된 경우 검색 정보를 출력합니다.
if node == "retrieve":
print(" 검색 점수:", update.get("scores"))
print(
" 검색 성공 여부:",
update.get("retrieval_ok"),
)
# ==================================================
# 모든 질문에 대해 그래프 실행 과정을 확인합니다.
# ==================================================
def main():
"""
질문 목록을 차례대로 실행합니다.
"""
# 질문을 하나씩 꺼냅니다.
for question in QUESTIONS:
# 현재 질문의 그래프 실행 과정을 출력합니다.
show_graph_process(question)
# ==================================================
# 이 파일을 직접 실행했을 때 시작합니다.
# ==================================================
if __name__ == "__main__":
main()실행 결과 샘플 (교안 기록. API 키가 필요한 코드는 키 없이 맞춘 예시이고, 모델의 답은 실행할 때마다 달라집니다)
===== 실행 결과 샘플 =====
실행: cd 실습/src/22
python test_migration_stream.py
예시 출력 (API 키 필요, 실행하지 않음)
=======================================================
Q: 환불은 며칠 이내인가요?
=======================================================
[retrieve] ['documents', 'scores', 'retrieval_ok', 'log']
검색 점수: (유사도 점수 미기록)
검색 성공 여부: (미기록)
[generate] ['answer', 'insufficient', 'has_citation', 'log']
=======================================================
Q: 대표이사가 누구인가요?
=======================================================
[retrieve] ['documents', 'scores', 'retrieval_ok', 'log']
검색 점수: (유사도 점수 미기록)
검색 성공 여부: (미기록)
(검색이 비면 다음은 fallback, 아니면 generate. 어느 쪽인지는 점수 기록이 없어 단정하지 않는다)
기록된 사실만: 환불은 grade pass, rewrites 0, elapsed 6.69.
대표이사는 grade giveup, rewrites 2, elapsed 3.41.
이 파일은 그 세 값을 출력하지 않는다.
경로: retrieve → generate → END코드 해설
stream 으로 노드가 끝날 때마다 갱신된 State 키를 출력한다. 입력은 QUESTIONS 두 개. make_initial_state 로 첫 노트를 만든다.이고, 출력은 노드 이름, 그 노드가 돌려준 키 목록. retrieve 이면 점수와 성공 여부.
주석에 적힌 흐름입니다.
흐름
START → retrieve
├─ generate → END
└─ fallback → END코드 앞에서 고정해 두는 말입니다.
- 22차시 | RAG를 Graph로 바꾸기
- 교안: 22차시_RAG를Graph로바꾸기.pptx
- State = graph.py 가 compile 해 둔 앱의 RAGState.
- Node = retrieve, generate, fallback.
- stream = invoke 처럼 끝까지 모으지 않는다.
- 노드 하나가 끝나면 {노드이름: 그 노드가 return 한 칸} 을 하나씩 준다.
- 조건부 엣지 = retrieval_ok 이면 generate, 아니면 fallback.
함수와 클래스로 나누어 읽습니다.
show_graph_process: 하나의 질문에 대해 그래프 실행 과정을 출력하는 함수main: 모든 질문에 대해 그래프 실행 과정을 확인합니다.
파일 맨 아래 실행 결과 샘플 주석은 실행되지 않습니다. 화면 출력은 다음 절에 다시 적어 두었습니다.
7-1) app.py 연결
■ app.py에서 import 한 줄만 바꾸면 끝
# from rag import ask # 15차시 버전
from graph import ask # 22차시 버전
→ 나머지 코드는 하나도 안 고칩니다
→ python app.py 실행하면 똑같이 동작합니다실습 코드 · 22/app.py
실습 코드: 실습/src/22/app.py · 위의 원본 PDF 22차시 12쪽 「7-1) app.py 연결」과 이어집니다.
# ============================================================
# 22차시 | RAG를 Graph로 바꾸기
# 교안: 22차시_RAG를Graph로바꾸기.pptx
# 역할: 질문 한 줄을 입력받아 ask() 결과에 있는 답변과 출처를 출력한다.
# 입력: input() 한 번. 반복 루프는 없다.
# 출력: 답변 본문, 출처 딕셔너리를 한 줄씩
#
# 이 파일에는 State, Node, Edge 정의가 없다.
# ../rag_app/graph.py 의 ask() 가 그래프를 invoke 한다.
# 그 그래프의 흐름은 아래와 같다.
#
# START → retrieve
# ├─ generate → END
# └─ fallback → END
# ============================================================
# 이 파일은 사용자의 질문을 받아 LangGraph 기반 RAG를 실행합니다.
# 실행 결과로 답변과 답변에 사용된 문서 출처를 화면에 출력합니다.
import os
import sys
# ==================================================
# graph.py 파일을 찾기 위한 경로 설정
# ==================================================
# 현재 app.py 파일이 있는 폴더입니다.
CURRENT_DIR = os.path.dirname(__file__)
# graph.py가 있는 rag_app 폴더의 경로입니다.
RAG_APP_DIR = os.path.join(
CURRENT_DIR,
"..",
"rag_app",
)
# Python이 rag_app 폴더의 모듈을 찾도록 경로를 추가합니다.
sys.path.insert(0, RAG_APP_DIR)
# LangGraph 기반 RAG의 ask() 함수를 가져옵니다.
from graph import ask
# ==================================================
# 질문을 받고 답변을 출력하는 함수
# ==================================================
def main():
"""
사용자에게 질문을 받고 RAG 답변과 출처를 출력합니다.
"""
# 사용자에게 질문을 입력받습니다.
question = input("질문을 입력하세요: ")
# 입력받은 질문을 RAG 시스템에 전달합니다.
result = ask(question)
# 최종 답변을 출력합니다.
print()
print("답변:")
print(result["answer"])
# 답변에 사용된 출처를 출력합니다.
print()
print("출처:")
# 출처를 하나씩 꺼내 출력합니다.
for source in result["sources"]:
print(source)
# ==================================================
# 이 파일을 직접 실행했을 때 시작합니다.
# ==================================================
if __name__ == "__main__":
main()실행 결과 샘플 (교안 기록. API 키가 필요한 코드는 키 없이 맞춘 예시이고, 모델의 답은 실행할 때마다 달라집니다)
===== 실행 결과 샘플 =====
실행: cd 실습/src/22
python app.py
예시 출력 (API 키 필요, 실행하지 않음)
질문을 입력하세요: 환불은 며칠 이내인가요?
답변:
(답변 본문은 기록되어 있지 않다)
출처:
(파일명과 페이지는 기록되어 있지 않다)
기록된 사실만: 이 질문은 grade pass, rewrites 0, elapsed 6.69.
app.py 는 그 세 값을 출력하지 않는다.
경로: retrieve → generate → END코드 해설
질문 한 줄을 입력받아 ask() 결과에 있는 답변과 출처를 출력한다. 입력은 input() 한 번. 반복 루프는 없다.이고, 출력은 답변 본문, 출처 딕셔너리를 한 줄씩입니다.
코드 앞에서 고정해 두는 말입니다.
- 22차시 | RAG를 Graph로 바꾸기
- 교안: 22차시_RAG를Graph로바꾸기.pptx
- 이 파일에는 State, Node, Edge 정의가 없다.
- ../rag_app/graph.py 의 ask() 가 그래프를 invoke 한다.
- 그 그래프의 흐름은 아래와 같다.
- START → retrieve
- ├─ generate → END
- └─ fallback → END
함수와 클래스로 나누어 읽습니다.
main: 질문을 받고 답변을 출력하는 함수
파일 맨 아래 실행 결과 샘플 주석은 실행되지 않습니다. 화면 출력은 다음 절에 다시 적어 두었습니다.