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.py | 9쪽 | 18차시 「4. 실습 ① — State 정의하고 흐름 보기」 아래에 실었습니다 |
18/reducer_demo.py | 13쪽 | 18차시 「6. 실습 ② — Reducer 비교 실험」 아래에 실었습니다 |
18/graph_state.py | 17쪽 | 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 에 같은 이름의 쪽이 없어 여기에 실었습니다.
# ============================================================
# 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 덕분에 그 뒤에 붙습니다.