RAG2026-5일18차시 · State 이해15 / 19← 이전목차다음 →MetaContents

18차시. State 이해

실습 1 — State 이해 실습 따라 하기

관련 수업 자료: 18차시 「4. 실습 ① — State 정의하고 흐름 보기」(9쪽), 18차시 「6. 실습 ② — Reducer 비교 실험」(13쪽), 18차시 「8. 실습 ③ — RAGState 완성」(17~18쪽) · 실습 코드 18/state_demo.py, 18/reducer_demo.py, 18/graph_state.py, 18/graph_state2.py

이 실습은 교안 해설(18_State이해.md)의 실습 순서를 옮긴 것입니다. OpenAI 임베딩이나 채팅 모델을 부르는 파일은 .env 에 API 키가 있어야 돌아갑니다. 키가 없으면 실행 결과 샘플로 출력 모양을 확인합니다.

수업 자료와 이어지는 곳

실습 코드원본 PDF이어지는 내용
18/state_demo.py9쪽18차시 「4. 실습 ① — State 정의하고 흐름 보기」 아래에 실었습니다
18/reducer_demo.py13쪽18차시 「6. 실습 ② — Reducer 비교 실험」 아래에 실었습니다
18/graph_state.py17쪽18차시 「8. 실습 ③ — RAGState 완성」 아래에 실었습니다
18/graph_state2.py-원본 PDF 에 짝이 되는 쪽이 없어 이 페이지 아래에 실었습니다

실행 흐름

출력
log: 빈 리스트 → step1
step1 → reducer 없음: 마지막 값만
step1 → reducer 있음: 이어 붙임

실습 순서

python state_demo.py 다음 python reducer_demo.py를 실행하고, 두 log 리스트가 왜 다른지 주석의 A/B 흐름과 맞춰 봅니다.

스스로 확인

  • 바꾼 키만 return하는 규칙을 설명할 수 있는가?
  • log에 reducer가 필요한 이유를 아는가?
  • question과 query를 나눈 이유를 한 문장으로 말하는가?

이어지는 실습 코드

실습 코드 · 18/graph_state2.py

실습 코드: 실습/src/18/graph_state2.py · 원본 PDF 에 같은 이름의 쪽이 없어 여기에 실었습니다.

Python
# ============================================================
# 18차시 | State 이해
# 교안: 18차시_State이해.pptx
# 역할: RAG State 전체 칸과, 질문 하나로 빈 노트를 만드는 함수.
# 입력: make_initial_state(question)
# 출력: 모든 키가 기본값인 딕셔너리. 이 파일을 실행해도 화면 출력은 없다.
#
#   State  = RAGState. 이후 노드가 같은 노트를 나눠 쓴다.
#   Node   = 이 파일에는 없다. make_initial_state 는 노드가 아니라
#            invoke 에 넣기 전 첫 노트를 만드는 함수다.
#   reducer = tried_queries, log.
#            초기값 tried_queries=[question] 은 이미 한 칸이 들어 있는 리스트다.
#            이후 노드가 ["다른 검색어"] 를 돌려주면 뒤에 붙는다.
#            log 는 [] 에서 시작한다.
#
# 그래프는 없다. 첫 노트의 칸만 채운다.
#
#   question ──? query, tried_queries[0]
#   나머지 칸은 빈 문자열, 빈 리스트, 0, False
# ============================================================

from typing import TypedDict, Annotated, List
import operator

# ============================================================
# 1. RAG State 정의
# ============================================================

class RAGState(TypedDict):
    """LangGraph의 모든 노드가 공유하는 State입니다."""

    # --------------------------------------------------------
    # 기본 질문
    # --------------------------------------------------------

    # 사용자가 처음 입력한 질문
    question: str

    # 실제 검색에 사용하는 질문
    query: str

    # --------------------------------------------------------
    # 29차시 - 질문 분류
    # --------------------------------------------------------

    # 질문 종류
    intent: str

    # --------------------------------------------------------
    # Retriever
    # --------------------------------------------------------

    # 검색된 문서
    documents: list

    # 검색된 문서의 유사도 점수
    scores: list

    # 검색 성공 여부
    retrieval_ok: bool

    # 검색 실패 이유
    fail_reason: str

    # --------------------------------------------------------
    # Generator
    # --------------------------------------------------------

    # 생성된 답변
    answer: str

    # 근거 부족 여부
    insufficient: bool

    # 답변에 출처가 포함되었는지 여부
    has_citation: bool

    # 답변 생성 중 발생한 오류
    gen_error: str

    # --------------------------------------------------------
    # Verifier
    # --------------------------------------------------------

    # 검증 결과
    grade: str

    # 검증 결과의 이유
    reason: str

    # --------------------------------------------------------
    # 재생성
    # --------------------------------------------------------

    # 답변 재생성 횟수
    retries: int

    # --------------------------------------------------------
    # 30차시 - Rewrite
    # --------------------------------------------------------

    # 검색어 재작성 횟수
    rewrites: int

    # 지금까지 사용한 검색어를 계속 누적
    # reducer: return {"tried_queries": ["새 검색어"]} 이면 뒤에 붙는다.
    tried_queries: Annotated[List[str], operator.add]

    # --------------------------------------------------------
    # 전체 실행 로그
    # --------------------------------------------------------

    # 각 노드의 실행 기록을 계속 누적
    # reducer: return {"log": ["한 줄"]} 이면 기존 log 뒤에 붙는다.
    log: Annotated[List[str], operator.add]

# ============================================================
# 2. 초기 State 생성
# ============================================================

# 노드가 아님. invoke 에 넘길 첫 공유 노트를 만든다.
#   읽기: 인자 question
#   쓰기: 아래 딕셔너리의 모든 키 (State를 갱신하는 return 이 아니다)
def make_initial_state(question: str) -> dict:
    """사용자 질문을 받아 처음 사용할 State를 만듭니다."""

    return {
        # 기본 질문
        "question": question,
        "query": question,

        # 질문 분류
        "intent": "",

        # Retriever
        "documents": [],
        "scores": [],
        "retrieval_ok": False,
        "fail_reason": "",

        # Generator
        "answer": "",
        "insufficient": False,
        "has_citation": False,
        "gen_error": "",

        # Verifier
        "grade": "",
        "reason": "",

        # 재생성 횟수
        "retries": 0,

        # 검색어 재작성 횟수
        "rewrites": 0,

        # 최초 검색어 기록
        "tried_queries": [question],

        # 실행 로그
        "log": [],
    }

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

출력
===== 실행 결과 샘플 =====
실행: cd 실습/src/18
      python graph_state2.py
(실제 실행 출력)
(표준 출력 없음. 클래스와 make_initial_state 만 정의한다.)

코드 해설

RAG State 전체 칸과, 질문 하나로 빈 노트를 만드는 함수.입니다. 입력은 make_initial_state(question)이고, 출력은 모든 키가 기본값인 딕셔너리. 이 파일을 실행해도 화면 출력은 없다.

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

  • 18차시 | State 이해
  • 교안: 18차시_State이해.pptx
  • State = RAGState. 이후 노드가 같은 노트를 나눠 쓴다.
  • Node = 이 파일에는 없다. make_initial_state 는 노드가 아니라
  • invoke 에 넣기 전 첫 노트를 만드는 함수다.
  • reducer = tried_queries, log.
  • 초기값 tried_queries=[question] 은 이미 한 칸이 들어 있는 리스트다.
  • 이후 노드가 ["다른 검색어"] 를 돌려주면 뒤에 붙는다.

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

  • RAGState: LangGraph의 모든 노드가 공유하는 State입니다.
  • make_initial_state: 노드가 아님. invoke 에 넘길 첫 공유 노트를 만든다. 읽기: 인자 question 쓰기: 아래 딕셔너리의 모든 키 (State를 갱신하는 return 이 아니다)

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

코드 설명

  • state_demo.py 의 [retrieve] 받은 State 출력에 documents 와 answer 가 빈 값으로 보이는 것은 initial_state 에 미리 넣었기 때문입니다. 원본 PDF 처럼 question 하나만 넣으면 그 키만 보입니다.
  • return {"documents": documents} 만 돌려줬는데 generate 가 받은 State에 question 이 그대로 있습니다. 반환하지 않은 키는 유지된다는 규칙이 출력으로 보이는 대목입니다.
  • reducer_demo.py 는 같은 step1~step3 함수를 두 그래프에 넣었습니다. 바뀐 것은 State 클래스의 log 선언 한 줄뿐인데 결과가 ['3단계 완료'] 와 세 줄로 갈립니다.
  • Annotated[list[str], operator.add] 에서 앞은 타입, 뒤는 합치는 방법입니다. 리스트끼리 + 하면 이어 붙으므로 노드는 늘 한 칸짜리 리스트 ["…"] 를 돌려줘야 합니다. 문자열을 그냥 돌려주면 리스트에 더할 수 없어 오류가 납니다.
  • class RAGState(TypedDict, total=False) 의 total=False 는 모든 키를 선택 사항으로 만듭니다. graph_state2.py 는 대신 make_initial_state 로 모든 키에 기본값을 채워 넣는 방식을 씁니다.
  • graph_state2.py 의 초기값 "tried_queries": [question] 은 이미 원문 질문이 한 칸 들어 있는 리스트입니다. 이후 재작성 노드가 돌려주는 검색어는 reducer 덕분에 그 뒤에 붙습니다.
← 이전 절18차시 · 체크리스트와 과제다음 절 →18차시 · 퀴즈 1 — 덮어쓰기와 이어 붙이기 견주기