RAG2026-5일22차시 · RAG를 Graph로 바꾸기7 / 20← 이전목차다음 →MetaContents

22차시. RAG를 Graph로 바꾸기

4. 실습 ② — 그래프 완성

출처: 원본 PDF 22차시_RAG를Graph로바꾸기.pdf 6~7쪽 · 쪽 그림 6쪽 7쪽

📝 src/graph.py

Python
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️⃣ 실습 ② — 그래프 완성」과 이어집니다.

Python
# ============================================================
# 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️⃣ 실습 ② — 그래프 완성」과 이어집니다.

Python
# ============================================================
# 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️⃣ 실습 ② — 그래프 완성」과 이어집니다.

Python
# ============================================================
# 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: 외부에서 사용하는 대표 함수

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

← 이전 절22차시 · 3. 실습 ① — 코드 매핑 확인다음 절 →22차시 · 5. 실습 ③ — 단계별로 붙이기