ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • LangGraph 조건부 라우팅 — 상태 값으로 다음 노드를 결정하는 방법
    IT 2026. 6. 27. 22:00
    LangGraph 조건부 라우팅 — 상태 값으로 다음 노드를 결정하는 방법

    LangGraph로 멀티스텝 AI 파이프라인을 만들다 보면 반드시 마주치는 패턴이 있다. 입력이 어떤 종류인지에 따라 다른 처리 경로로 보내는 것 — 이른바 조건부 라우팅(conditional routing)이다. 단순해 보이지만 실제로 구현하다 보면 "라우팅 함수가 정확히 무엇을 해야 하는가", "add_conditional_edges의 세 번째 인자는 언제 필요한가" 같은 질문에서 한 번씩 막힌다. 이 글은 그 지점을 정확히 짚는다.

    라우팅 함수의 역할: 오직 "어디로 갈지"만 결정

    LangGraph에서 라우팅 함수를 처음 만들 때 가장 흔한 실수는 이 함수 안에서 무언가를 "생성"하려는 것이다. 라우팅 함수의 역할은 단 하나다 — state를 보고 다음에 실행할 노드의 이름을 문자열로 반환하는 것. 응답 생성도, 상태 변경도, 로그 출력도 이 함수의 책임이 아니다.

    # Literal로 반환 가능한 노드 이름을 열거 — 오타 시 타입 에러로 즉시 발견
    def route_by_category(state: State) -> Literal["recommend_handler", "info_handler", "general_handler"]:
        if state["category"] == "recommend":  return "recommend_handler"
        elif state["category"] == "info":     return "info_handler"
        else:                                 return "general_handler"
    

    반환 타입에 Literal["recommend_handler", "info_handler", "general_handler"]를 명시한 것에 주목하자. 이것은 단순한 문서화가 아니다. 반환 가능한 값을 타입으로 열거해 둠으로써 오타로 "recommend_handlerr"를 반환해도 mypy나 pyright가 즉시 경고를 띄운다. 노드 이름을 바꿀 때도 이 Literal이 변경 범위를 코드에서 명시해 준다 — Literal 수정 없이 노드 이름만 바꾸면 타입 에러가 발생하므로 놓칠 수가 없다.

    전체 그래프 구조

    diagram

    이 다이어그램은 전체 그래프의 흐름을 보여준다. START에서 classifier 노드로 진입하고, classifier가 상태에 category를 기록하면 라우팅 함수가 그 값을 읽어 세 갈래 중 하나로 분기한다. 각 handler 노드는 자신의 전문 영역에 특화된 system prompt로 응답을 생성한 뒤 END로 향한다. classifier는 응답을 만들지 않는다 — 분류만 한다. 이 경계가 흐려지면 그래프 전체의 책임이 뒤섞이기 시작한다.

    add_conditional_edges 연결

    그래프를 구성하는 코드에서 핵심은 add_conditional_edges의 세 번째 인자다.

    graph = StateGraph(State)
    graph.add_node("classifier", classify_node)
    graph.add_node("recommend_handler", recommend_node)
    graph.add_node("info_handler", info_node)
    graph.add_node("general_handler", general_node)
    
    graph.add_edge(START, "classifier")
    graph.add_conditional_edges("classifier", route_by_category,
        {"recommend_handler": "recommend_handler", "info_handler": "info_handler", "general_handler": "general_handler"})
    
    app = graph.compile()
    

    세 번째 인자인 매핑 딕셔너리는 "라우팅 함수가 이 값을 반환하면 저 노드로 가라"는 명시적 연결표다. 위 예시처럼 반환값과 노드 이름이 동일하면 딕셔너리를 생략해도 자동으로 연결되지만, 명시적으로 두는 편이 낫다 — 나중에 노드 이름을 리팩터링할 때 매핑 딕셔너리를 보면 어디를 함께 수정해야 하는지 한눈에 보인다. tools_condition처럼 LangGraph가 사전 제공하는 라우팅 함수를 쓸 수도 있고, 위처럼 직접 만들 수도 있다.

    add_conditional_edges 내부 동작

    diagram

    add_conditional_edges가 실제로 하는 일을 단계별로 풀면 이렇다. classifier 노드가 실행을 마치면 LangGraph 런타임이 route_by_category를 호출한다. 이 함수는 현재 state를 받아 노드 이름 문자열을 반환한다. 런타임은 그 반환값을 세 번째 인자로 넘긴 딕셔너리에서 조회해 실제 노드 이름을 확정하고, 그 노드로 실행을 이동시킨다. 함정: 라우팅 함수가 딕셔너리에 없는 값을 반환하면 런타임 에러가 발생한다. Literal 타입 선언과 딕셔너리 키를 항상 일치시켜야 하는 이유가 여기 있다.

    왜 이 패턴인가 — 단일 LLM과의 비교

    직관적으로 "LLM 하나에 다 시키면 안 되나?"라는 의문이 생긴다. 구조 차이를 눈으로 보자.

    diagramdiagram

    단일 LLM 방식은 "영화 추천도 잘하고, 영화 정보도 잘 알려주고, 일반 대화도 잘해야 한다"는 요구를 한 system prompt에 담아야 한다. 요구사항이 늘수록 prompt가 길어지고 서로 간섭하기 시작한다. 반면 라우터 패턴에서 각 handler의 system prompt는 자신의 역할만 기술한다 — recommend_handler는 "영화 추천 전문가"이고 info_handler는 "영화 정보 전문가"다. 새로운 카테고리(예: "review_handler")를 추가할 때도 기존 handler를 건드리지 않는다. 노드 하나를 추가하고 라우팅 함수에 분기를 하나 더 넣는 것으로 끝난다.

    새 카테고리 추가 시 변경 범위

    diagram

    이 다이어그램이 이 패턴의 핵심 장점을 요약한다. 변경이 필요한 곳이 명확하게 다섯 군데로 제한되고, 기존 노드(recommend_handler, info_handler, general_handler)는 손대지 않아도 된다. Literal 타입이 변경 범위를 코드에서 강제하기 때문에 4번(매핑 추가)을 빠뜨리면 3번에서 타입 에러가 발생한다. 함정 하나 더: 5번(add_edge → END)을 빠뜨려도 타입 에러는 없다. 런타임에서 그래프가 새 노드에 도달한 후 다음 엣지를 찾지 못해 중단된다. 노드 추가 후엔 항상 종료 엣지도 함께 확인하는 습관이 필요하다.

    정리

    LangGraph 조건부 라우팅의 핵심을 세 문장으로 압축하면 이렇다. 라우팅 함수는 state를 읽어 노드 이름 문자열만 반환한다 — 아무것도 생성하지 않는다. Literal 반환 타입은 단순 문서화가 아니라 노드 이름 변경 시 수정 범위를 강제하는 안전장치다. 역할 분리는 각 handler의 system prompt를 단순하게 유지하고 새 카테고리 추가를 기존 코드 변경 없이 가능하게 한다.

    파이프라인이 복잡해질수록 이 세 가지 원칙이 그래프를 읽기 쉽고 확장 가능하게 유지하는 토대가 된다.


    이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.

Designed by Tistory.