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

18차시. State 이해

8. 실습 ③ — RAGState 완성

출처: 원본 PDF 18차시_State이해.pdf 17~18쪽 · 쪽 그림 17쪽 18쪽

📝 src/graph_state.py

Python
from typing import TypedDict, Annotated, List, Any
import operator

class RAGState(TypedDict, total=False):
    # ── 입력 ──
    question: str            # 사용자 원본 질문 (변경 금지)
    query: str               # 검색용 질문 (재작성 가능)

    # ── 검색 결과 ──
    documents: List[Any]     # 검색된 조각
    scores: List[float]      # 유사도 점수
    retrieval_ok: bool       # 검색 성공 여부

    # ── 생성 결과 ──
    answer: str              # 생성된 답변
    insufficient: bool       # 근거 부족 자가신고
    has_citation: bool       # 출처 번호 포함 여부

    # ── 검증 결과 ──
    grade: str               # "pass" | "retry"
    reason: str              # 판정 이유

    # ── 제어 ──
    retries: int             # 재시도 횟수
    top_k: int               # 검색 개수 (동적 조절용)

    # ── 이력 (누적) ──
    log: Annotated[List[str], operator.add]
    tried_queries: Annotated[List[str], operator.add]

💡 total=False 는 무엇인가요?
기본적으로 TypedDict는 모든 키가 필수입니다.
그러면 invoke할 때 9개 키를 다 넣어야 합니다.
total=False를 붙이면 전부 선택 사항이 됩니다.
app.invoke({"question": "환불 규정은?"})
이렇게 하나만 넣어도 됩니다.
나머지는 노드들이 채워 나갑니다.

실습 코드 · 18/graph_state.py

실습 코드: 실습/src/18/graph_state.py · 위의 원본 PDF 18차시 17쪽 「8️⃣ 실습 ③ — RAGState 완성」과 이어집니다.

Python
# ============================================================
# 18차시 | State 이해
# 교안: 18차시_State이해.pptx
# 역할: 이후 RAG 그래프가 공유할 State 칸(키)만 정의한다. 실행하지 않는다.
# 입력: 없음 (클래스 정의)
# 출력: 없음
#
#   State  = 아래 RAGState 딕셔너리. 노드 함수는 이 파일에 없다.
#   reducer = log, tried_queries 만 Annotated[..., operator.add].
#            노드가 ["한 줄"] 을 돌려주면 기존 리스트 뒤에 붙는다.
#            그 외 키는 return 한 값으로 그 칸만 덮어쓴다.
#   total=False = 초기 State에 모든 키가 없어도 타입 검사에서 빠진다.
#
# 이 파일은 그래프를 compile 하지 않는다. 칸의 흐름만 적는다.
#
#   question / query
#        |
#        v
#   documents / scores / retrieval_ok
#        |
#        v
#   answer / insufficient / has_citation
#        |
#        v
#   grade / reason
#   retries, top_k 는 반복 제어용 칸
#   log, tried_queries 는 reducer 로 누적
# ============================================================

from typing import TypedDict, Annotated, Any
import operator

# ============================================================
# RAG State 정의
# ============================================================

class RAGState(TypedDict, total=False):
    """RAG 그래프의 모든 노드가 함께 사용하는 데이터 구조입니다."""

    # --------------------------------------------------------
    # 1. 사용자 입력
    # --------------------------------------------------------

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

    # 실제 검색에 사용할 질문입니다.
    # 검색 실패 시 다른 문장으로 변경할 수 있습니다.
    query: str

    # --------------------------------------------------------
    # 2. 검색 결과
    # --------------------------------------------------------

    # 검색된 문서 조각들을 저장합니다.
    documents: list[Any]

    # 검색된 문서들의 유사도 점수를 저장합니다.
    scores: list[float]

    # 검색 결과를 사용할 수 있는지 저장합니다.
    retrieval_ok: bool

    # --------------------------------------------------------
    # 3. 답변 생성 결과
    # --------------------------------------------------------

    # LLM이 생성한 최종 답변입니다.
    answer: str

    # 근거가 부족한지 저장합니다.
    insufficient: bool

    # 답변에 출처 번호가 포함되었는지 저장합니다.
    has_citation: bool

    # --------------------------------------------------------
    # 4. 답변 검증 결과
    # --------------------------------------------------------

    # 답변 검증 결과를 저장합니다.
    # 예: "pass", "retry"
    grade: str

    # 검증 결과가 나온 이유를 저장합니다.
    reason: str

    # --------------------------------------------------------
    # 5. 그래프 제어 정보
    # --------------------------------------------------------

    # 답변 생성 등을 다시 시도한 횟수입니다.
    retries: int

    # 검색할 문서 개수입니다.
    top_k: int

    # --------------------------------------------------------
    # 6. 실행 이력
    # --------------------------------------------------------

    # 각 노드의 실행 기록을 계속 누적합니다.
    # reducer: 노드가 돌려준 리스트를 기존 log 뒤에 이어 붙인다.
    log: Annotated[list[str], operator.add]

    # 지금까지 사용한 검색어를 계속 누적합니다.
    # reducer: 같은 방식으로 tried_queries 뒤에 이어 붙인다.
    tried_queries: Annotated[list[str], operator.add]

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

출력
===== 실행 결과 샘플 =====
실행: cd 실습/src/18
      python graph_state.py
(실제 실행 출력)
(표준 출력 없음. State 칸만 정의하고 그래프는 실행하지 않는다.)

코드 해설

이후 RAG 그래프가 공유할 State 칸(키)만 정의한다. 실행하지 않는다. 입력은 없음 (클래스 정의)이고, 출력은 없음입니다.

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

  • 18차시 | State 이해
  • 교안: 18차시_State이해.pptx
  • State = 아래 RAGState 딕셔너리. 노드 함수는 이 파일에 없다.
  • reducer = log, tried_queries 만 Annotated[..., operator.add].
  • 노드가 ["한 줄"] 을 돌려주면 기존 리스트 뒤에 붙는다.
  • 그 외 키는 return 한 값으로 그 칸만 덮어쓴다.
  • total=False = 초기 State에 모든 키가 없어도 타입 검사에서 빠진다.
  • 이 파일은 그래프를 compile 하지 않는다. 칸의 흐름만 적는다.

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

  • RAGState: RAG 그래프의 모든 노드가 함께 사용하는 데이터 구조입니다.

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

8-1) 초기값 채우기

출처: 원본 PDF 18쪽

Python
def make_initial_state(question: str) -> dict:
    # invoke에 넣을 초기 State를 만든다
    return {
        "question": question,
        "query": question,      # 처음엔 원본과 동일
        "retries": 0,
        "log": [],
        "tried_queries": [],
    }

# 사용
app.invoke(make_initial_state("환불은 며칠 이내인가요?"))

⚠️ 가장 흔한 실수
query를 초기화하지 않는 것
app.invoke({"question": "환불 규정은?"})
retrieve 노드가 state["query"]를 읽으려는데
아직 없어서 KeyError가 납니다.
해결책 두 가지
① 초기 State에 query를 함께 넣기 (위 방식)
② 노드에서 state.get("query") or state["question"]

← 이전 절18차시 · 7. RAG용 State 설계하기다음 절 →18차시 · 9. 실습 ④ — State 흐름 추적하기