ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • LangGraph StateGraph 완전 분해 — 노드·엣지·조건부 라우팅
    IT 2026. 6. 25. 23:00
    LangGraph StateGraph 완전 분해 — 노드·엣지·조건부 라우팅

    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의 사전 구현 노드다. 상태에서 마지막 AIMessagetool_calls를 자동으로 추출하고, 해당 도구를 실행하고, 결과를 ToolMessage로 변환해서 반환한다. 이 과정을 직접 구현할 필요가 없다.

    diagram

    add_node가 하는 일을 보여주는 다이어그램이다. 노드 이름과 함수 둘 다 그래프 내부 레지스트리에 저장된다. 이름은 엣지 연결 시 참조 키로 사용되고, 함수는 그 노드가 실행될 때 호출된다. 주의할 점은 이 시점에 함수가 실행되지 않는다는 것이다. add_node는 등록이지 실행이 아니다. 실행은 compile() 이후 invokestream을 호출할 때 일어난다. 함정은 이름 충돌이다. 같은 이름으로 add_node를 두 번 호출하면 덮어써진다. 또한 add_edge에서 참조하는 이름과 add_node에서 등록한 이름이 정확히 일치해야 한다 — 오타가 있으면 컴파일 시점에 오류가 난다.

    3. add_edge vs add_conditional_edges — 두 종류의 전환

    노드를 등록했다면 그 사이를 연결해야 한다. 연결에는 두 종류가 있다. 무조건 전환과 조건부 전환이다. 아래 두 다이어그램이 차이를 보여준다.

    diagram

    add_edge(A, B)는 A가 완료되면 항상 B로 이동하는 연결이다. 조건이 없다. 예를 들어 add_edge("tools", "agent")는 도구 실행이 끝나면 반드시 agent 노드로 돌아오는 루프를 만든다. 도구 실행 후에는 항상 LLM이 다시 판단해야 하므로, 이 전환은 무조건적이다.

    diagram

    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를 반환해 그래프 실행을 종료한다.

    diagram

    tools_condition의 분기 로직이다. LLM이 응답할 때 도구가 필요하다고 판단하면 응답 AIMessagetool_calls 필드에 호출 정보를 담는다. 도구가 필요 없다고 판단하면 일반 텍스트 응답만 반환하고 tool_calls는 비어 있다. tools_condition은 이 차이를 읽어 라우팅 결정을 내린다. 함정은 tool_calls가 빈 리스트일 때도 END로 처리된다는 점이다. LLM이 의도치 않게 tool_calls를 빈 값으로 반환하는 경우 루프가 예상보다 일찍 종료된다. 실제 에이전트 디버깅에서 자주 마주치는 패턴이다.

    5. ToolNode — 도구 실행을 자동화하는 사전 구현 노드

    ToolNode는 도구 실행 전체를 자동 처리하는 사전 구현 노드다. 직접 구현하면 반복 작업이 되는 세 단계를 처리한다.

    diagram

    ToolNode의 내부 동작 5단계를 보여주는 다이어그램이다. 개발자가 직접 구현해야 한다면 매번 반복해야 하는 작업들이다. 마지막 메시지 추출, tool_calls 파싱, 도구 실행, ToolMessage 변환, 반환까지 모두 ToolNode가 처리한다. ToolNode(tools)에서 tools는 도구 함수 목록이다. ToolNode는 tool_calls에 있는 도구 이름을 이 목록에서 찾아 실행한다. 함정은 tool_calls에 등록되지 않은 도구 이름이 있으면 ToolNode가 오류를 낸다는 점이다. LLM이 등록된 도구 목록 밖의 이름을 환각(hallucination)하는 경우가 있다. LLM에 전달하는 도구 목록과 ToolNode에 전달하는 도구 목록이 반드시 일치해야 한다.

    6. START와 END — 특수 노드의 역할

    STARTEND는 LangGraph의 특수 노드다. 처리 로직이 없는, 진입점과 종료점을 표현하는 노드다.

    START는 그래프의 첫 번째 노드가 무엇인지를 명시하기 위해 존재한다. builder.add_edge(START, "agent")는 "그래프 실행이 시작되면 agent 노드부터 실행하라"는 선언이다. 이 선언이 없으면 그래프가 어디서 시작해야 하는지 알 수 없다.

    END는 그래프 실행을 종료하는 지점을 표현한다. 조건부 엣지에서 반환값으로 END를 사용하면 "이 조건에서 그래프를 멈춰라"가 된다.

    diagram

    위 코드 예시의 전체 그래프 구조다. 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를 분해했다. 마지막으로 전체 조립 순서와 각 단계가 무엇을 하는지를 하나의 흐름으로 정리한다.

    diagram

    StateGraph 조립의 6단계 순서다. 각 단계는 선형으로 진행되고, 순서를 바꾸면 오류가 난다. 특히 add_edgeadd_conditional_edges는 참조하는 노드가 먼저 add_node로 등록되어 있어야 한다. compile()은 반드시 마지막이다. 컴파일 이후에 add_nodeadd_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가 초안을 생성하고, 작성자가 검토·편집하였습니다.

Designed by Tistory.