LangGraph의 State·Node·Edge, 흐름을 그래프로 그리는 이유

문의

설명을 위한 예시 문의입니다. 칸이 채워지는 순서는 아래 코드를 langgraph 1.2로 직접 실행해 확인한 결과와 같습니다.

은행 창구를 생각해 봅니다. 서류 한 장을 들고 접수 창구에서 이름을 적고 확인 창구에서 담당자를 정하고 마지막 창구에서 안내문을 받습니다. 창구마다 하는 일은 달라도 서류는 한 장이라서 앞 창구가 적은 칸을 뒤 창구가 읽습니다.

LangGraph는 이 구조를 그대로 코드로 옮깁니다. 서류는 State(상태), 창구는 Node(노드), 창구 사이의 길은 Edge(엣지)입니다. 이렇게 그린 흐름을 그래프(graph)라고 부릅니다. 순서가 눈에 보이면 고치기도 쉽고 중간에 길을 갈라 놓기도 쉽습니다. 모델을 부르는 노드는 없어도 됩니다. 위의 세 노드도 평범한 Python 함수입니다.

노드는 바꿀 칸만 돌려줍니다

State는 칸 이름과 종류를 적은 클래스로 만듭니다. 여기서는 TypedDict(칸 이름과 종류를 정해 둔 딕셔너리)를 씁니다. 노드는 State를 받아서 바꿀 칸만 딕셔너리로 돌려주는 함수입니다. LangGraph 문서는 노드가 State 전체가 아니라 업데이트만 돌려주면 된다고 설명합니다.

돌려준 칸은 State에 반영되고 나머지 칸은 그대로 남습니다. 그림에서 classify가 category만 채우고 text는 건드리지 않는 것이 이 규칙입니다. 반환값이 없는 노드(None)도 오류를 내지 않습니다. State가 바뀌지 않을 뿐입니다.

nodes.pypython
# 1 State: 모든 노드가 같이 보는 서류의 칸
class Ticket(TypedDict):
    text: str
    category: str
    queue: str
    reply: str

# 2 Node: state를 받아 바꿀 칸만 돌려주는 함수
def classify(state: Ticket) -> dict:
    net = "VPN" in state["text"] or "와이파이" in state["text"]
    return {"category": "네트워크" if net else "기타"}

def lookup(state: Ticket) -> dict:
    queues = {"네트워크": "네트워크 팀 큐", "기타": "일반 큐"}
    return {"queue": queues[state["category"]]}
  1. 서류의 칸입니다. 칸 이름과 종류를 적은 클래스가 State의 모양이 됩니다. 처음 실행할 때는 text만 채워서 넣고 나머지 칸은 노드가 채웁니다.
  2. 노드는 함수입니다. State를 받아 읽고 바꿀 칸만 딕셔너리로 돌려줍니다. lookup은 앞 노드가 채운 category를 읽어 queue 칸만 돌려줍니다. reply 노드도 같은 방식이라 생략했습니다.
graph.pypython
# 3 노드와 화살표를 등록한다
builder = StateGraph(Ticket)
builder.add_node("classify", classify)
builder.add_node("lookup", lookup)
builder.add_node("reply", reply)
builder.add_edge(START, "classify")
builder.add_edge("classify", "lookup")
builder.add_edge("lookup", "reply")
builder.add_edge("reply", END)
# 4 컴파일한 뒤에야 실행할 수 있다
graph = builder.compile()
# 5 실행하면 마지막 State가 돌아온다
print(graph.invoke({"text": "VPN이 안 붙어요"}))
# 6 단계마다 바뀐 칸만 본다
for step in graph.stream({"text": "VPN이 안 붙어요"}, stream_mode="updates"):
    print(step)
터미널
$ python graph.py {'text': 'VPN이 안 붙어요', 'category': '네트워크', 'queue': '네트워크 팀 큐', 'reply': "'네트워크' 문의로 접수해 네트워크 팀 큐에 전달했습니다."} {'classify': {'category': '네트워크'}} {'lookup': {'queue': '네트워크 팀 큐'}} {'reply': {'reply': "'네트워크' 문의로 접수해 네트워크 팀 큐에 전달했습니다."}}

출력은 예시 답입니다. 코드는 langgraph 1.2 기준이고 reply 노드와 import는 생략했습니다.

  1. 등록합니다. add_node에는 노드 이름과 함수를, add_edge에는 출발 노드와 도착 노드를 줍니다. START는 입력이 들어오는 곳이고 END는 끝입니다. 화살표를 하나도 안 이으면 그래프가 어디서 시작하는지 몰라서 컴파일에서 오류가 납니다.
  2. 컴파일합니다. compile()이 구조를 점검하고 실행할 수 있는 그래프를 만듭니다. 문서도 컴파일 전에는 쓸 수 없다고 안내합니다.
  3. 실행합니다. invoke는 모든 노드를 지난 뒤의 마지막 State를 돌려주고 stream은 노드가 끝날 때마다 그 노드가 바꾼 칸을 줍니다. 위 그림에서 칸이 채워지던 순서가 바로 이 출력입니다.
이 그래프가 그려 내는 Mermaid 코드 보기
graph.mmd
graph TD;
    __start__ --> classify;
    classify --> lookup;
    lookup --> reply;
    reply --> __end__;

graph.get_graph().draw_mermaid()가 내놓는 글에서 화살표만 줄여 옮겼습니다. 이 글을 Mermaid 도구에 넣으면 같은 그림이 그려집니다.

같은 칸에 또 쓰면 덮어쓰거나 이어 붙입니다

노드 둘이 같은 칸을 바꾸면 어떻게 될까요? LangGraph는 칸마다 업데이트를 합치는 규칙인 reducer(리듀서)를 둡니다. 따로 정하지 않으면 새 값이 이전 값을 덮어씁니다. 문서도 기본 reducer는 이전 값을 무시하고 새 값으로 바꾼다고 설명합니다.

목록 칸에 이어 붙이게 하려면 칸의 종류를 Annotated[list[str], operator.add]로 적습니다. 스크롤하면서 같은 두 노드가 쪽지를 두 방식의 칸에 붙이는 모습을 나란히 보세요. 직접 실행해서 확인한 값입니다.

처음 State

notes 칸에는 "시작" 하나가 들어 있습니다. 노드 A와 B가 차례로 이 칸에 새 쪽지를 돌려줄 참입니다.

노드 A가 ["A 노드"]를 돌려줌

reducer에 따라 이 쪽지가 칸에 반영됩니다. 기본 방식은 칸 전체를 새 값으로 바꾸고 operator.add는 뒤에 이어 붙입니다.

노드 B가 ["B 노드"]를 돌려줌

덮어쓰기에서는 앞의 값이 모두 사라지고 ["B 노드"]만 남습니다. 이어 붙이기에서는 ["시작", "A 노드", "B 노드"]가 됩니다.

reducer.pypython
# 7 이어 붙이는 칸은 종류에 reducer를 적는다
class S(TypedDict):
    notes: Annotated[list[str], operator.add]
    last: str
터미널
$ python reducer.py {'notes': ['시작', 'A 노드', 'B 노드'], 'last': 'B'} {'notes': ['B 노드'], 'last': 'B'}

출력은 예시입니다. 첫 줄은 reducer를 단 경우이고 둘째 줄은 reducer 없이 list[str]만 적은 경우입니다.

  1. 칸마다 합치는 규칙을 정합니다. Annotated[종류, 함수]에서 함수가 reducer입니다. operator.add는 목록 둘을 이어 붙입니다. 대화 메시지를 쌓는 칸이 이 방식이고 메시지용 reducer add_messages가 따로 있습니다. last처럼 reducer가 없는 칸은 마지막 값이 남습니다.
순서가 정해진 흐름 · 코드가 길을 정함 classifylookupreply 이 글의 그래프입니다.입력이 무엇이든 같은 순서로 지나갑니다. 돌아가는 흐름 · 모델이 길을 정함 도구가 필요하면결과를 들고 돌아옴 model tools 답이 나오면 끝 앞서 본 create_agent의 안쪽입니다.모델의 응답에 따라 tools로 갔다가 돌아오거나 끝납니다.

두 그래프 모두 노드와 엣지로 그린다는 점은 같고 길을 누가 고르는지가 다릅니다.

그래프는 순서를 눈에 보이게 만듭니다

순서가 고정된 일이라면 함수 안에서 차례로 호출해도 됩니다. 그래도 그래프로 그리면 얻는 게 있습니다. 어느 단계에서 실패했는지 노드 이름으로 알 수 있고 중간 결과가 State에 남고 길을 갈라 놓거나 단계를 하나 끼워 넣을 때 다른 노드를 건드리지 않습니다.

LangGraph는 다른 도구보다 낮은 층에 놓인 도구입니다. 문서는 오래 도는 상태 있는 흐름을 위한 기반이라고 설명하고 LangChain의 Agent도 그 위에 만들어졌다고 밝힙니다. 앞서 본 create_agent의 model과 tools 노드가 이 구조입니다. 다음 글에서는 이 그래프를 직접 조립해 Agent를 다시 만들어 봅니다.

처음 만들 때 자주 나는 실수

그래프를 처음 짜면 비슷한 실수를 합니다. 아래는 직접 실행해서 얻은 오류 메시지입니다. 모든 실수가 오류로 알려 주지는 않고 조용히 넘어가는 경우도 있어서 하나는 따로 표시했습니다.

모델 없이도 그래프가 돌아가고 State가 바뀌는 과정이 눈에 보이면 다음 단계가 쉽습니다. 다음 글에서는 이 State와 노드로 model과 tools를 직접 짠 Agent 그래프를 살펴봅니다.

참고 자료

출처 4개 펼치기