add_edge는 다음 노드가 정의 시점에 고정됩니다.
add_conditional_edges는 출발 노드가 끝난 뒤 라우팅 함수에 현재 State를 넘기고, 돌려받은 노드 이름으로 이동합니다.
이 파일은 LLM이 분류한 intent 값에 따라 LLM 답변 경로와 고정 응답 경로로 나눕니다.
classify가 intent를 State에 기록하고, route_by_intent가 그 값을 읽어 두 노드 중 하나를 고릅니다. 두 경로 모두 END로 끝납니다.
question / exclaim 중 하나를 받아 {"intent": ...}로 반환합니다. 판단 결과가 State에 남습니다.
stream_mode="updates"에도 나타나지 않습니다.
response만 채웁니다. 답변 생성 LLM 호출은 질문 경로에서만 일어납니다.
판단을 라우팅 함수 안에서 바로 해도 동작은 같습니다. 하지만 노드에서 intent를 State에 기록해 두면 최종 결과에서 왜 이 경로로 갔는지 확인할 수 있고, 라우팅 함수는 부수 효과 없는 단순 분기로 유지됩니다.
분류 결과를 자유 텍스트로 받으면 "질문입니다", "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}
Field(description=...)은 모델에게 전달되는 분류 기준입니다. 기준을 바꾸고 싶으면 여기를 고칩니다.llm과 분리해 temperature=0으로 두었습니다. 같은 입력이 실행마다 다른 경로로 가는 일을 줄이기 위해서입니다.route_by_intent와 그래프 구조는 그대로입니다. 판단을 노드에 둔 덕분에 라우팅 쪽은 수정할 필요가 없었습니다.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 출력이라 실행할 때마다 달라질 수 있습니다. 아래 두 개는 물음표 규칙에서 잘못 분류되던 입력입니다.
| 입력 | 이전: "?" in user_input | 현재: LLM 분류 |
|---|---|---|
| 오늘 사내 식당 메뉴 뭐야? | question | question |
| 와 배포가 한 번에 성공했다 | exclaim | exclaim |
| LangGraph가 뭐야? | question | question |
| 오늘 메뉴 뭐야 | exclaim (오분류) | question |
| 이게 되네?! | question (오분류) | exclaim |
| 항목 | 물음표 규칙 | LLM 분류 |
|---|---|---|
| 입력당 LLM 호출 | 질문 0+1 / 감탄 0 | 질문 1+1 / 감탄 1 |
| 지연 시간 | 거의 없음 | 분류 호출만큼 증가 |
| 결과 재현성 | 항상 같음 | temperature=0이어도 완전히 보장되지는 않음 |
| 실패 지점 | 없음 | 네트워크 오류, 스로틀링 시 그래프 전체가 실패 |
트래픽이 많다면 명확한 경우(?로 끝나는 문장 등)는 규칙으로 먼저 거르고, 애매한 경우만 LLM에 넘기는 식으로 섞어 쓰기도 합니다. 이 경우에도 바뀌는 곳은 classify 하나입니다.
add_conditional_edges(source, path_fn): source 노드가 끝나면 path_fn(state)의 반환값으로 다음 노드가 정해집니다.with_structured_output + Literal로 반환 값을 라우팅이 아는 값으로 제한합니다.Literal 반환 타입이나 path_map으로 명시해 두면 그래프 시각화가 정확해집니다.