22차시. RAG를 Graph로 바꾸기
4. 실습 ② — 그래프 완성
출처: 원본 PDF 22차시_RAG를Graph로바꾸기.pdf 6~7쪽 · 쪽 그림 6쪽 7쪽
📝 src/graph.py
from langgraph.graph import StateGraph, START, END
from state import RAGState, init_state
from nodes.retrieve import retrieve_node
from nodes.generate import generate_node
from nodes.fallback import fallback_node
# ── 라우팅 ──
def route_after_retrieve(state: RAGState) -> str:
return "ok" if state.get("retrieval_ok") else "empty"
# ── 조립 ──
def build_graph():
g = StateGraph(RAGState)
g.add_node("retrieve", retrieve_node)
g.add_node("generate", generate_node)
g.add_node("fallback", fallback_node)
g.add_edge(START, "retrieve")
g.add_conditional_edges(
"retrieve", route_after_retrieve,
{"ok": "generate", "empty": "fallback"})
g.add_edge("generate", END)
g.add_edge("fallback", END)
# ─────────────────────────────────────────
# 3부에서 여기에 추가됩니다
# 26차시 verify 노드
# 27차시 bump 노드 + 재생성 루프
# 30차시 rewrite 노드 + 재검색 루프
# ─────────────────────────────────────────
return g.compile()
app = build_graph()
def ask(question: str) -> dict:
# 15차시 ask()와 같은 인터페이스를 유지한다
final = app.invoke(init_state(question),
{"recursion_limit": 20})
return {
"answer": final["answer"],
"sources": [{"file": d.metadata["filename"],
"page": d.metadata["page_no"]}
for d in final.get("documents", [])],
"cited": final.get("has_citation", False),
"log": final.get("log", []),
"ok": True,
}
if __name__ == "__main__":
print(app.get_graph().draw_ascii())실행 결과 예시 (코드를 읽고 만든 예상 화면입니다. 실제 실행 결과, 특히 모델의 답은 다를 수 있습니다)
+-----------+
| __start__ |
+-----------+
*
*
+----------+
| retrieve |
+----------+
.. ..
.. ..
+----------+ +----------+
| generate | | fallback |
+----------+ +----------+
** **
* *
+---------+
| __end__ |
+---------+🔑 ask() 인터페이스를 그대로 유지한 이유
15차시에서 만든 app.py를 안 고쳐도 되게 하려는 것입니다.
from rag import ask → from graph import ask
import 한 줄만 바꾸면 화면은 그대로 동작합니다.
이것이 15차시에서 모듈을 나눠둔 보상입니다.
실습 코드 · 22/graph_step_by_step.py
실습 코드: 실습/src/22/graph_step_by_step.py · 위의 원본 PDF 22차시 6~7쪽 「4️⃣ 실습 ② — 그래프 완성」과 이어집니다.
# ============================================================
# 22차시 | RAG를 Graph로 바꾸기
# 교안: 22차시_RAG를Graph로바꾸기.pptx
# 역할: 검색만, 검색+생성, 검색 후 갈림길 순으로 그래프를 세 번 키워 본다.
# 입력: make_initial_state(질문) 이 만든 RAGState
# 출력: 1단계 검색 건수와 점수, 2·3단계 답변과 log, 마지막에 세 그래프의 문자 그림
#
# State = 18차시 RAGState. 노드가 고친 칸만 갱신되는 공유 노트.
# Node = retrieve, generate, fallback (19차시 함수).
# Edge = 단계마다 다르다. 1·2단계는 직선, 3단계는 조건부 엣지.
# 조건부 엣지 = route_after_retrieve.
# retrieval_ok 이면 "generate" 노드, 아니면 "fallback" 노드.
# (20차시 routes.py 의 "ok"/"empty" 와 반환 문자열이 다르다.)
# compile = 각 build_step 함수가 앱을 반환한다.
# invoke = 초기 State를 넣고 END까지의 최종 State를 한 번에 받는다.
# log reducer = 노드가 돌려준 log 리스트는 기존 log 뒤에 붙는다.
#
# 1단계
# START → retrieve → END
# 2단계
# START → retrieve → generate → END
# 3단계
# START → retrieve
# ├─ generate → generate노드 → END
# └─ fallback → fallback노드 → END
# ============================================================
# 이 파일은 RAG 그래프를 세 단계로 나누어 만들어 보는 실습 파일입니다.
# 검색 노드만 연결한 뒤, 답변 생성과 fallback을 차례대로 추가하며 각 단계를 확인합니다.
import os
import sys
import warnings
warnings.filterwarnings("ignore")
# ==================================================
# 다른 차시 폴더의 모듈을 찾기 위한 설정
# ==================================================
# 현재 파일이 있는 폴더입니다.
CURRENT_DIR = os.path.dirname(__file__)
# 18차시 폴더를 모듈 검색 경로에 추가합니다.
sys.path.insert(
0,
os.path.join(CURRENT_DIR, "..", "18")
)
# 19차시 폴더를 모듈 검색 경로에 추가합니다.
sys.path.insert(
0,
os.path.join(CURRENT_DIR, "..", "19")
)
# ==================================================
# 필요한 모듈 가져오기
# ==================================================
# LangGraph 그래프 구성 도구입니다.
from langgraph.graph import StateGraph, START, END
# 그래프가 사용할 상태 형식입니다.
from graph_state import RAGState
# 질문을 초기 상태로 만드는 함수입니다.
from graph_state2 import make_initial_state
# 각 작업을 수행하는 노드 함수입니다.
from retrieve import retrieve_node
from generate import generate_node
from fallback import fallback_node
# ==================================================
# 검색 결과에 따라 다음 노드를 결정하는 함수
# ==================================================
# 조건부 엣지 함수. 노드가 아니다.
# 읽기: retrieval_ok
# 쓰기: 없음
# 반환 "generate" → generate 노드, "fallback" → fallback 노드
def route_after_retrieve(state):
"""
검색에 성공하면 generate로 이동하고,
검색에 실패하면 fallback으로 이동합니다.
"""
# 검색 성공 여부를 확인합니다.
if state.get("retrieval_ok"):
return "generate"
# 검색 실패 시 fallback으로 이동합니다.
return "fallback"
# ==================================================
# 1단계: retrieve 노드만 연결
# ==================================================
def build_step1_graph():
"""
검색 노드만 연결한 가장 간단한 그래프를 만듭니다.
"""
# RAGState를 사용하는 그래프를 만듭니다.
graph = StateGraph(RAGState)
# 검색 노드를 등록합니다.
# 노드 retrieve: 읽기 query/question/top_k, 쓰기 documents/scores/retrieval_ok/log
graph.add_node("retrieve", retrieve_node)
# START → retrieve → END로 연결합니다.
graph.add_edge(START, "retrieve")
graph.add_edge("retrieve", END)
# 실행 가능한 그래프로 완성합니다.
# compile: 이 단계의 노드와 엣지를 실행 앱으로 확정한다.
return graph.compile()
# ==================================================
# 1단계 그래프 실행
# ==================================================
def test_step1():
"""
검색 노드가 문서를 제대로 찾는지 확인합니다.
"""
print()
print("=" * 60)
print("1단계: retrieve 노드만 연결")
print("=" * 60)
# 1단계 그래프를 만듭니다.
app = build_step1_graph()
# 질문을 초기 상태로 만들어 그래프에 전달합니다.
result = app.invoke(
make_initial_state("환불은 며칠 이내인가요?")
)
# 검색된 문서 개수를 출력합니다.
print("검색 결과:", len(result.get("documents", [])), "건")
# 검색 점수를 출력합니다.
print("점수:", result.get("scores", []))
# 검색 성공 여부를 출력합니다.
print("검색 성공 여부:", result.get("retrieval_ok"))
# ==================================================
# 2단계: retrieve와 generate 연결
# ==================================================
def build_step2_graph():
"""
검색 후 답변을 생성하는 그래프를 만듭니다.
"""
# RAGState를 사용하는 그래프를 만듭니다.
graph = StateGraph(RAGState)
# 검색 노드를 등록합니다.
# 노드 retrieve: 읽기 query/question/top_k, 쓰기 documents/scores/retrieval_ok/log
graph.add_node("retrieve", retrieve_node)
# 답변 생성 노드를 등록합니다.
# 노드 generate: 읽기 documents/question, 쓰기 answer/insufficient/has_citation/log
graph.add_node("generate", generate_node)
# START → retrieve → generate → END로 연결합니다.
graph.add_edge(START, "retrieve")
graph.add_edge("retrieve", "generate")
graph.add_edge("generate", END)
# 실행 가능한 그래프로 완성합니다.
# compile: 이 단계의 노드와 엣지를 실행 앱으로 확정한다.
return graph.compile()
# ==================================================
# 2단계 그래프 실행
# ==================================================
def test_step2():
"""
검색 결과를 이용해 답변을 생성하는지 확인합니다.
"""
print()
print("=" * 60)
print("2단계: generate 노드 추가")
print("=" * 60)
# 2단계 그래프를 만듭니다.
app = build_step2_graph()
# 질문을 초기 상태로 만들어 그래프에 전달합니다.
result = app.invoke(
make_initial_state("환불은 며칠 이내인가요?")
)
# 생성된 답변을 출력합니다.
print("답변:")
print(result.get("answer", ""))
# ==================================================
# 3단계: 조건 분기와 fallback 추가
# ==================================================
def build_step3_graph():
"""
검색 성공과 검색 실패를 나누는 최종 그래프를 만듭니다.
"""
# RAGState를 사용하는 그래프를 만듭니다.
graph = StateGraph(RAGState)
# 검색 노드를 등록합니다.
# 노드 retrieve: 읽기 query/question/top_k, 쓰기 documents/scores/retrieval_ok/log
graph.add_node("retrieve", retrieve_node)
# 답변 생성 노드를 등록합니다.
# 노드 generate: 읽기 documents/question, 쓰기 answer/insufficient/has_citation/log
graph.add_node("generate", generate_node)
# 검색 실패 처리 노드를 등록합니다.
# 노드 fallback: 읽기 tried_queries, 쓰기 answer/grade/log
graph.add_node("fallback", fallback_node)
# 그래프가 시작되면 검색부터 실행합니다.
graph.add_edge(START, "retrieve")
# 검색 결과에 따라 다음 노드를 결정합니다.
graph.add_conditional_edges(
"retrieve",
route_after_retrieve,
{
# 검색 성공 시 답변을 생성합니다.
"generate": "generate",
# 검색 실패 시 fallback을 실행합니다.
"fallback": "fallback",
},
)
# 답변 생성 후 그래프를 종료합니다.
graph.add_edge("generate", END)
# fallback 처리 후 그래프를 종료합니다.
graph.add_edge("fallback", END)
# 실행 가능한 그래프로 완성합니다.
# compile: 이 단계의 노드와 엣지를 실행 앱으로 확정한다.
return graph.compile()
# ==================================================
# 3단계 그래프 실행
# ==================================================
def test_step3():
"""
정상 질문과 검색 실패 질문의 경로를 확인합니다.
"""
print()
print("=" * 60)
print("3단계: 조건 분기와 fallback 추가")
print("=" * 60)
# 3단계 그래프를 만듭니다.
app = build_step3_graph()
# ----------------------------------------------
# 정상 질문 테스트
# ----------------------------------------------
print()
print("[정상 질문]")
# 정상적인 질문을 그래프에 전달합니다.
result1 = app.invoke(
make_initial_state("환불은 며칠 이내인가요?")
)
# 생성된 답변을 출력합니다.
print("답변:")
print(result1.get("answer", ""))
# 실행 과정을 출력합니다.
print("실행 로그:", result1.get("log", []))
# ----------------------------------------------
# 검색 실패 질문 테스트
# ----------------------------------------------
print()
print("[검색 실패 질문]")
# 문서에 없는 질문을 그래프에 전달합니다.
result2 = app.invoke(
make_initial_state("대표이사가 누구인가요?")
)
# fallback 답변을 출력합니다.
print("답변:")
print(result2.get("answer", ""))
# 실행 과정을 출력합니다.
print("실행 로그:", result2.get("log", []))
# ==================================================
# 세 단계 그래프의 구조 출력
# ==================================================
def print_graphs():
"""
각 단계의 그래프 구조를 화면에 출력합니다.
"""
print()
print("=" * 60)
print("그래프 구조 확인")
print("=" * 60)
# 1단계 그래프 구조를 출력합니다.
print()
print("[1단계 그래프]")
app1 = build_step1_graph()
print(app1.get_graph().draw_ascii())
# 2단계 그래프 구조를 출력합니다.
print()
print("[2단계 그래프]")
app2 = build_step2_graph()
print(app2.get_graph().draw_ascii())
# 3단계 그래프 구조를 출력합니다.
print()
print("[3단계 그래프]")
app3 = build_step3_graph()
print(app3.get_graph().draw_ascii())
# ==================================================
# 프로그램 시작
# ==================================================
if __name__ == "__main__":
# 1단계 그래프를 실행합니다.
test_step1()
# 2단계 그래프를 실행합니다.
test_step2()
# 3단계 그래프를 실행합니다.
test_step3()
# 세 그래프의 구조를 출력합니다.
print_graphs()실행 결과 샘플 (교안 기록. API 키가 필요한 코드는 키 없이 맞춘 예시이고, 모델의 답은 실행할 때마다 달라집니다)
===== 실행 결과 샘플 =====
실행: cd 실습/src/22
python graph_step_by_step.py
예시 출력 (API 키 필요, 실행하지 않음)
============================================================
1단계: retrieve 노드만 연결
============================================================
검색 결과: (건수 미기록) 건
점수: (유사도 점수 미기록)
검색 성공 여부: (미기록)
============================================================
2단계: generate 노드 추가
============================================================
답변:
(본문 미기록)
============================================================
3단계: 조건 분기와 fallback 추가
============================================================
[정상 질문]
답변:
(본문 미기록)
실행 로그: (retrieve/generate 로그 문장 미기록)
[검색 실패 질문]
답변:
(fallback 이면 안내 문장. 본문 미기록)
실행 로그: (retrieve/fallback 로그 문장 미기록)
============================================================
그래프 구조 확인
============================================================
[1단계 그래프]
(draw_ascii: __start__ → retrieve → __end__)
[2단계 그래프]
(draw_ascii: __start__ → retrieve → generate → __end__)
[3단계 그래프]
(draw_ascii: __start__ → retrieve 에서 generate / fallback 으로 갈라져 __end__)
기록된 사실만: 환불은 grade pass, rewrites 0, elapsed 6.69.
대표이사는 grade giveup, rewrites 2, elapsed 3.41.
이 파일은 grade, rewrites, elapsed 를 출력하지 않는다. 점수는 적지 않는다.
경로: retrieve → generate → END코드 해설
검색만, 검색+생성, 검색 후 갈림길 순으로 그래프를 세 번 키워 본다. 입력은 make_initial_state(질문) 이 만든 RAGState이고, 출력은 1단계 검색 건수와 점수, 2·3단계 답변과 log, 마지막에 세 그래프의 문자 그림입니다.
코드 앞에서 고정해 두는 말입니다.
- 22차시 | RAG를 Graph로 바꾸기
- 교안: 22차시_RAG를Graph로바꾸기.pptx
- State = 18차시 RAGState. 노드가 고친 칸만 갱신되는 공유 노트.
- Node = retrieve, generate, fallback (19차시 함수).
- Edge = 단계마다 다르다. 1·2단계는 직선, 3단계는 조건부 엣지.
- 조건부 엣지 = route_after_retrieve.
- retrieval_ok 이면 "generate" 노드, 아니면 "fallback" 노드.
- (20차시 routes.py 의 "ok"/"empty" 와 반환 문자열이 다르다.)
함수와 클래스로 나누어 읽습니다.
route_after_retrieve: 검색 결과에 따라 다음 노드를 결정하는 함수 조건부 엣지 함수. 노드가 아니다. 읽기: retrieval_ok 쓰기: 없음 반환 "generate" → generate 노드, "fallback" → fallback 노드build_step1_graph: 1단계: retrieve 노드만 연결test_step1: 1단계 그래프 실행build_step2_graph: 2단계: retrieve와 generate 연결test_step2: 2단계 그래프 실행build_step3_graph: 3단계: 조건 분기와 fallback 추가test_step3: 3단계 그래프 실행print_graphs: 세 단계 그래프의 구조 출력
파일 맨 아래 실행 결과 샘플 주석은 실행되지 않습니다. 화면 출력은 다음 절에 다시 적어 두었습니다.
실습 코드 · 22/graph.py
실습 코드: 실습/src/22/graph.py · 위의 원본 PDF 22차시 6~7쪽 「4️⃣ 실습 ② — 그래프 완성」과 이어집니다.
# ============================================================
# 22차시 | RAG를 Graph로 바꾸기
# 교안: 22차시_RAG를Graph로바꾸기.pptx
# 역할: 검색, 생성, 안내를 하나의 그래프로 잇고 ask() 로 질문만 받게 한다.
# 입력: ask(question)
# 출력: 직접 실행하면 문자 그래프. ask() 는 answer, sources, cited, log, ok.
#
# State = RAGState. make_initial_state 가 첫 노트를 만든다.
# Node = retrieve, generate, fallback.
# Edge = START → retrieve, generate → END, fallback → END.
# 조건부 엣지 = retrieval_ok 이면 "generate", 아니면 "fallback".
# compile = 파일을 불러올 때 build_graph() 로 앱을 한 번 만든다.
# invoke = ask() 안에서 초기 State로 END까지 실행한다.
# 이 파일을 직접 실행하면 invoke 하지 않고 draw_ascii 만 출력한다.
#
# 노드 칸
# retrieve: 읽기 query/question/top_k
# 쓰기 documents/scores/retrieval_ok/log
# generate: 읽기 documents/question
# 쓰기 answer/insufficient/has_citation/log
# fallback: 읽기 tried_queries
# 쓰기 answer/grade/log
#
# 흐름
# START → retrieve
# ├─ generate → generate노드 → END
# └─ fallback → fallback노드 → END
# ============================================================
# 이 파일은 LangGraph로 RAG의 검색·답변 생성·fallback 흐름을 연결합니다.
# 외부에서는 ask() 함수에 질문만 전달하면 최종 답변을 받을 수 있습니다.
import os
import sys
# ==================================================
# 다른 차시 폴더의 모듈을 가져오기 위한 설정
# ==================================================
# 현재 graph.py 파일이 있는 폴더 경로입니다.
CURRENT_DIR = os.path.dirname(__file__)
# 18차시 폴더에서 graph_state.py를 찾도록 설정합니다.
PATH_18 = os.path.join(CURRENT_DIR, "..", "18")
sys.path.insert(0, PATH_18)
# 19차시 폴더에서 나머지 노드 파일을 찾도록 설정합니다.
PATH_19 = os.path.join(CURRENT_DIR, "..", "19")
sys.path.insert(0, PATH_19)
# ==================================================
# 필요한 모듈 가져오기
# ==================================================
# LangGraph 그래프를 만드는 도구입니다.
from langgraph.graph import StateGraph, START, END
# 그래프가 사용할 상태 형식입니다.
from graph_state import RAGState
# 질문을 초기 상태로 바꾸는 함수입니다.
from graph_state2 import make_initial_state
# 각 작업을 담당하는 노드 함수입니다.
from retrieve import retrieve_node
from generate import generate_node
from fallback import fallback_node
# ==================================================
# 검색 결과에 따라 다음 노드를 결정하는 함수
# ==================================================
# 조건부 엣지 함수. 노드가 아니다.
# 읽기: retrieval_ok
# 쓰기: 없음
# 반환 "generate" → generate 노드, "fallback" → fallback 노드
def route_after_retrieve(state):
"""
검색 결과가 있으면 generate로 이동합니다.
검색 결과가 없으면 fallback으로 이동합니다.
"""
# 검색에 성공했는지 확인합니다.
retrieval_ok = state["retrieval_ok"]
# 검색 성공이면 답변 생성 노드로 이동합니다.
if retrieval_ok:
return "generate"
# 검색 실패이면 fallback 노드로 이동합니다.
return "fallback"
# ==================================================
# RAG 그래프를 만드는 함수
# ==================================================
def build_graph():
"""
검색, 답변 생성, fallback 노드를 하나의 그래프로 연결합니다.
"""
# RAGState를 사용하는 그래프를 만듭니다.
graph = StateGraph(RAGState)
# 검색 노드를 등록합니다.
# 노드 retrieve: 읽기 query/question/top_k, 쓰기 documents/scores/retrieval_ok/log
graph.add_node("retrieve", retrieve_node)
# 답변 생성 노드를 등록합니다.
# 노드 generate: 읽기 documents/question, 쓰기 answer/insufficient/has_citation/log
graph.add_node("generate", generate_node)
# 검색 실패 처리 노드를 등록합니다.
# 노드 fallback: 읽기 tried_queries, 쓰기 answer/grade/log
graph.add_node("fallback", fallback_node)
# 그래프가 시작되면 retrieve부터 실행합니다.
graph.add_edge(START, "retrieve")
# 검색 결과에 따라 다음 노드를 결정합니다.
graph.add_conditional_edges(
"retrieve",
route_after_retrieve,
{
# 검색 성공 시 generate를 실행합니다.
"generate": "generate",
# 검색 실패 시 fallback을 실행합니다.
"fallback": "fallback",
},
)
# 답변 생성이 끝나면 그래프를 종료합니다.
graph.add_edge("generate", END)
# fallback 처리가 끝나면 그래프를 종료합니다.
graph.add_edge("fallback", END)
# 그래프를 실제로 실행할 수 있는 형태로 완성합니다.
# compile: 엣지를 실행 앱으로 확정한다.
return graph.compile()
# 프로그램이 시작될 때 그래프를 한 번 만듭니다.
app = build_graph()
# ==================================================
# 외부에서 사용하는 대표 함수
# ==================================================
def ask(question):
"""
질문을 그래프에 전달하고 최종 결과를 반환합니다.
"""
# 질문을 LangGraph가 사용할 초기 상태로 만듭니다.
initial_state = make_initial_state(question)
# 초기 상태를 그래프에 전달해 실행합니다.
# invoke: 초기 State를 넣고 END까지의 최종 State를 받는다.
result = app.invoke(initial_state)
# 출처 정보를 저장할 빈 리스트를 만듭니다.
sources = []
# 검색된 문서를 하나씩 확인합니다.
for document in result.get("documents", []):
source = {
"file": document.metadata["filename"],
"page": document.metadata["page_no"],
}
# 파일명과 페이지 정보를 출처 목록에 추가합니다.
sources.append(source)
# 외부 프로그램에서 사용할 결과를 반환합니다.
return {
# 최종 답변입니다.
"answer": result["answer"],
# 답변 생성에 사용된 문서 출처입니다.
"sources": sources,
# 답변에 인용이 포함되었는지 나타냅니다.
"cited": result.get("has_citation", False),
# 그래프가 실행된 순서 기록입니다.
"log": result.get("log", []),
# 함수가 정상적으로 실행되었음을 나타냅니다.
"ok": True,
}
# ==================================================
# 이 파일을 직접 실행했을 때의 동작
# ==================================================
if __name__ == "__main__":
# 그래프의 노드와 연결 구조를 화면에 출력합니다.
print(app.get_graph().draw_ascii())실행 결과 샘플 (교안 기록. API 키가 필요한 코드는 키 없이 맞춘 예시이고, 모델의 답은 실행할 때마다 달라집니다)
===== 실행 결과 샘플 =====
실행: cd 실습/src/22
python graph.py
예시 출력 (API 키 필요, 실행하지 않음)
(draw_ascii: __start__ → retrieve 에서 generate 와 fallback 으로 갈라져 __end__)
경로: retrieve → generate → END코드 해설
검색, 생성, 안내를 하나의 그래프로 잇고 ask() 로 질문만 받게 한다. 입력은 ask(question)이고, 출력은 직접 실행하면 문자 그래프. ask() 는 answer, sources, cited, log, ok.
주석에 적힌 흐름입니다.
흐름
START → retrieve
├─ generate → generate노드 → END
└─ fallback → fallback노드 → END코드 앞에서 고정해 두는 말입니다.
- 22차시 | RAG를 Graph로 바꾸기
- 교안: 22차시_RAG를Graph로바꾸기.pptx
- State = RAGState. make_initial_state 가 첫 노트를 만든다.
- Node = retrieve, generate, fallback.
- Edge = START → retrieve, generate → END, fallback → END.
- 조건부 엣지 = retrieval_ok 이면 "generate", 아니면 "fallback".
- compile = 파일을 불러올 때 build_graph() 로 앱을 한 번 만든다.
- invoke = ask() 안에서 초기 State로 END까지 실행한다.
함수와 클래스로 나누어 읽습니다.
route_after_retrieve: 검색 결과에 따라 다음 노드를 결정하는 함수 조건부 엣지 함수. 노드가 아니다. 읽기: retrieval_ok 쓰기: 없음 반환 "generate" → generate 노드, "fallback" → fallback 노드build_graph: RAG 그래프를 만드는 함수ask: 외부에서 사용하는 대표 함수
파일 맨 아래 실행 결과 샘플 주석은 실행되지 않습니다. 화면 출력은 다음 절에 다시 적어 두었습니다.
실습 코드 · 22/graph22.py
실습 코드: 실습/src/22/graph22.py · 위의 원본 PDF 22차시 6~7쪽 「4️⃣ 실습 ② — 그래프 완성」과 이어집니다.
# ============================================================
# 22차시 | RAG를 Graph로 바꾸기
# 교안: 22차시_RAG를Graph로바꾸기.pptx
# 역할: graph.py 와 같은 갈림길에 recursion_limit 을 두고, 출처 키 이름을 여러 후보에서 찾는다.
# 입력: ask(question)
# 출력: 직접 실행하면 문자 그래프. ask() 는 answer, sources, cited, log, ok.
#
# State = RAGState. 공유 노트.
# Node = retrieve, generate, fallback. get_source 는 노드가 아니다.
# Edge = START → retrieve → (조건부) generate 또는 fallback → END.
# 조건부 엣지 = route_after_retrieve. 반환 문자열이 다음 노드 이름과 같다.
# compile = import 때 한 번. invoke = ask() 에서 recursion_limit 20 으로 실행.
#
# 노드 칸
# retrieve: 읽기 query/question/top_k, 쓰기 documents/scores/retrieval_ok/log
# generate: 읽기 documents/question, 쓰기 answer/insufficient/has_citation/log
# fallback: 읽기 tried_queries, 쓰기 answer/grade/log
#
# 흐름
# START → retrieve
# ├─ generate → END
# └─ fallback → END
# ============================================================
# 이 파일은 LangGraph로 검색, 답변 생성, fallback 흐름을 연결합니다.
# 질문은 ask() 함수로 전달하며, 그래프 실행 결과를 답변과 출처 형태로 반환합니다.
import os
import sys
# ==================================================
# 다른 차시 폴더의 모듈을 찾기 위한 설정
# ==================================================
# 현재 파일이 있는 폴더입니다.
CURRENT_DIR = os.path.dirname(__file__)
# 18차시 폴더를 모듈 검색 경로에 추가합니다.
sys.path.insert(
0,
os.path.join(CURRENT_DIR, "..", "18")
)
# 19차시 폴더를 모듈 검색 경로에 추가합니다.
sys.path.insert(
0,
os.path.join(CURRENT_DIR, "..", "19")
)
# ==================================================
# 필요한 모듈 가져오기
# ==================================================
# LangGraph 그래프를 만드는 도구입니다.
from langgraph.graph import StateGraph, START, END
# 그래프가 사용할 상태 형식입니다.
from graph_state import RAGState
# 질문을 초기 상태로 만드는 함수입니다.
from graph_state2 import make_initial_state
# 각 작업을 담당하는 노드 함수입니다.
from retrieve import retrieve_node
from generate import generate_node
from fallback import fallback_node
# ==================================================
# 검색 결과에 따라 다음 노드를 결정하는 함수
# ==================================================
# 조건부 엣지 함수. 노드가 아니다.
# 읽기: retrieval_ok
# 쓰기: 없음
# 반환 "generate" → generate 노드, "fallback" → fallback 노드
def route_after_retrieve(state):
"""
검색 성공 여부를 확인하고 다음 노드를 결정합니다.
"""
# 검색 성공이면 generate 노드로 이동합니다.
if state.get("retrieval_ok"):
return "generate"
# 검색 실패이면 fallback 노드로 이동합니다.
return "fallback"
# ==================================================
# LangGraph를 만드는 함수
# ==================================================
def build_graph():
"""
retrieve, generate, fallback 노드를 연결합니다.
"""
# RAGState를 사용하는 그래프를 만듭니다.
graph = StateGraph(RAGState)
# 검색 노드를 등록합니다.
# 노드 retrieve: 읽기 query/question/top_k, 쓰기 documents/scores/retrieval_ok/log
graph.add_node("retrieve", retrieve_node)
# 답변 생성 노드를 등록합니다.
# 노드 generate: 읽기 documents/question, 쓰기 answer/insufficient/has_citation/log
graph.add_node("generate", generate_node)
# 검색 실패 처리 노드를 등록합니다.
# 노드 fallback: 읽기 tried_queries, 쓰기 answer/grade/log
graph.add_node("fallback", fallback_node)
# 그래프가 시작되면 검색부터 실행합니다.
graph.add_edge(START, "retrieve")
# 검색 결과에 따라 다음 노드를 선택합니다.
graph.add_conditional_edges(
"retrieve",
route_after_retrieve,
{
# 검색 성공 시 답변을 생성합니다.
"generate": "generate",
# 검색 실패 시 안내 메시지를 반환합니다.
"fallback": "fallback",
},
)
# 답변 생성이 끝나면 그래프를 종료합니다.
graph.add_edge("generate", END)
# fallback 처리가 끝나면 그래프를 종료합니다.
graph.add_edge("fallback", END)
# 그래프를 실행 가능한 형태로 완성합니다.
# compile: 엣지를 실행 앱으로 확정한다.
return graph.compile()
# 프로그램이 시작될 때 그래프를 한 번 만듭니다.
app = build_graph()
# ==================================================
# 문서 하나에서 출처 정보를 가져오는 함수
# ==================================================
# 노드가 아님. 문서 메타데이터에서 파일명과 페이지를 고른다.
# 읽기: document.metadata
# 쓰기: 없음 (sources 리스트에 넣을 딕셔너리를 반환)
def get_source(document):
"""
문서의 메타데이터에서 파일명과 페이지 번호를 가져옵니다.
"""
# 문서의 메타데이터를 가져옵니다.
metadata = document.metadata
# 파일명 후보를 순서대로 확인합니다.
filename = metadata.get("filename")
if not filename:
filename = metadata.get("file_name")
if not filename:
filename = metadata.get("source")
if not filename:
filename = "unknown"
# 페이지 번호 후보를 순서대로 확인합니다.
page = metadata.get("page_no")
if not page:
page = metadata.get("page")
if not page:
page = metadata.get("pageno")
if not page:
page = "?"
# 파일명과 페이지 번호를 하나의 딕셔너리로 반환합니다.
return {
"file": filename,
"page": page,
}
# ==================================================
# 외부에서 사용하는 대표 함수
# ==================================================
def ask(question):
"""
질문을 그래프에 전달하고 최종 답변을 반환합니다.
"""
# 질문을 그래프가 사용할 초기 상태로 만듭니다.
initial_state = make_initial_state(question)
# 초기 상태를 그래프에 전달하여 실행합니다.
# invoke: recursion_limit 은 반복 상한. 이 그래프는 한 번 갈라진 뒤 END 로 간다.
final = app.invoke(
initial_state,
{
# 그래프가 너무 오래 반복되지 않도록 제한합니다.
"recursion_limit": 20
},
)
# 출처를 저장할 빈 리스트를 만듭니다.
sources = []
# 검색된 문서를 하나씩 확인합니다.
for document in final.get("documents", []):
# 문서에서 파일명과 페이지 정보를 가져옵니다.
source = get_source(document)
# 출처 목록에 추가합니다.
sources.append(source)
# 외부 프로그램에서 사용할 결과를 반환합니다.
return {
# 최종 답변입니다.
"answer": final["answer"],
# 답변 생성에 사용된 문서 목록입니다.
"sources": sources,
# 답변에 인용이 포함되었는지 나타냅니다.
"cited": final.get("has_citation", False),
# 그래프가 실행된 과정을 기록합니다.
"log": final.get("log", []),
# 함수가 정상적으로 실행되었음을 나타냅니다.
"ok": True,
}
# ==================================================
# 이 파일을 직접 실행했을 때의 동작
# ==================================================
if __name__ == "__main__":
# 그래프의 노드와 연결 구조를 출력합니다.
print(app.get_graph().draw_ascii())실행 결과 샘플 (교안 기록. API 키가 필요한 코드는 키 없이 맞춘 예시이고, 모델의 답은 실행할 때마다 달라집니다)
===== 실행 결과 샘플 =====
실행: cd 실습/src/22
python graph22.py
예시 출력 (API 키 필요, 실행하지 않음)
(draw_ascii: __start__ → retrieve 에서 generate 와 fallback 으로 갈라져 __end__)
경로: retrieve → generate → END코드 해설
graph.py 와 같은 갈림길에 recursion_limit 을 두고, 출처 키 이름을 여러 후보에서 찾는다. 입력은 ask(question)이고, 출력은 직접 실행하면 문자 그래프. ask() 는 answer, sources, cited, log, ok.
주석에 적힌 흐름입니다.
흐름
START → retrieve
├─ generate → END
└─ fallback → END코드 앞에서 고정해 두는 말입니다.
- 22차시 | RAG를 Graph로 바꾸기
- 교안: 22차시_RAG를Graph로바꾸기.pptx
- State = RAGState. 공유 노트.
- Node = retrieve, generate, fallback. get_source 는 노드가 아니다.
- Edge = START → retrieve → (조건부) generate 또는 fallback → END.
- 조건부 엣지 = route_after_retrieve. 반환 문자열이 다음 노드 이름과 같다.
- compile = import 때 한 번. invoke = ask() 에서 recursion_limit 20 으로 실행.
- 노드 칸
함수와 클래스로 나누어 읽습니다.
route_after_retrieve: 검색 결과에 따라 다음 노드를 결정하는 함수 조건부 엣지 함수. 노드가 아니다. 읽기: retrieval_ok 쓰기: 없음 반환 "generate" → generate 노드, "fallback" → fallback 노드build_graph: LangGraph를 만드는 함수get_source: 문서 하나에서 출처 정보를 가져오는 함수 노드가 아님. 문서 메타데이터에서 파일명과 페이지를 고른다. 읽기: document.metadata 쓰기: 없음 (sources 리스트에 넣을 딕셔너리를 반환)ask: 외부에서 사용하는 대표 함수
파일 맨 아래 실행 결과 샘플 주석은 실행되지 않습니다. 화면 출력은 다음 절에 다시 적어 두었습니다.