실무 Multi-Agent 오케스트레이션 19장 · 조건 분기와 라우팅 7 / 7 ← 이전목차다음 → TechLead Cro

19장. 조건 분기와 라우팅

실습문제와 해답

19장의 라우터를 직접 시험하고 고쳐 봅니다. 먼저 스스로 해 본 뒤 해답을 펼쳐 보세요. 해답은 새 파일로 만들어 실행합니다.


문제 1 — 문의 열 개는 어느 길로 가는가

지금까지 다뤄 온 문의 가운데 열 개를 골랐습니다. 새 파일 ex19_route_check.py를 만들어, 각 문의가 무엇으로 분류되고 어느 노드로 가는지만 출력하세요. 갈래 노드는 실행하지 않습니다.

  • 힌트: from lesson19_router import classify_node, route로 두 함수만 가져옵니다. 노드도 분기 함수도 그냥 파이썬 함수이므로 그래프 없이 직접 부를 수 있습니다.
  • 실행하기 전에, 열 개가 각각 어느 노드로 갈지 라우팅 표를 보고 적어 두세요.
해답 보기
from lesson19_router import classify_node, route

QUESTIONS = [
    "제 최근 주문 지금 어디까지 왔어요?",
    "스테인리스 텀블러 열탕 소독 되나요? 민트색 재고도 있어요?",
    "불량품이 왔는데 반품 배송비를 제가 내야 하나요?",
    "하루클럽 골드 등급은 어떻게 되는 거예요? 혜택도 알려주세요.",
    "저 VIP인데요, 단순 변심 반품해도 배송비 내야 해요?",
    "지난주에 산 운동화가 작아서요. 검정 270으로 교환하면 배송비 얼마고, 재고는 있어요?",
    "콜드브루 주문한 거 배송지를 회사 주소로 바꿔주세요.",
    "AI 말고 사람이랑 통화하고 싶어요.",
    "요즘 비트코인 사도 될까요?",
    "카드 결제가 세 번이나 실패했어요. 빨리 해결해 주세요.",
]

for q in QUESTIONS:
    state = {"question": q, "intent": "", "answer": "", "log": []}
    state.update(classify_node(state))        # 분류만 한다 (LLM 호출 1번)
    print(f"{state['intent']:<8} → {route(state):<8} | {q}")
$ python ex19_route_check.py

실행 결과

주문배송조회   → order    | 제 최근 주문 지금 어디까지 왔어요?
제품문의     → general  | 스테인리스 텀블러 열탕 소독 되나요? 민트색 재고도 있어요?
환불교환     → policy   | 불량품이 왔는데 반품 배송비를 제가 내야 하나요?
멤버십적립금   → policy   | 하루클럽 골드 등급은 어떻게 되는 거예요? 혜택도 알려주세요.
환불교환     → policy   | 저 VIP인데요, 단순 변심 반품해도 배송비 내야 해요?
환불교환     → policy   | 지난주에 산 운동화가 작아서요. 검정 270으로 교환하면 배송비 얼마고, 재고는 있어요?
주문배송조회   → order    | 콜드브루 주문한 거 배송지를 회사 주소로 바꿔주세요.
상담원연결    → human    | AI 말고 사람이랑 통화하고 싶어요.
기타       → general  | 요즘 비트코인 사도 될까요?
계정결제     → human    | 카드 결제가 세 번이나 실패했어요. 빨리 해결해 주세요.

해설 — 열 개 모두 길이 정해졌고, 두 번 실행했을 때 두 번 모두 같은 결과였습니다. 노드별로 묶어 보면 이렇습니다.

간 노드 문의 그 노드로 간 까닭
order 최근 주문, 배송지 변경 주문배송조회 → 답이 주문 데이터에 있다
policy 불량품 반품, 골드 등급, VIP 반품, 운동화 교환 환불교환·멤버십적립금 → 답이 정책 문서에 있다
human 사람이랑 통화, 카드 결제 실패 상담원연결·계정결제 → 접수가 필요하다
general 텀블러 열탕 소독, 비트코인 제품문의·기타 → 표에 없는 유형이라 기본 경로로

"골드 등급" 문의는 멤버십적립금으로, "불량품 반품" 문의는 환불교환으로 분류가 달랐지만 같은 policy 노드로 갔습니다. 처리가 같으면 길도 같다는 라우팅 표의 결정이 그대로 보입니다. "비트코인" 문의는 기타로 분류되어 기본 경로로 갔고, 일반 노드는 쇼핑과 무관한 질문에 답하지 않도록 적어 두었습니다.

이 파일이 갈래 노드를 한 번도 실행하지 않고 길만 확인했다는 것을 기억해 둡니다. 문의 하나에 LLM 호출 한 번입니다. 판단과 길 안내를 노드와 함수로 나눠 두었기 때문에, 라우터만 따로 떼어 빠르고 싸게 시험할 수 있습니다. 한 문장에 일이 둘 든 문의("운동화 교환 배송비와 재고")를 작업으로 나눠 처리하는 구조는 20장에서 만듭니다.


문제 2 — 급한 문의는 유형과 상관없이 사람에게

4장의 분류기는 유형(intent)과 함께 긴급도(urgency) 도 돌려줍니다. 19장의 그래프는 이 값을 쓰지 않았습니다. 새 파일 ex19_urgent.py를 만들어 다음 규칙을 넣으세요.

  • 긴급도가 "높음" 이면 유형과 상관없이 human 노드로 보낸다.
  • 그렇지 않으면 19장의 라우팅 표를 그대로 따른다.

아래 두 문의로 확인합니다.

  1. "주문번호 HR20260708076 지금 어디쯤인가요? 내일까지 꼭 필요해요."
  2. "단순 변심 반품이면 배송비 얼마예요?"
  • 힌트: 상태에 urgency 칸을 추가해야 분기 함수가 그 값을 볼 수 있습니다. 갈래 노드 네 개는 lesson19_router.py에서 가져와 그대로 씁니다.
해답 보기
from typing import TypedDict

from langgraph.graph import END, START, StateGraph

from lesson04_intent_classifier import classify
from lesson19_router import general_node, human_node, order_node, policy_node, route


class CSState(TypedDict):
    question: str
    intent: str
    urgency: str          # 새로 추가한 칸
    answer: str
    log: list[str]


def classify_node(state: CSState) -> dict:
    result = classify(state["question"])
    return {"intent": result.intent.value,
            "urgency": result.urgency.value,
            "log": state["log"] + [f"분류 → {result.intent.value}/{result.urgency.value}"]}


def route_with_urgency(state: CSState) -> str:
    if state["urgency"] == "높음":      # 급한 문의는 유형과 상관없이 사람에게
        return "human"
    return route(state)                 # 나머지는 19장의 표 그대로


builder = StateGraph(CSState)
builder.add_node("classify", classify_node)
builder.add_node("order", order_node)
builder.add_node("policy", policy_node)
builder.add_node("human", human_node)
builder.add_node("general", general_node)
builder.add_edge(START, "classify")
builder.add_conditional_edges("classify", route_with_urgency,
                              ["order", "policy", "human", "general"])
for node in ("order", "policy", "human", "general"):
    builder.add_edge(node, END)
graph = builder.compile()

for q in ["주문번호 HR20260708076 지금 어디쯤인가요? 내일까지 꼭 필요해요.",
          "단순 변심 반품이면 배송비 얼마예요?"]:
    result = graph.invoke({"question": q, "intent": "", "urgency": "",
                           "answer": "", "log": []})
    print(f"\n고객: {q}")
    print(f"  경로: {' → '.join(result['log'])}")
    print(f"하루: {result['answer']}")
$ python ex19_urgent.py

이전 정책 노드의 실행 기록 (현재 정책 노드는 14장을 재사용하여 doc_title · article 전체를 표시합니다. 아래는 변경 후 재실행한 기록이 아닙니다.)

고객: 주문번호 HR20260708076 지금 어디쯤인가요? 내일까지 꼭 필요해요.
  경로: 분류 → 주문배송조회/높음 → 티켓 발행 T00013
하루: 상담원 연결이 필요한 문의로 접수했습니다. 접수번호는 T00013 입니다. 순차적으로 연락드릴게요.

고객: 단순 변심 반품이면 배송비 얼마예요?
  경로: 분류 → 환불교환/낮음 → 정책 RAG 노드 처리
하루: 단순 변심 반품 시 편도 배송비 3,000원을 환불 금액에서 차감하며, 고객이 부담합니다. (근거: 제6조, 자주 묻는 질문 (FAQ))

해설 — 첫 문의는 주문배송조회/높음으로 분류되었습니다. 원래 표대로라면 order로 갔겠지만, 새 규칙이 먼저 걸려 티켓이 발행되었습니다. 둘째 문의는 낮음이라 표대로 policy로 갔습니다.

바뀐 것을 세어 봅니다.

  • 상태에 칸 하나(urgency)
  • 분류 노드가 그 칸을 채우는 한 줄
  • 분기 함수의 if 한 줄

갈래 노드 네 개는 한 글자도 고치지 않았습니다. 길을 정하는 규칙이 코드 한곳에 모여 있기 때문에, 규칙을 바꿀 때 고칠 곳도 한곳입니다.

"급하면 사람에게"가 맞는 규칙인지, "급해도 조회는 해 주고 티켓도 함께 만든다"가 맞는지는 코드가 아니라 업무를 아는 사람이 정할 일입니다. 코드는 그 결정을 한 줄로 옮길 수 있게 해 줍니다.


문제 3 — 어디를 고치면 되는가

다음 세 가지 요구가 왔습니다. 각각 lesson19_router.py의 어느 부분을 고치면 되는지 글로 답하세요. (코드를 실행하지 않습니다.)

  1. "멤버십 문의가 요즘 많으니, 정책 노드와 따로 membership 노드를 만들어 그쪽으로 보내자."
  2. "상품 문의의 답이 길다. 일반 노드의 답을 두 문장 이내로 줄이자."
  3. "'환불해 주세요'를 주문 문의로 분류하는 일이 잦다. 환불 문의로 분류되게 하자."
해답 보기
요구 고칠 곳 왜 그곳인가
1. 멤버십을 따로 보낸다 노드 추가 + route의 딕셔너리 + 조립 부분 새 노드 함수를 만들어 add_node로 등록하고, 딕셔너리의 "멤버십적립금": "policy"를 "membership"으로 바꾸고, add_conditional_edges의 목록과 END로 가는 엣지에 "membership"을 더합니다. 분류기는 건드리지 않습니다
2. 일반 노드의 답을 줄인다 general_node의 시스템 프롬프트 길은 그대로입니다. 바꿀 것은 도착한 노드의 지시입니다. 끝의 "존댓말 3문장"을 "존댓말 2문장 이내"로 고칩니다. 다른 노드의 답 길이는 바뀌지 않습니다
3. 분류가 틀린다 4장의 분류기 (lesson04_intent_classifier.py의 CLASSIFY_SYSTEM) 로그에 분류 → 주문배송조회가 찍혔다면 길 안내는 그 값대로 정확히 한 것입니다. 틀린 곳은 판단입니다. route를 고쳐서는 해결되지 않습니다

세 요구는 겉으로는 모두 "답을 바꾸고 싶다"입니다. 그런데 고칠 곳은 길 안내, 갈래 노드, 분류기로 전부 다릅니다. 실행 결과의 경로 한 줄을 보면 셋 중 어디인지 가려낼 수 있습니다.

  • 분류 결과를 바꾸고 싶다 → 분류기
  • 분류는 그대로 두고 가는 노드를 바꾸고 싶다 → 라우팅 표
  • 노드는 그대로 두고 답하는 방식을 바꾸고 싶다 → 그 노드의 도구와 프롬프트
← 이전 절정리와 체크리스트20장 →왜 복합 요청 분해가 필요한가 — 한 문장에 일이 세 개다
오명운 · macro@prag-ai.com