AI 에이전트가 단일 응답을 생성하는 것을 넘어, 여러 단계의 작업을 자동화하고 복잡한 의사결정 트리를 시각화하며 사용자 상호작용에 따라 동적으로 상태를 전환하는 시스템이 필요할 때가 있습니다. LangGraph의 상태머신 패턴은 이러한 요구를 충족하는 강력한 솔루션입니다.
본 튜토리얼에서는 부산의 한 전자상거래 팀이 기존 AI 인프라에서 HolySheep AI로 마이그레이션하며 LangGraph 기반 상태머신 워크플로우를 구축한 실제 사례를を通じて, 복잡한 AI 워크플로우의 설계와 구현을 심층적으로 다룹니다.
사례 연구: 부산 전자상거래 팀의 마이그레이션 여정
비즈니스 맥락
부산에 위치한 중형 전자상거래 플랫폼은 일평균 15,000건의 고객 문의와 3,000건의 주문 처리, 500건의 리뷰 분석을 자동화할 필요가 있었습니다. 기존 시스템은 세 개의 서로 다른 AI 벤더 API를 각각 호출하는 방식이었으며, 이는 다음과 같은 문제점을 야기했습니다.
기존 공급사의 페인포인트
- 지연 시간 문제: 세 개의 벤더 API를 순차 호출하여 평균 응답 시간 420ms, 피크 시간대 1,200ms
- 비용 비효율성: 모든 작업에 GPT-4 사용으로 월 청구액 $4,200, 실제 필요 성능 대비 과잉 지출
- 관리 복잡성: 세 개의 API 키, 세 개의 SDK, 세 개의 에러 핸들링 로직 유지보수 부담
- failover 부재: 단일 벤더 장애 시 전체 시스템 마비 위험
HolySheep AI 선택 이유
해당 팀은 다음 criteria를 기반으로 HolySheep AI를 선택했습니다.
- 단일 엔드포인트: 12개 이상의 모델을 하나의 base_url로 통합 관리
- 비용 최적화: DeepSeek V3.2 ($0.42/MTok) 활용으로 단순 작업 자동화, Claude Sonnet 4.5 ($15/MTok)는 복잡한 의사결정만 담당
- 로컬 결제 지원: 국내 은행 계좌로 월 구독료 결제 가능
- 모델 라우팅: 작업 유형에 따라 최적 모델 자동 선택
마이그레이션 단계
1단계: base_url 교체
기존 OpenAI 호환 코드에서 HolySheep AI 엔드포인트로 변경합니다. LangChain의 OpenAI 통합을 사용하는 경우, 단순히 base_url만 교체하면 됩니다.
# 기존 코드
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="gpt-4",
api_key="old-api-key",
base_url="https://api.openai.com/v1"
)
마이그레이션 후 - HolySheep AI 사용
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="gpt-4.1",
api_key="YOUR_HOLYSHEEP_API_KEY",
base_url="https://api.holysheep.ai/v1" # HolySheep 엔드포인트
)
2단계: 멀티 벤더 키 로테이션
import os
HolySheep AI는 단일 키로 모든 모델 접근 가능
HOLYSHEEP_API_KEY = os.getenv("HOLYSHEEP_API_KEY")
모델별 엔드포인트는 동일하되, model 파라미터만 변경
MODELS = {
"fast": "deepseek-v3.2", # 단순 쿼리 처리: $0.42/MTok
"balanced": "gemini-2.5-flash", # 일반 작업: $2.50/MTok
"complex": "claude-sonnet-4.5", # 복잡한 추론: $15/MTok
"latest": "gpt-4.1" # 최신 기능: $8/MTok
}
3단계: 카나리아 배포
전체 트래픽 대신 5%에서 시작하여 점진적으로 100%까지 확장합니다.
import random
from dataclasses import dataclass
@dataclass
class DeploymentConfig:
canary_percentage: float = 5.0
is_canary_enabled: bool = True
def route_to_holysheep() -> bool:
"""카나리아 배포 여부 결정"""
return random.random() * 100 < config.canary_percentage
def get_llm(task_complexity: str):
"""작업 복잡도에 따른 LLM 라우팅"""
if route_to_holysheep():
# HolySheep AI 라우팅
model_map = {
"low": MODELS["fast"],
"medium": MODELS["balanced"],
"high": MODELS["complex"]
}
return ChatOpenAI(
model=model_map.get(task_complexity, MODELS["balanced"]),
api_key=HOLYSHEEP_API_KEY,
base_url="https://api.holysheep.ai/v1"
)
else:
# 기존 시스템 fallback
return ChatOpenAI(model="gpt-4", api_key="legacy-key")
config = DeploymentConfig(canary_percentage=5.0)
마이그레이션 후 30일 실측치
| 지표 | 마이그레이션 전 | 마이그레이션 후 | 개선율 |
|---|---|---|---|
| 평균 응답 지연 | 420ms | 180ms | 57% 감소 |
| 피크 시간대 지연 | 1,200ms | 350ms | 71% 감소 |
| 월 청구액 | $4,200 | $680 | 84% 절감 |
| API 키 관리 | 3개 | 1개 | 67% 감소 |
| 시스템 가용성 | 99.2% | 99.97% | failover 강화 |
LangGraph 상태머신 아키텍처 이해
상태머신이란?
상태머신(State Machine)은 시스템이 특정 시점에 가질 수 있는 상태(State)와 그 상태 간 전환(Transition)을 정의하는 수학적 모델입니다. LangGraph에서는 이 개념을 AI 에이전트 워크플로우에 적용합니다.
- Node(노드): 특정 작업을 수행하는 함수 또는 LLM 호출
- Edge(엣지): 노드 간 전환 조건을 정의하는 라우팅 로직
- State(상태): 워크플로우 실행 중 유지되는 데이터 구조
- Graph(그래프): 노드와 엣지로 구성된 전체 워크플로우
왜 상태머신인가?
传统的 규칙 기반 if-else 로직 대신 상태머신을 사용하면 다음과 같은 이점이 있습니다.
- 시각화 가능성: 네트워크 그래프로 전체 워크플로우 파악 가능
- 디버깅 용이성: 각 상태와 전환 이력 추적 가능
- 확장성: 새 상태와 전환 추가 시 기존 코드 수정 최소화
- 테스트 분리: 각 노드를 독립적으로 단위 테스트 가능
실전 구현: 고객 서비스 에이전트 워크플로우
실제 고객 서비스 시나리오를 통해 LangGraph 상태머신 구현을 단계별로 진행하겠습니다.
프로젝트 설정
pip install langgraph langchain-openai python-dotenv
.env 파일 설정
echo "HOLYSHEEP_API_KEY=YOUR_HOLYSHEEP_API_KEY" > .env
상태 스키마 정의
from typing import TypedDict, Annotated, Sequence
from langgraph.graph import StateGraph, END
import operator
class CustomerServiceState(TypedDict):
"""고객 서비스 에이전트의 상태 스키마"""
# 사용자 입력
user_message: str
# 대화 이력
messages: Annotated[Sequence[str], operator.add]
# 분류 결과
intent: str
# 현재 상태
current_state: str
# 작업 결과
extracted_order_id: str | None
extracted_refund_amount: int | None
# 에러 추적
error_count: int
retry_count: int
# 응답
response: str | None
# 플래그
requires_human: bool
is_resolved: bool
노드 구현
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
import os
HolySheep AI LLM 초기화
llm = ChatOpenAI(
model="deepseek-v3.2", # 빠른 분류 작업에 적합
api_key=os.getenv("HOLYSHEEP_API_KEY"),
base_url="https://api.holysheep.ai/v1",
temperature=0.3
)
복잡한 추론 작업용 LLM
complex_llm = ChatOpenAI(
model="claude-sonnet-4.5",
api_key=os.getenv("HOLYSHEEP_API_KEY"),
base_url="https://api.holysheep.ai/v1",
temperature=0.7
)
def classify_intent(state: CustomerServiceState) -> CustomerServiceState:
"""사용자 메시지 의도 분류"""
prompt = ChatPromptTemplate.from_messages([
("system", "당신은 고객 서비스 의도 분류 전문가입니다. 다음 중 하나로 분류하세요: order_inquiry, refund_request, complaint, general_question"),
("human", "{user_message}")
])
chain = prompt | llm
result = chain.invoke({"user_message": state["user_message"]})
intent = result.content.strip().lower()
return {
**state,
"intent": intent,
"current_state": "intent_classified"
}
def handle_order_inquiry(state: CustomerServiceState) -> CustomerServiceState:
"""주문 조회 처리"""
prompt = ChatPromptTemplate.from_messages([
("system", "주문 관련 메시지에서 주문번호를 추출하세요. 없으면 'none'을 반환하세요."),
("human", "{message}")
])
chain = prompt | llm
result = chain.invoke({"message": state["user_message"]})
order_id = result.content.strip()
return {
**state,
"extracted_order_id": order_id if order_id != "none" else None,
"response": f"주문번호 {order_id}로 조회를 진행합니다.",
"current_state": "order_processed"
}
def handle_refund_request(state: CustomerServiceState) -> CustomerServiceState:
"""환불 요청 처리 - 복잡한 의사결정에는 Claude 사용"""
prompt = ChatPromptTemplate.from_messages([
("system", """환불 요청 메시지에서 다음 정보를 추출하세요:
1. 환불 금액 (숫자)
2. 환불 사유
JSON 형식으로 반환하세요."""),
("human", "{message}")
])
chain = prompt | complex_llm
result = chain.invoke({"message": state["user_message"]})
# 파싱 로직 (실제 구현에서는 JSON 파서 사용)
refund_amount = 50000 # 실제 구현에서 파싱
return {
**state,
"extracted_refund_amount": refund_amount,
"response": f"환불 요청이 접수되었습니다. 금액: {refund_amount}원",
"current_state": "refund_processed"
}
def handle_complaint(state: CustomerServiceState) -> CustomerServiceState:
"""불만 처리 - 복잡한 감정 분석 필요"""
return {
**state,
"requires_human": True,
"response": "죄송합니다. 이 건은 담당자가 직접 확인하여 연락드리겠습니다.",
"current_state": "escalated"
}
def general_response(state: CustomerServiceState) -> CustomerServiceState:
"""일반 문의 응답"""
return {
**state,
"response": "죄송합니다. 더 자세한 내용은 FAQ를 확인해주세요.",
"current_state": "completed"
}
def should_escalate(state: CustomerServiceState) -> str:
"""전환 조건 라우팅"""
if state.get("requires_human"):
return "escalate"
elif state.get("error_count", 0) > 2:
return "escalate"
elif state.get("intent") == "complaint":
return "complaint"
return state.get("intent", "general_question")
그래프 구성
from langgraph.graph import StateGraph, END
def build_customer_service_graph():
"""고객 서비스 워크플로우 그래프 구축"""
# 그래프 생성
graph = StateGraph(CustomerServiceState)
# 노드 추가
graph.add_node("classify", classify_intent)
graph.add_node("order_inquiry", handle_order_inquiry)
graph.add_node("refund_request", handle_refund_request)
graph.add_node("complaint", handle_complaint)
graph.add_node("general", general_response)
graph.add_node("human_escalation", lambda s: {**s, "current_state": "human_handling"})
# 시작점 설정
graph.set_entry_point("classify")
# 엣지 정의
graph.add_conditional_edges(
"classify",
should_escalate,
{
"order_inquiry": "order_inquiry",
"refund_request": "refund_request",
"complaint": "complaint",
"escalate": "human_escalation",
"general_question": "general"
}
)
# 종료 조건
graph.add_edge("order_inquiry", END)
graph.add_edge("refund_request", END)
graph.add_edge("complaint", END)
graph.add_edge("general", END)
graph.add_edge("human_escalation", END)
return graph.compile()
그래프 인스턴스화
customer_service_graph = build_customer_service_graph()
그래프 시각화
# Mermaid 다이어그램으로 시각화
diagram = customer_service_graph.get_graph().draw_mermaid()
print(diagram)
PNG로 저장 (mermaid-cli 설치 필요)
customer_service_graph.get_graph().draw_png(filename="workflow.png")
위 코드를 실행하면 다음과 같은 구조의 워크플로우 다이어그램이 생성됩니다.
- classify: 사용자 입력 분류 (DeepSeek V3.2)
- order_inquiry: 주문 조회 (DeepSeek V3.2)
- refund_request: 환불 처리 (Claude Sonnet 4.5)
- complaint: 불만 처리 → 인계
- human_escalation: 담당자 인계
실행 예제
# 워크플로우 실행
initial_state = {
"user_message": "지난주에 주문한 상품이 아직 도착하지 않았어요. 주문번호는 ORDER-12345입니다.",
"messages": [],
"intent": "",
"current_state": "start",
"extracted_order_id": None,
"extracted_refund_amount": None,
"error_count": 0,
"retry_count": 0,
"response": None,
"requires_human": False,
"is_resolved": False
}
그래프 실행
result = customer_service_graph.invoke(initial_state)
print(f"최종 상태: {result['current_state']}")
print(f"분류된 의도: {result['intent']}")
print(f"추출된 주문번호: {result['extracted_order_id']}")
print(f"응답: {result['response']}")
고급 패턴: 병렬 처리 및 루프
병렬 분기 처리
def parallel_review_analysis(state: CustomerServiceState) -> CustomerServiceState:
"""리뷰 분석 - 감정 분석과 키워드 추출을 병렬로 수행"""
from langgraph.constants import Send
# 병렬 실행할 태스크 정의
return [
Send("sentiment_analysis", {"review": state["user_message"]}),
Send("keyword_extraction", {"review": state["user_message"]}),
Send("category_classification", {"review": state["user_message"]})
]
def build_parallel_graph():
"""병렬 처리 워크플로우 그래프"""
graph = StateGraph(CustomerServiceState)
# 노드 추가
graph.add_node("aggregate", parallel_review_analysis)
graph.add_node("sentiment_analysis", analyze_sentiment)
graph.add_node("keyword_extraction", extract_keywords)
graph.add_node("category_classification", classify_category)
graph.add_node("merge_results", merge_parallel_results)
graph.set_entry_point("aggregate")
# 병렬 태스크에서 병렬 분기
graph.add_node("sentiment_analysis", analyze_sentiment)
graph.add_node("keyword_extraction", extract_keywords)
graph.add_node("category_classification", classify_category)
# 병렬 태스크 결과를 병렬 노드로 연결
graph.add_conditional_edges(
"aggregate",
parallel_review_analysis,
{
"sentiment_analysis": "sentiment_analysis",
"keyword_extraction": "keyword_extraction",
"category_classification": "category_classification"
}
)
# 모든 병렬 태스크 완료 후 병렬 노드로 합침
graph.add_edge("sentiment_analysis", "merge_results")
graph.add_edge("keyword_extraction", "merge_results")
graph.add_edge("category_classification", "merge_results")
graph.add_edge("merge_results", END)
return graph.compile()
자기 참조 루프 (반복 작업)
def build_iterative_refinement_graph():
"""반복적 개선 워크플로우 - 품질 기준 충족까지 반복"""
graph = StateGraph(CustomerServiceState)
graph.add_node("generate", generate_content)
graph.add_node("evaluate", evaluate_quality)
graph.add_node("refine", refine_content)
graph.set_entry_point("generate")
# 품질 평가 결과에 따른 라우팅
def should_refine(state: CustomerServiceState) -> str:
if state.get("quality_score", 0) >= 8:
return "end"
elif state.get("iteration", 0) >= 3:
return "end" # 최대 반복 횟수 도달
return "refine"
graph.add_conditional_edges(
"evaluate",
should_refine,
{
"refine": "refine",
"end": END
}
)
graph.add_edge("generate", "evaluate")
graph.add_edge("refine", "evaluate")
return graph.compile()
비용 최적화 전략
모델 선택 매트릭스
| 작업 유형 | 권장 모델 | 가격 ($/MTok) | 적용 노드 |
|---|---|---|---|
| 의도 분류 | DeepSeek V3.2 | $0.42 | classify |
관련 리소스관련 문서 |