12. 조건부 엣지와 도구 호출
- 조건부 엣지(conditional edge)로 그래프에 분기를 만든다
- 라우터 함수가 어떻게 다음 노드를 결정하는지 이해한다
- 그래프 안에서 도구를 호출하는 에이전트 루프를 만든다
- 사이클(cycle)이 어떻게 "재시도·반복"을 가능하게 하는지 본다
비유로 시작하기: 갈림길의 안내원
앞 장의 그래프는 START → 노드 → END로 한 길만 갔습니다. 하지만 현실은 갈림길투성이입니다. "이 고객 문의가 결제 관련인가, 기술 지원인가?"에 따라 가는 길이 달라야 합니다.
조건부 엣지가 이 갈림길을 만듭니다. 한 노드가 끝나면, 라우터 함수가 현재 상태를 보고 "다음엔 어디로 갈지" 결정합니다. 갈림길에 선 안내원이 상황을 보고 길을 가리키는 것과 같습니다.
조건부 엣지 만들기
강의의 고객 지원 라우팅 예제를 따라가 봅시다. 먼저 분류 노드가 메시지를 보고 카테고리를 정하고, 라우터가 그 결과로 다음 노드를 고릅니다.
from typing import TypedDict, Literal from langgraph.graph import StateGraph, START, END class State(TypedDict): message: str category: str reply: str def classify(state: State) -> dict: # 실전에서는 LLM으로 분류. 여기서는 단순 규칙. msg = state["message"] cat = "billing" if "환불" in msg or "결제" in msg else "general" return {"category": cat} def billing_node(state: State) -> dict: return {"reply": "결제팀이 도와드리겠습니다."} def general_node(state: State) -> dict: return {"reply": "일반 상담입니다."} # 라우터 함수 — 상태를 읽고 다음 노드 '이름'을 반환 def route_by_category(state: State) -> Literal["billing_node", "general_node"]: return "billing_node" if state["category"] == "billing" else "general_node" builder = StateGraph(State) builder.add_node("classify", classify) builder.add_node("billing_node", billing_node) builder.add_node("general_node", general_node) builder.add_edge(START, "classify") # 조건부 엣지: classify 후 route_by_category가 다음 노드 결정 builder.add_conditional_edges( "classify", route_by_category, { "billing_node": "billing_node", "general_node": "general_node", }, ) builder.add_edge("billing_node", END) builder.add_edge("general_node", END) graph = builder.compile() print(graph.invoke({"message": "환불 받고 싶어요"})["reply"]) # 결제팀...
flowchart TD
classDef node fill:#80DEEA,stroke:#00838F,color:#000
classDef router fill:#FFE082,stroke:#F9A825,color:#000
classDef result fill:#A5D6A7,stroke:#2E7D32,color:#000
S([START]) --> C[classify 노드]:::node
C --> R{route_by_category<br/>라우터}:::router
R -->|billing| B[billing_node]:::node
R -->|general| G[general_node]:::node
B --> E([END]):::result
G --> E
라우터 함수의 규칙
라우터 함수에는 중요한 규칙이 있습니다. 상태를 읽고 문자열(다음 노드 이름)만 반환해야 합니다. LLM을 호출하거나 상태를 쓰거나 부수효과를 내면 안 됩니다. 모든 계산은 노드에서 하고, 라우터는 그 결과를 읽어 "어디로 갈지"만 정합니다.
또 하나: LangGraph는 소스 노드가 완전히 끝나고 상태 병합이 끝난 뒤 라우터를 호출합니다. 그래서 라우터가 실행될 때 상태에는 이전 노드가 쓴 모든 것이 이미 들어 있습니다. classify가 category를 쓰고, 라우터가 그것을 읽는 패턴이 자연스럽게 동작하는 이유입니다.
⚠️ 흔한 실수: 라우터 함수 안에서 LLM을 호출하거나 상태를 변경하는 것. 흐름이 꼬이고 디버깅이 어려워집니다. 계산은 노드, 결정은 라우터로 역할을 분리하세요.
사이클: 재시도와 에이전트 루프
조건부 엣지의 진짜 힘은 사이클(cycle), 즉 이전 노드로 되돌아가는 길을 만들 수 있다는 점입니다. 이것이 1부에서 배운 에이전트 루프를 그래프로 구현하는 방법입니다.
flowchart TD
classDef node fill:#80DEEA,stroke:#00838F,color:#000
classDef tool fill:#90CAF9,stroke:#1565C0,color:#000
classDef router fill:#FFE082,stroke:#F9A825,color:#000
classDef result fill:#A5D6A7,stroke:#2E7D32,color:#000
S([START]) --> A[agent 노드<br/>LLM 추론]:::node
A --> R{도구가 필요한가?}:::router
R -->|예| T[tools 노드<br/>도구 실행]:::tool
T --> A
R -->|아니오| E([END]):::result
agent 노드가 추론하고, "도구가 필요한가?"를 라우터가 판단합니다. 필요하면 tools 노드로 갔다가 다시 agent로 돌아옵니다(사이클). 도구가 더 필요 없으면 END로 갑니다. 1부의 ReAct 루프가 그래프 위에서 그대로 재현됩니다.
그래프 안에서 도구 호출하기
LangGraph는 도구 실행을 위한 사전 제작 노드 ToolNode를 제공합니다. 직접 만든 도구 호출 로직을 대신해 줍니다.
from langgraph.graph import StateGraph, START, END, MessagesState from langgraph.prebuilt import ToolNode from langchain_core.tools import tool from langchain_openai import ChatOpenAI @tool def get_weather(city: str) -> str: """도시의 현재 날씨를 반환한다.""" return f"{city}: 맑음, 24도" llm = ChatOpenAI(model="gpt-4o-mini").bind_tools([get_weather]) def agent(state: MessagesState) -> dict: return {"messages": [llm.invoke(state["messages"])]} def should_continue(state: MessagesState): last = state["messages"][-1] # 모델이 도구를 호출했으면 tools로, 아니면 종료 return "tools" if last.tool_calls else END builder = StateGraph(MessagesState) builder.add_node("agent", agent) builder.add_node("tools", ToolNode([get_weather])) builder.add_edge(START, "agent") builder.add_conditional_edges("agent", should_continue, ["tools", END]) builder.add_edge("tools", "agent") # 사이클: 도구 실행 후 다시 agent로 graph = builder.compile() result = graph.invoke({"messages": [{"role": "user", "content": "서울 날씨?"}]}) print(result["messages"][-1].content)
MessagesState는 메시지 누적용 리듀서가 미리 적용된 상태로, 11장의 Annotated[list, add_messages]를 빌트인으로 제공합니다. should_continue가 모델의 응답에 도구 호출이 있는지 보고 분기합니다.
📌 핵심: "agent → (도구 필요?) → tools → agent" 사이클이 LangGraph식 에이전트 루프입니다. OpenAI SDK가 이 루프를 내부에 감췄다면, LangGraph는 루프를 눈에 보이게 펼쳐 정밀 제어를 가능하게 합니다.
💡 팁: 사이클이 있으면 무한 루프 위험이 있습니다. LangGraph는 recursion_limit으로 최대 반복을 제한합니다. 초과하면 GraphRecursionError가 발생하니, invoke(..., {"recursion_limit": 10})처럼 적절히 설정하세요.
💡 실습 아이디어(강의의 LangGraph 도구·조건부 엣지 대응): 감성 분석 노드를 추가해, 긍정이면 "감사 응답" 노드로, 부정이면 "에스컬레이션" 노드로 분기하는 그래프를 만들어 보세요. 조건부 엣지 하나로 고객 대응 흐름이 갈립니다.
이 장에서 배운 것
- 조건부 엣지는 라우터 함수의 반환값에 따라 다음 노드를 고르는 분기다.
- 라우터는 상태를 읽고 노드 이름 문자열만 반환한다. 계산은 노드, 결정은 라우터.
- 사이클(되돌아가는 엣지)로 재시도·에이전트 루프를 그래프에 구현한다.
ToolNode와MessagesState로 도구 호출 에이전트 루프를 간결하게 만든다.recursion_limit으로 무한 루프를 막는다.
✍️ 확인 문제
- 라우터 함수 안에서 LLM을 호출하면 왜 안 되는가? 그 계산은 어디서 해야 하는가?
- "agent → tools → agent" 구조에서 엣지 하나를 빼면 에이전트 루프가 성립하지 않는다. 어느 엣지인가?
- 사이클이 있는 그래프에서 무한 반복을 방지하는 장치는 무엇인가?
이전 장: 11. LangGraph 입문
다음 장: 13. 체크포인트·메모리·휴먼 인 더 루프