18장. 그래프 기반 워크플로우
상태·노드·엣지 — 그래프를 이루는 세 가지
한 줄 요약
상태 그래프는 세 가지로 이루어집니다. 흐름 전체가 함께 보는 데이터인 상태(State), 일하는 단계인 노드(Node), 단계 사이의 길인 엣지(Edge) 입니다. 상태가 엣지를 따라 노드들을 지나가며 채워지고, 끝에 도착했을 때의 상태가 결과입니다.
1. 상태(State) — 단계마다 넘겨받는 서류철
상태는 흐름이 진행되는 동안 모든 단계가 함께 보는 데이터 묶음입니다. 창구에서 창구로 넘어가는 서류철을 떠올리면 됩니다. 처음에는 고객 문의만 적혀 있고, 단계를 지날 때마다 칸이 채워집니다.
어떤 칸이 있는지는 TypedDict로 미리 선언합니다. TypedDict는 "이 딕셔너리에는 이런 키가 있고 값의 종류는 이렇다"를 적어 두는 파이썬 문법입니다.
class CSState(TypedDict):
question: str # 고객 문의 (입력)
intent: str # 분류 결과
urgency: str
answer: str # 최종 답변 (출력)
log: list[str] # 처리 과정 기록
| 칸 | 누가 채우나 |
|---|---|
question |
그래프를 실행할 때 우리가 넣는다 |
intent, urgency |
분류 단계 |
answer |
응답 단계 |
log |
모든 단계가 한 줄씩 보탠다 |
if문 방식에서는 intent, docs, answer 같은 변수가 함수 여기저기에 흩어져 있었습니다. 그래프에서는 흐름이 쓰는 데이터가 한 곳에 모여 선언되어 있습니다.
2. 노드(Node) — 상태를 받아 바뀐 부분만 돌려주는 함수
노드는 일하는 단계 하나입니다. 만드는 법은 평범한 함수입니다. 약속은 하나입니다.
상태를 받아서, 자기가 바꾼 칸만 딕셔너리로 돌려준다.
def classify_node(state: CSState) -> dict:
"""4장 분류기를 그래프 노드로 감쌌다."""
result = classify(state["question"])
return {
"intent": result.intent.value,
"urgency": result.urgency.value,
"log": state["log"] + [f"분류: {result.intent.value}/{result.urgency.value}"],
}
| 줄 | 하는 일 |
|---|---|
state["question"] |
상태에서 필요한 칸만 읽는다 |
classify(...) |
4장에서 만든 분류기를 그대로 부른다. 노드는 이미 있는 부품을 감싼 껍데기다 |
return {...} |
intent, urgency, log 세 칸만 돌려준다. question과 answer는 건드리지 않는다 |
돌려준 딕셔너리를 상태에 합치는 일은 LangGraph가 합니다. 노드는 상태 전체를 들고 다니며 고칠 필요가 없고, 자기 일과 상관없는 칸을 실수로 망가뜨릴 일도 줄어듭니다.
3. 합치는 규칙 — 기본은 덮어쓰기
LangGraph가 노드의 반환값을 상태에 합칠 때의 기본 규칙은 덮어쓰기입니다. 돌려준 칸의 새 값이 옛 값을 대신합니다.
intent나 answer는 덮어쓰면 됩니다. 그런데 log는 단계마다 한 줄씩 쌓여야 합니다. 그래서 노드는 새 줄만 돌려주지 않고, 기존 기록에 새 줄을 이어 붙인 리스트를 돌려줍니다.
"log": state["log"] + ["응답 생성 완료"] # 기존 기록 + 새 줄
새 줄만 돌려주면 어떻게 되는지 작은 그래프로 확인해 봤습니다. 처음 상태가 {"log": ["시작"]}이고, 첫 노드는 이어 붙여 돌려주고 둘째 노드는 {"log": ["둘째 단계"]}만 돌려주게 했습니다.
{'log': ['둘째 단계']}
앞의 기록이 전부 사라졌습니다. 17장에서 프로필을 다시 저장하면 덮어썼던 것과 같은 일입니다.
칸마다 "덮어쓰지 말고 이어 붙여라" 같은 합치는 방식을 따로 지정하는 기능도 있습니다. 이것을 리듀서(reducer) 라고 부릅니다. 이번 장에서는 기본 규칙만 씁니다.
4. 엣지(Edge) — 다음에 어디로 가는가
엣지는 "이 노드가 끝나면 저 노드로 간다"는 선언입니다.
builder.add_edge(START, "classify") # 시작 → 분류
builder.add_edge("classify", "answer")
builder.add_edge("answer", "log")
builder.add_edge("log", END) # 로그 → 종료
START와 END는 LangGraph가 미리 정해 둔 특별한 지점입니다. START는 우리가 넣은 입력이 그래프로 들어오는 자리이고, END는 그래프가 끝나는 자리입니다.
이 네 줄이 곧 흐름의 명세입니다. if문 방식에서는 코드를 처음부터 읽어야 알 수 있던 "분류 다음에 무엇이 오는가"가 한 줄에 적혀 있습니다.
노드 함수 안에는 "다음에 어디로 가라"는 말이 없다는 점도 봅니다. classify_node는 자기 다음이 answer인지 모릅니다. 일하는 것(노드)과 순서(엣지)가 따로 적혀 있습니다.
5. 누가 무엇을 정하는가
| 일 | 누가 |
|---|---|
| 상태에 어떤 칸을 둘지 | 개발자 |
| 어떤 노드가 있고 어떤 순서로 이어지는지 | 개발자 (엣지 선언) |
| 노드를 순서대로 부르고, 반환값을 상태에 합치는 일 | LangGraph |
| 문의가 어떤 유형인지, 답을 어떻게 쓸지 | 모델 (노드 안에서) |
이번 장의 그래프에서 순서는 전부 개발자가 정했습니다. 모델은 정해진 단계 안에서만 일합니다. 9장의 에이전트 루프에서는 "다음에 무엇을 할지"를 모델이 정했습니다. 그래프는 그 반대쪽 끝에서 시작해, 19장부터 모델의 판단에 따라 길이 갈리는 부분을 조금씩 넣어 갑니다.
핵심 정리
- 상태는 흐름이 함께 보는 데이터이고
TypedDict로 칸을 선언합니다. - 노드는 상태를 받아 바뀐 칸만 딕셔너리로 돌려주는 함수입니다.
- 합치는 기본 규칙은 덮어쓰기입니다. 쌓아야 하는 칸은 기존 값에 이어 붙여 돌려줍니다.
- 엣지는 노드 사이의 길이고,
START에서 시작해END에서 끝납니다. - 일(노드)과 순서(엣지)를 따로 적는 것이 그래프의 핵심입니다.