day3_practice/conditional.py

조건부 엣지: 다음 노드를 런타임에 고르기

add_edge는 다음 노드가 정의 시점에 고정됩니다. add_conditional_edges는 출발 노드가 끝난 뒤 라우팅 함수에 현재 State를 넘기고, 돌려받은 노드 이름으로 이동합니다. 이 파일은 LLM이 분류한 intent 값에 따라 LLM 답변 경로와 고정 응답 경로로 나눕니다.

그래프 구조

classify가 intent를 State에 기록하고, route_by_intent가 그 값을 읽어 두 노드 중 하나를 고릅니다. 두 경로 모두 END로 끝납니다.

START classify LLM 분류 → intent route_by _intent intent == "question" answer_question llm.invoke()로 답변 그 외 ("exclaim") react_exclaim 고정 문자열, LLM 없음 END

역할 분리

분류 노드 classify 분류용 LLM에 입력을 보내 question / exclaim 중 하나를 받아 {"intent": ...}로 반환합니다. 판단 결과가 State에 남습니다.
라우팅 함수 route_by_intent State를 읽기만 하고 다음 노드 이름을 문자열로 반환합니다. 노드가 아니므로 State를 갱신할 수 없고, stream_mode="updates"에도 나타나지 않습니다.
분기 대상 노드 answer_question / react_exclaim 각각 response만 채웁니다. 답변 생성 LLM 호출은 질문 경로에서만 일어납니다.

판단을 라우팅 함수 안에서 바로 해도 동작은 같습니다. 하지만 노드에서 intent를 State에 기록해 두면 최종 결과에서 왜 이 경로로 갔는지 확인할 수 있고, 라우팅 함수는 부수 효과 없는 단순 분기로 유지됩니다.

classify: 구조화 출력으로 분류

분류 결과를 자유 텍스트로 받으면 "질문입니다", "Question"처럼 라우팅 함수가 처리할 수 없는 값이 나올 수 있습니다. 그래서 Pydantic 스키마의 Literal로 허용 값을 두 개로 제한하고 with_structured_output으로 받습니다.

class Intent(BaseModel): intent: Literal["question", "exclaim"] = Field( description="답을 원하는 질문이면 'question', 감탄이나 소감이면 'exclaim'" ) # 분류는 매번 같은 답이 나와야 하므로 temperature=0으로 따로 둡니다 classifier = ChatBedrockConverse(..., temperature=0).with_structured_output(Intent) def classify(state): result = classifier.invoke(f"다음 문장이 답을 원하는 질문인지, 감탄이나 소감인지 분류하세요: {state['user_input']}") return {"intent": result.intent}

add_edge vs add_conditional_edges

고정 엣지
builder.add_edge("answer_question", END)

다음 노드가 정의할 때 정해집니다.

조건부 엣지
builder.add_conditional_edges( "classify", route_by_intent)

classify가 끝날 때마다 route_by_intent(state)를 호출해 다음 노드를 정합니다.

갈 수 있는 노드를 알려 주는 방법

라우팅 함수가 반환할 수 있는 노드 목록은 그래프 시각화(get_graph())와 검증에 쓰입니다. 이 파일은 반환 타입 힌트로 알려 줍니다.

def route_by_intent(state) -> Literal["answer_question", "react_exclaim"]:

같은 정보를 세 번째 인자(path_map)로 직접 줄 수도 있습니다. dict를 주면 라우팅 함수가 노드 이름 대신 라벨을 반환하게 만들 수 있습니다.

# 라우팅 함수는 intent 값을 그대로 반환하고, 매핑은 그래프 쪽에서 관리 builder.add_conditional_edges( "classify", lambda s: s["intent"], {"question": "answer_question", "exclaim": "react_exclaim"}, )

타입 힌트도 path_map도 없으면 그래프는 가능한 경로를 알 수 없어서, 시각화하면 모든 노드로 가는 점선이 그려집니다.

실행 결과

conditional.py를 실제로 실행한 결과입니다. 응답은 LLM 출력이라 실행할 때마다 달라질 수 있습니다. 아래 두 개는 물음표 규칙에서 잘못 분류되던 입력입니다.

"오늘 사내 식당 메뉴 뭐야?"
classify→intent="question"→answer_question
response죄송하지만 저는 귀사의 사내 식당 메뉴 정보에 접근할 수 없습니다.
"와 배포가 한 번에 성공했다"
classify→intent="exclaim"→react_exclaim
response'와 배포가 한 번에 성공했다' 라니 멋진 일이네요.
"LangGraph가 뭐야?"
classify→intent="question"→answer_question
responseLangGraph는 LLM 기반 애플리케이션을 상태 그래프(stateful graph)로 구성하고 관리할 수 있게 해주는 LangChain의 확장 프레임워크입니다.
"오늘 메뉴 뭐야"
classify→intent="question"→answer_question
response구체적인 장소나 상황을 알려주시면 메뉴를 확인해드릴 수 있어요.
"이게 되네?!"
classify→intent="exclaim"→react_exclaim
response'이게 되네?!' 라니 멋진 일이네요.

물음표 규칙 vs LLM 분류

입력이전: "?" in user_input현재: LLM 분류
오늘 사내 식당 메뉴 뭐야?questionquestion
와 배포가 한 번에 성공했다exclaimexclaim
LangGraph가 뭐야?questionquestion
오늘 메뉴 뭐야exclaim (오분류)question
이게 되네?!question (오분류)exclaim

대가: 분류에도 LLM 호출이 든다

항목물음표 규칙LLM 분류
입력당 LLM 호출질문 0+1 / 감탄 0질문 1+1 / 감탄 1
지연 시간거의 없음분류 호출만큼 증가
결과 재현성항상 같음temperature=0이어도 완전히 보장되지는 않음
실패 지점없음네트워크 오류, 스로틀링 시 그래프 전체가 실패

트래픽이 많다면 명확한 경우(?로 끝나는 문장 등)는 규칙으로 먼저 거르고, 애매한 경우만 LLM에 넘기는 식으로 섞어 쓰기도 합니다. 이 경우에도 바뀌는 곳은 classify 하나입니다.

정리