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

18차시. State 이해

6. 실습 ② — Reducer 비교 실험

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

📝 붙였을 때 vs 뗐을 때

출력
from typing import TypedDict, Annotated, List
import operator
from langgraph.graph import StateGraph, START, END

# ── 실험 A: Reducer 없음 ──
class StateA(TypedDict):
    log: List[str]

# ── 실험 B: Reducer 있음 ──
class StateB(TypedDict):
    log: Annotated[List[str], operator.add]

def step1(state): return {"log": ["1단계 완료"]}
def step2(state): return {"log": ["2단계 완료"]}
def step3(state): return {"log": ["3단계 완료"]}

def run(StateCls, label):
    g = StateGraph(StateCls)
    for name, fn in [("s1", step1), ("s2", step2), ("s3", step3)]:
        g.add_node(name, fn)
    g.add_edge(START, "s1")
    g.add_edge("s1", "s2")
    g.add_edge("s2", "s3")
    g.add_edge("s3", END)
    result = g.compile().invoke({"log": []})
    print(f"{label}: {result['log']}")

run(StateA, "Reducer 없음")
run(StateB, "Reducer 있음")

실행 결과
──────────────────────────────────────────
Reducer 없음: ['3단계 완료']
Reducer 있음: ['1단계 완료', '2단계 완료', '3단계 완료']

💡 직접 실행해보세요
이 차이를 글로 읽는 것과 눈으로 보는 것은 다릅니다.
나중에 "로그가 하나만 남는데?" 하고 당황할 때
오늘 본 이 화면이 떠오를 겁니다.

실습 코드 · 18/reducer_demo.py

실습 코드: 실습/src/18/reducer_demo.py · 위의 원본 PDF 18차시 13쪽 「6️⃣ 실습 ② — Reducer 비교 실험」과 이어집니다.

Python
# ============================================================
# 18차시 | State 이해
# 교안: 18차시_State이해.pptx
# 역할: reducer 가 없을 때와 있을 때 log 리스트가 어떻게 다른지 비교한다.
# 입력: log (빈 리스트)
# 출력: reducer 없음이면 마지막 값만, 있으면 세 단계가 이어 붙은 리스트
#
#   State  = 공유 노트. 같은 노드 함수를 두 그래프에 그대로 넣는다.
#   Node   = step1, step2, step3. 각자 log 에 한 칸짜리 리스트를 돌려준다.
#   Edge   = START → step1 → step2 → step3 → END. 두 그래프의 연결은 같다.
#   reducer = 노드가 돌려준 값을 기존 값에 합치는 규칙.
#            없으면 그 키는 통째로 덮어쓴다.
#            Annotated[list[str], operator.add] 이면 리스트를 뒤에 이어 붙인다.
#            (새 리스트로 교체하지 않는다.)
#   compile / invoke = 각 그래프를 확정한 뒤 빈 log 로 한 번씩 실행한다.
#
# 흐름 (A, B 둘 다 같은 연결)
#   START → step1 → step2 → step3 → END
#
#   A log: [] → ["1단계 완료"] → ["2단계 완료"] → ["3단계 완료"]
#   B log: [] → ["1단계 완료"]
#             → ["1단계 완료", "2단계 완료"]
#             → ["1단계 완료", "2단계 완료", "3단계 완료"]
# ============================================================

from typing import TypedDict, Annotated
import operator

from langgraph.graph import StateGraph, START, END

# ============================================================
# 1. Reducer가 없는 State
# ============================================================

class StateA(TypedDict):
    """log에 새 값이 들어오면 기존 값을 덮어씁니다."""

    log: list[str]

# ============================================================
# 2. Reducer가 있는 State
# ============================================================

class StateB(TypedDict):
    """log에 새 값이 들어오면 기존 리스트 뒤에 추가합니다."""

    log: Annotated[list[str], operator.add]

# ============================================================
# 3. Node 정의
# ============================================================

# 노드 step1
#   읽기: 없음 (state 는 받지만 사용하지 않음)
#   쓰기: log
def step1(state):
    """1단계 실행 기록을 반환합니다."""

    return {"log": ["1단계 완료"]}

# 노드 step2
#   읽기: 없음
#   쓰기: log
def step2(state):
    """2단계 실행 기록을 반환합니다."""

    return {"log": ["2단계 완료"]}

# 노드 step3
#   읽기: 없음
#   쓰기: log
def step3(state):
    """3단계 실행 기록을 반환합니다."""

    return {"log": ["3단계 완료"]}

# ============================================================
# 4. 실험 A - Reducer 없음
# ============================================================

# StateA를 사용하는 그래프를 만듭니다.
graph_a = StateGraph(StateA)

# 노드를 등록합니다.
graph_a.add_node("step1", step1)
graph_a.add_node("step2", step2)
graph_a.add_node("step3", step3)

# START → step1 → step2 → step3 → END 순서로 연결합니다.
graph_a.add_edge(START, "step1")
graph_a.add_edge("step1", "step2")
graph_a.add_edge("step2", "step3")
graph_a.add_edge("step3", END)

# compile 후 invoke. reducer 가 없으면 각 return 이 log 를 덮어쓴다.
app_a = graph_a.compile()

# 빈 log에서 실행을 시작합니다.
result_a = app_a.invoke({"log": []})

# 최종 log를 출력합니다.
print("Reducer 없음:", result_a["log"])

# ============================================================
# 5. 실험 B - Reducer 있음
# ============================================================

# StateB를 사용하는 그래프를 만듭니다.
graph_b = StateGraph(StateB)

# 노드를 등록합니다.
graph_b.add_node("step1", step1)
graph_b.add_node("step2", step2)
graph_b.add_node("step3", step3)

# START → step1 → step2 → step3 → END 순서로 연결합니다.
graph_b.add_edge(START, "step1")
graph_b.add_edge("step1", "step2")
graph_b.add_edge("step2", "step3")
graph_b.add_edge("step3", END)

# 같은 노드라도 reducer 가 있으면 log 리스트를 뒤에 이어 붙인다.
app_b = graph_b.compile()

# 빈 log에서 실행을 시작합니다.
result_b = app_b.invoke({"log": []})

# 최종 log를 출력합니다.
print("Reducer 있음:", result_b["log"])

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

출력
===== 실행 결과 샘플 =====
실행: cd 실습/src/18
      python reducer_demo.py
(실제 실행 출력)
Reducer 없음: ['3단계 완료']
Reducer 있음: ['1단계 완료', '2단계 완료', '3단계 완료']
경로: step1 → step2 → step3 → END

코드 해설

reducer 가 없을 때와 있을 때 log 리스트가 어떻게 다른지 비교한다. 입력은 log (빈 리스트)이고, 출력은 reducer 없음이면 마지막 값만, 있으면 세 단계가 이어 붙은 리스트입니다.

주석에 적힌 흐름입니다.

출력
흐름 (A, B 둘 다 같은 연결)
START → step1 → step2 → step3 → END
A log: [] → ["1단계 완료"] → ["2단계 완료"] → ["3단계 완료"]
B log: [] → ["1단계 완료"]
→ ["1단계 완료", "2단계 완료"]
→ ["1단계 완료", "2단계 완료", "3단계 완료"]

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

  • 18차시 | State 이해
  • 교안: 18차시_State이해.pptx
  • State = 공유 노트. 같은 노드 함수를 두 그래프에 그대로 넣는다.
  • Node = step1, step2, step3. 각자 log 에 한 칸짜리 리스트를 돌려준다.
  • Edge = START → step1 → step2 → step3 → END. 두 그래프의 연결은 같다.
  • reducer = 노드가 돌려준 값을 기존 값에 합치는 규칙.
  • 없으면 그 키는 통째로 덮어쓴다.
  • Annotated[list[str], operator.add] 이면 리스트를 뒤에 이어 붙인다.

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

  • StateA: log에 새 값이 들어오면 기존 값을 덮어씁니다.
  • StateB: log에 새 값이 들어오면 기존 리스트 뒤에 추가합니다.
  • step1: 노드 step1 읽기: 없음 (state 는 받지만 사용하지 않음) 쓰기: log
  • step2: 노드 step2 읽기: 없음 쓰기: log
  • step3: 노드 step3 읽기: 없음 쓰기: log

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

← 이전 절18차시 · 5. Reducer — 덮어쓰지 않고 쌓기다음 절 →18차시 · 7. RAG용 State 설계하기