11. LangGraph 입문: 상태 그래프의 3대 요소
- LangGraph가 왜 "그래프"인지, 기존 SDK와 무엇이 다른지 이해한다
- 세 가지 핵심 원시 State·Node·Edge를 익힌다
- 가장 단순한 그래프를 정의하고 컴파일·실행한다
- 리듀서(reducer)로 상태 업데이트를 제어하는 법을 안다
비유로 시작하기: 공정도(flowchart) 엔진
지금까지의 OpenAI Agents SDK가 "잘 정리된 공구함"이었다면, LangGraph는 공정도 엔진에 가깝습니다. 작업의 흐름을 그래프(노드와 엣지)로 명시적으로 그려두고, 그 그래프대로 실행합니다.
왜 이런 접근이 필요할까요? 현실의 에이전트는 직선으로 흐르지 않습니다. 고객 메시지를 읽고 → 지식베이스를 검색할지 도구를 부를지 판단하고 → 실패하면 재시도하고 → 사람의 승인을 기다리고 → 전체 대화를 기억해야 합니다. 이런 분기·반복·중단·복구를 깔끔하게 다루려면, 흐름 자체를 그래프로 표현하는 것이 유리합니다.
config_schema 등)는 context_schema로 대체되었으니, 오래된 튜토리얼을 볼 때 주의하세요.설치
pip install langgraph langchain langchain-openai
세 가지 핵심 원시
LangGraph의 모든 것은 단 세 가지로 환원됩니다.
flowchart LR
classDef state fill:#A5D6A7,stroke:#2E7D32,color:#000
classDef node fill:#80DEEA,stroke:#00838F,color:#000
classDef edge fill:#90CAF9,stroke:#1565C0,color:#000
S[State<br/>공유 데이터]:::state
N[Node<br/>일을 하는 함수]:::node
E[Edge<br/>노드 간 연결]:::edge
S -.모든 노드가 읽고 씀.-> N
N -->|Edge로 다음 노드 결정| N
세 요소의 정체는 이렇습니다. State는 그래프 전체가 공유하는 데이터(보통 TypedDict)입니다. TypedDict는 딕셔너리({"키": 값})의 각 키에 어떤 자료형이 들어갈지 미리 선언해 두는 파이썬 타입으로, 상태의 구조를 분명히 해 줍니다. Node는 현재 상태를 받아 일을 하고 바뀐 부분을 돌려주는 함수입니다. Edge는 노드에서 노드로의 전환을 정의합니다. 이 셋을 어떻게 엮느냐에 따라 단순 파이프라인부터 멀티에이전트 시스템까지 모두 표현됩니다.
📌 핵심: 1부에서 배운 "메모리=대화 기록"이 여기서는 State로 격상됩니다. LangGraph에서 메모리는 그래프의 상태이고, 모든 노드가 그 상태를 읽고 씁니다.
가장 단순한 그래프
State를 정의하고, Node 하나를 만들고, START → node → END로 연결합니다.
from typing import TypedDict from langgraph.graph import StateGraph, START, END # 1) State 정의 — 그래프가 읽고 쓸 데이터의 스키마 class State(TypedDict): question: str answer: str # 2) Node 정의 — 상태를 받아 바뀐 부분만 반환 def answer_node(state: State) -> dict: q = state["question"] return {"answer": f"'{q}'에 대한 답변입니다."} # 3) 그래프 조립 builder = StateGraph(State) builder.add_node("answer", answer_node) builder.add_edge(START, "answer") # 시작 → answer 노드 builder.add_edge("answer", END) # answer 노드 → 끝 # 4) 컴파일 (반드시 필요) graph = builder.compile() # 5) 실행 result = graph.invoke({"question": "LangGraph가 뭐야?"}) print(result["answer"])
흐름은 다섯 단계입니다. State 스키마를 정하고, 노드 함수를 만들고, add_node/add_edge로 조립하고, compile()로 실행 가능한 그래프를 만들고, invoke()로 돌립니다.
⚠️ 흔한 실수: compile()을 빼먹는 것. StateGraph는 설계도일 뿐이고, 컴파일해야 실행 가능한 그래프가 됩니다. 컴파일 단계에서 고아 노드 같은 구조적 오류도 검사합니다.
💡 팁: START와 END는 진짜 노드가 아니라 표식입니다. START에서 나가는 엣지는 "그래프가 여기서 시작한다", END로 가는 엣지는 "여기서 멈춘다"는 의미입니다.
노드는 "바뀐 부분만" 반환한다
중요한 규칙: 노드는 상태 전체를 반환하지 않고, 자기가 바꾼 키만 반환합니다. 위 예에서 answer_node는 answer만 돌려줬습니다. question은 건드리지 않았으니 그대로 유지됩니다. LangGraph가 반환된 부분을 기존 상태에 병합(merge)합니다.
리듀서: 상태를 어떻게 병합할까
기본 병합 방식은 덮어쓰기(replacement)입니다. 같은 키에 새 값을 반환하면 이전 값을 대체합니다. 그런데 대화 기록처럼 누적해야 하는 값은 어떻게 할까요? 이때 리듀서(reducer)를 씁니다.
from typing import Annotated, TypedDict import operator class State(TypedDict): # messages는 덮어쓰지 않고 계속 더해진다(누적) messages: Annotated[list, operator.add] counter: int # 리듀서 없음 → 덮어쓰기
Annotated[list, operator.add]는 "이 키는 새 값을 기존 리스트에 더하라"는 뜻입니다. 메시지처럼 쌓여야 하는 데이터에 필수입니다. LangGraph는 메시지 누적을 위한 전용 헬퍼 add_messages도 제공합니다.
| 상황 | 리듀서 | 동작 |
|---|---|---|
| 단일 값(현재 상태·카운터) | 없음(기본) | 덮어쓰기 |
| 누적 목록(대화 기록) | operator.add / add_messages | 기존에 추가 |
📌 핵심: 리듀서는 "여러 노드가 같은 키에 쓸 때 어떻게 합칠지"를 정합니다. 1부에서 손으로 history.append(...) 했던 것을, 리듀서가 선언적으로 자동화한 셈입니다.
그래프 시각화
조립한 그래프는 그림으로 확인할 수 있습니다. 디버깅에 매우 유용합니다.
# Jupyter 등에서 from IPython.display import Image, display display(Image(graph.get_graph().draw_mermaid_png()))
이 장에서 배운 것
- LangGraph는 작업 흐름을 그래프로 명시해 분기·반복·중단·복구를 깔끔하게 다루는 "공정도 엔진"이다.
- 세 핵심 원시는 State(공유 데이터)·Node(일하는 함수)·Edge(전환)다.
- 그래프는 정의 → 조립 →
compile()→invoke()순으로 쓴다. 컴파일을 빼먹으면 안 된다. - 노드는 바뀐 키만 반환하고, 리듀서가 병합 방식(덮어쓰기 vs 누적)을 정한다.
✍️ 확인 문제
StateGraph를 만들고 노드·엣지를 추가했는데invoke가 안 된다. 무엇을 빠뜨렸을 가능성이 높은가?- 노드 함수가 상태 전체가 아니라 "바뀐 키만" 반환하도록 설계된 이유는 무엇인가?
- 대화 기록을 담는
messages키가 매 노드마다 덮어쓰여 사라진다. 어떻게 고치는가?
이전 부: 10. 핸드오프
다음 장: 12. 조건부 엣지와 도구 호출