-
LangGraph StateGraph 완전 분해 — 노드·엣지·조건부 라우팅IT 2026. 6. 25. 23:00
LangGraph의 상태(State)가 무엇이고 어떻게 설계하는지 알았다면, 이제 그 상태를 가지고 에이전트 워크플로우를 실제로 조립하는 법을 볼 차례다. LangGraph에서 그 조립 도구가
StateGraph다.StateGraph는 설계도다. 노드를 등록하고, 엣지로 연결하고, 컴파일하면 실행 가능한 에이전트가 된다. 이 글은StateGraph를 구성하는 API 하나하나 —add_node,add_edge,add_conditional_edges,compile— 가 각각 무슨 일을 하고, 어떤 선택을 내리는지를 분해한다.1. StateGraph — 에이전트 설계도를 만드는 빌더
LangGraph에서 에이전트 워크플로우를 만드는 방식은 빌더 패턴(builder pattern)이다.
StateGraph인스턴스를 만들고, 여기에 노드와 엣지를 하나씩 추가한 뒤, 마지막에compile()을 호출해 실행 가능한 객체를 얻는다. 마치 건물 설계도를 그리고 나서 시공하는 것과 같다.builder = StateGraph(AgentState) builder.add_node("agent", agent_node) builder.add_node("tools", ToolNode(tools)) builder.add_edge(START, "agent") builder.add_edge("tools", "agent") # 루프: 도구 실행 후 agent 복귀 builder.add_conditional_edges("agent", tools_condition, {"tools": "tools", END: END}) graph = builder.compile()코드의 구조를 먼저 잡으면 이렇다. 빌더를 만들고, 노드를 등록하고, 엣지를 연결하고, 컴파일한다. 각 API가 무엇을 하는지는 이어지는 절에서 하나씩 분해한다.
2. add_node — 처리 단위를 그래프에 등록하는 방법
add_node(이름, 함수)는 처리 단위를 그래프에 등록한다. 이름은 문자열 식별자고, 함수는 해당 노드의 실제 로직이다.노드 함수의 계약은 단순하다. 현재 상태(AgentState)를 인자로 받고, 상태를 업데이트할 딕셔너리를 반환한다. 반환된 딕셔너리의 키만 상태에서 업데이트된다. 전달하지 않은 키는 이전 값을 유지한다.
# 노드 함수의 계약: state를 받아 업데이트할 키만 반환 def agent_node(state: AgentState) -> dict: # state["messages"]로 이력 전체 읽기 response = llm_with_tools.invoke(state["messages"]) # 업데이트할 키만 반환 — 다른 키는 변경되지 않음 return {"messages": [response]}builder.add_node("tools", ToolNode(tools))에서ToolNode는 LangGraph의 사전 구현 노드다. 상태에서 마지막AIMessage의tool_calls를 자동으로 추출하고, 해당 도구를 실행하고, 결과를ToolMessage로 변환해서 반환한다. 이 과정을 직접 구현할 필요가 없다.add_node가 하는 일을 보여주는 다이어그램이다. 노드 이름과 함수 둘 다 그래프 내부 레지스트리에 저장된다. 이름은 엣지 연결 시 참조 키로 사용되고, 함수는 그 노드가 실행될 때 호출된다. 주의할 점은 이 시점에 함수가 실행되지 않는다는 것이다.add_node는 등록이지 실행이 아니다. 실행은compile()이후invoke나stream을 호출할 때 일어난다. 함정은 이름 충돌이다. 같은 이름으로add_node를 두 번 호출하면 덮어써진다. 또한add_edge에서 참조하는 이름과add_node에서 등록한 이름이 정확히 일치해야 한다 — 오타가 있으면 컴파일 시점에 오류가 난다.3. add_edge vs add_conditional_edges — 두 종류의 전환
노드를 등록했다면 그 사이를 연결해야 한다. 연결에는 두 종류가 있다. 무조건 전환과 조건부 전환이다. 아래 두 다이어그램이 차이를 보여준다.
add_edge(A, B)는 A가 완료되면 항상 B로 이동하는 연결이다. 조건이 없다. 예를 들어add_edge("tools", "agent")는 도구 실행이 끝나면 반드시 agent 노드로 돌아오는 루프를 만든다. 도구 실행 후에는 항상 LLM이 다시 판단해야 하므로, 이 전환은 무조건적이다.add_conditional_edges(A, 조건함수, 매핑)는 조건 함수의 반환값에 따라 다음 노드가 결정된다. A가 완료되면 조건 함수가 현재 상태를 받아 문자열을 반환한다. 그 문자열을 매핑 딕셔너리에서 찾아 해당 노드로 이동한다. 조건 함수와 매핑의 분리가 중요하다. 조건 함수는 순수 로직이고, 매핑은 로직 결과를 노드 이름으로 변환하는 테이블이다. 함정은 조건 함수가 반환하는 값이 매핑에 없으면 런타임 오류가 발생한다는 점이다. 가능한 반환값 전부를 매핑에 포함해야 한다.4. tools_condition — 내장 조건 함수 해부
tools_condition은 LangGraph가 제공하는 사전 구현 조건 함수다. 직접 구현하지 않아도 되는 가장 흔한 패턴이다. 동작 원리는 간단하다.# tools_condition의 내부 동작 (단순화) def tools_condition(state: AgentState) -> str: last_message = state["messages"][-1] # 가장 최근 메시지 # AIMessage에 tool_calls가 있으면 "tools" 반환 if hasattr(last_message, "tool_calls") and last_message.tool_calls: return "tools" # 없으면 END 반환 return END상태의 마지막 메시지가
AIMessage이고tool_calls가 비어 있지 않으면"tools"를 반환해 tools 노드로 이동하고,tool_calls가 없으면END를 반환해 그래프 실행을 종료한다.tools_condition의 분기 로직이다. LLM이 응답할 때 도구가 필요하다고 판단하면 응답AIMessage의tool_calls필드에 호출 정보를 담는다. 도구가 필요 없다고 판단하면 일반 텍스트 응답만 반환하고tool_calls는 비어 있다.tools_condition은 이 차이를 읽어 라우팅 결정을 내린다. 함정은tool_calls가 빈 리스트일 때도END로 처리된다는 점이다. LLM이 의도치 않게tool_calls를 빈 값으로 반환하는 경우 루프가 예상보다 일찍 종료된다. 실제 에이전트 디버깅에서 자주 마주치는 패턴이다.5. ToolNode — 도구 실행을 자동화하는 사전 구현 노드
ToolNode는 도구 실행 전체를 자동 처리하는 사전 구현 노드다. 직접 구현하면 반복 작업이 되는 세 단계를 처리한다.ToolNode의 내부 동작 5단계를 보여주는 다이어그램이다. 개발자가 직접 구현해야 한다면 매번 반복해야 하는 작업들이다. 마지막 메시지 추출, tool_calls 파싱, 도구 실행, ToolMessage 변환, 반환까지 모두 ToolNode가 처리한다.
ToolNode(tools)에서tools는 도구 함수 목록이다. ToolNode는tool_calls에 있는 도구 이름을 이 목록에서 찾아 실행한다. 함정은tool_calls에 등록되지 않은 도구 이름이 있으면 ToolNode가 오류를 낸다는 점이다. LLM이 등록된 도구 목록 밖의 이름을 환각(hallucination)하는 경우가 있다. LLM에 전달하는 도구 목록과 ToolNode에 전달하는 도구 목록이 반드시 일치해야 한다.6. START와 END — 특수 노드의 역할
START와END는 LangGraph의 특수 노드다. 처리 로직이 없는, 진입점과 종료점을 표현하는 노드다.START는 그래프의 첫 번째 노드가 무엇인지를 명시하기 위해 존재한다.builder.add_edge(START, "agent")는 "그래프 실행이 시작되면 agent 노드부터 실행하라"는 선언이다. 이 선언이 없으면 그래프가 어디서 시작해야 하는지 알 수 없다.END는 그래프 실행을 종료하는 지점을 표현한다. 조건부 엣지에서 반환값으로END를 사용하면 "이 조건에서 그래프를 멈춰라"가 된다.위 코드 예시의 전체 그래프 구조다. START에서 agent로 시작하고, agent의 조건부 분기가 tools 또는 END로 향한다. tools에서 다시 agent로 돌아오는 엣지가 루프를 형성한다. 이 구조가 ReAct(Reasoning + Acting) 루프의 정확한 표현이다. LLM이 판단하고(
agent), 도구를 실행하고(tools), 다시 판단하는 사이클이tool_calls가 없을 때까지 반복된다. 루프 종료 조건은 LLM이 결정한다는 점이 핵심이다. 몇 번을 돌지는 코드에 하드코딩되어 있지 않다. 함정은 바로 여기 있다 — LLM이 판단을 못 내리거나 도구가 계속 실패하면 루프가 영원히 돌 수 있다.StateGraph(AgentState, config={"recursion_limit": 10})처럼 최대 반복 횟수를 제한하는 것이 프로덕션 코드의 필수 조치다.7. compile() — 설계도를 실행 가능한 객체로 변환
builder.compile()은 설계도 검증과 실행 객체 생성을 동시에 한다. 이 시점에 몇 가지 검증이 일어난다. 모든add_edge에서 참조한 노드 이름이 실제로 등록되었는지, 진입점(START에서의 엣지)이 존재하는지, 도달 불가능한 노드는 없는지를 확인한다. 문제가 있으면 이 시점에 예외가 발생한다.컴파일된 그래프는 두 가지 실행 방법을 제공한다.
# 동기 실행 — 최종 상태만 반환 result = graph.invoke({"messages": [HumanMessage(content="2 + 3은?")]}) print(result["messages"][-1].content) # 최종 AIMessage # 스트리밍 실행 — 각 노드의 업데이트를 순서대로 받음 for chunk in graph.stream( {"messages": [HumanMessage(content="2 + 3은?")]}, stream_mode="updates" # 각 노드 완료 시 상태 변화 반환 ): print(chunk) # {"agent": {"messages": [AIMessage(...)]}} 형태invoke는 그래프 실행이 완료된 뒤 최종 상태 전체를 반환한다.stream은 각 노드가 완료될 때마다 상태 업데이트를 순서대로 반환한다. 디버깅 시stream_mode="updates"를 사용하면 어느 노드에서 어떤 메시지가 추가되었는지 추적할 수 있다. 함정은invoke는 루프가 길어질수록 응답이 늦어진다는 것이다. 사용자에게 피드백이 필요한 경우stream을 사용해야 한다. 특히 도구 실행에 시간이 걸리는 경우 중간 상태를 스트리밍으로 전달해 사용자 경험을 개선할 수 있다.8. 전체 조립 패턴을 한눈에
지금까지 각 API를 분해했다. 마지막으로 전체 조립 순서와 각 단계가 무엇을 하는지를 하나의 흐름으로 정리한다.
StateGraph 조립의 6단계 순서다. 각 단계는 선형으로 진행되고, 순서를 바꾸면 오류가 난다. 특히
add_edge와add_conditional_edges는 참조하는 노드가 먼저add_node로 등록되어 있어야 한다.compile()은 반드시 마지막이다. 컴파일 이후에add_node나add_edge를 호출해도 반영되지 않는다. 설계도는 컴파일 시점에 고정된다. 흐름 설명을 하자면, StateGraph에 상태 클래스를 전달하면 그래프가 상태 구조를 인식하고, 이후 add_node로 처리 단위를 등록하고, add_edge/add_conditional_edges로 흐름을 연결한 뒤 compile로 검증과 동시에 실행 가능한 객체를 만든다. 실제 실행은 invoke나 stream 호출 시 시작된다. 놓치기 쉬운 함정은 설계도를 수정해야 할 때 컴파일된 그래프를 수정할 수 없으므로 빌더부터 다시 만들어야 한다는 점이다.9. 마무리 — 설계도가 에이전트를 만드는 방법
StateGraph는 에이전트 워크플로우를 코드 로직이 아니라 선언으로 표현하는 방법이다. "agent 다음에 tools로 가거나 종료한다"는 동작이 코드 안에 if-else로 숨어 있는 것이 아니라,add_conditional_edges라는 선언으로 구조에 드러난다.각 API의 핵심을 다시 정리하면 이렇다.
add_node는 처리 단위를 이름과 함께 등록한다.add_edge는 항상 일어나는 전환을 연결한다.add_conditional_edges는 상태에 따라 달라지는 전환을 조건 함수와 매핑으로 선언한다.compile()은 설계도를 검증하고 실행 가능한 객체로 변환한다.이 구조가 중요한 이유는 복잡도 관리 때문이다. 노드 수가 늘고 엣지가 복잡해져도, 각 노드는 "무엇을 처리할지"만 담당하고 "다음에 어디로 갈지"는 엣지가 담당한다. 관심사가 분리된다. 흐름을 바꾸려면 엣지 선언만 수정하면 된다. 노드 로직은 건드리지 않아도 된다. 에이전트가 단순한 도구 사용을 넘어 다중 분기, 루프, 외부 조건에 따른 라우팅을 필요로 하기 시작하는 순간, 이 선언적 구조의 가치가 드러난다.
이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.
'IT' 카테고리의 다른 글
LangGraph 조건부 라우팅 — 상태 값으로 다음 노드를 결정하는 방법 (0) 2026.06.27 LLM을 분류기로 쓰기 — SystemMessage와 HumanMessage로 Classifier Node 만들기 (0) 2026.06.27 LangGraph 상태에 메시지 외 필드 추가하기 — RouterState 설계 (0) 2026.06.26 graph.invoke vs graph.stream — 에이전트 실행의 두 가지 방식 (1) 2026.06.26 bind_tools — LLM이 도구를 인식하는 방법 (0) 2026.06.26 LangGraph의 상태(State) 설계 — Annotated와 add_messages가 하는 일 (0) 2026.06.25 LangGraph가 등장한 이유 — 선형 체인을 넘어 그래프로 (0) 2026.06.25 Retriever를 에이전트 도구로 — RAG 패턴 구현 (0) 2026.06.24 LLM에 코드 실행 능력 붙이기 — PythonAstREPLTool (0) 2026.06.24 Pydantic Field로 LLM 출력 스키마를 제약하는 방법 (0) 2026.06.23