안녕하세요, 저는 HolySheep AI의 기술 튜토리얼 작성자입니다. 오늘은 LangGraph StateGraph를 이용해서 AI 에이전트의 "상태 기계(State Machine)"를 만드는 방법을 처음부터 알려드리겠습니다. 프로그래밍을 막 시작한 분도 충분히 따라올 수 있도록 쉬운 말로 설명할게요.
상태 기계란 무엇인가요?
상상해보세요. 여러분이 카페에서 커피를 주문하면:
- 주문 대기 중 → 주문 접수 → 커피 만들기 → 완성 → 손님에게 전달
이렇게 하나의 상태에서 다른 상태로 순서대로 이동하는 것이 바로 상태 기계예요. AI 에이전트도 마찬가지로 사용자의 요청을 받아서 여러 단계를 거치면서 최종 결과를 만들어냅니다.
왜 LangGraph인가요?
LangGraph는 AI 에이전트가 "생각하고 판단하고行动的" 과정을 코드로 쉽게 만들 수 있게 도와주는 도구입니다. 복잡한 대화 흐름, 조건 분기, 반복 작업 등을 체계적으로 관리할 수 있어요.
1단계: 준비물 설치하기
먼저 필요한 도구를 설치해주세요. 터미널(명령 프롬프트)에서 아래 명령어를 실행하세요:
# LangGraph와 관련 라이브러리 설치
pip install langgraph langchain-core langchain-openai
Python 버전 확인 (3.11 이상 권장)
python --version
2단계: HolySheep AI API 키 설정하기
AI 모델을 사용하려면 API 키가 필요해요. HolySheep AI는 해외 신용카드 없이도 로컬 결제가 가능해서 초보자에게 정말 편리합니다. 지금 가입하시면 무료 크레딧도 받을 수 있어요.
import os
HolySheep AI API 키 설정
HolySheep AI 대시보드에서 받은 키를 아래 따옴표 안에 붙여넣기
os.environ["HOLYSHEEP_API_KEY"] = "YOUR_HOLYSHEEP_API_KEY"
os.environ["HOLYSHEEP_BASE_URL"] = "https://api.holysheep.ai/v1"
LangChain OpenAI 통합에서 HolySheep 사용 설정
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="gpt-4.1",
api_key=os.environ["HOLYSHEEP_API_KEY"],
base_url=os.environ["HOLYSHEEP_BASE_URL"]
)
연결 테스트
print("✅ HolySheep AI 연결 성공!")
response = llm.invoke("안녕하세요! 연결 테스트입니다.")
print(f"AI 응답: {response.content}")
화면 힌트: 위 코드를 실행하면 터미널에 "AI 응답: ..."으로 AI가 대답하는 것이 보일 거예요.
3단계: 가장 간단한 상태 기계 만들기
이제 LangGraph로 상태 기계를 만들어볼게요. 예제를 통해 자연어 처리를 하는 간단한 에이전트를 만들겠습니다.
from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, START, END
===== 1단계: 상태(State) 정의하기 =====
에이전트가 기억해야 할 데이터들의 모양을 정해줍니다
class AgentState(TypedDict):
"""에이전트가 관리할 데이터 구조"""
user_input: str # 사용자가 입력한 내용
sentiment: str # 감정 분석 결과 (positive/negative/neutral)
response: str # 최종 응답
step_count: int # 현재 몇 번째 단계를 진행했는지
===== 2단계: 노드(Node) 함수 만들기 =====
상태 기계에서 "할 일"을 정의합니다
def analyze_sentiment(state: AgentState) -> AgentState:
"""1단계: 사용자의 텍스트에서 감정을 분석합니다"""
user_text = state["user_input"]
# HolySheep AI의 GPT-4.1로 감정 분석 요청
prompt = f"""다음 텍스트의 감정을 positive, negative, neutral 중 하나로만 답변해주세요.
텍스트: "{user_text}"
감정:"""
result = llm.invoke(prompt)
sentiment = result.content.strip().lower()
# 감정 값이 이상할 때 기본값 처리
if sentiment not in ["positive", "negative", "neutral"]:
sentiment = "neutral"
return {"sentiment": sentiment, "step_count": state["step_count"] + 1}
def generate_response(state: AgentState) -> AgentState:
"""2단계: 감정 분석 결과를 바탕으로 적절한 응답을 생성합니다"""
sentiment = state["sentiment"]
# 감정에 따라 다른 스타일로 응답 생성
style_prompt = {
"positive": "밝고 친근한",
"negative": "공감하고 따뜻한",
"neutral": "중립적이고 정보성 있는"
}.get(sentiment, "친절한")
prompt = f"""사용자의 감정이 '{sentiment}'으로 분석되었습니다.
'{style_prompt} 스타일'로 사용자에게 짧고 좋은 응답을 해주세요."""
result = llm.invoke(prompt)
return {"response": result.content, "step_count": state["step_count"] + 1}
===== 3단계: 그래프(상태 기계) 구성하기 =====
builder = StateGraph(AgentState)
노드 추가: "분석기"와 "응답기"를 그래프에 등록
builder.add_node("sentiment_analyzer", analyze_sentiment)
builder.add_node("response_generator", generate_response)
엣지(연결선) 추가: 어떻게 상태가 이동할지 정의
builder.add_edge(START, "sentiment_analyzer") # 시작 → 감정 분석
builder.add_edge("sentiment_analyzer", "response_generator") # 감정 분석 → 응답 생성
builder.add_edge("response_generator", END) # 응답 생성 → 종료
그래프 컴파일
graph = builder.compile()
print("✅ 상태 기계 구성 완료!")
4단계: 상태 기계 실행하기
이제 만든 상태 기계를 실제로 동작시켜볼게요.
# ===== 4단계: 상태 기계 실행 =====
print("=" * 50)
print("🚀 상태 기계 실행 시작!")
print("=" * 50)
초기 상태 설정
initial_state = {
"user_input": "오늘 날씨가 정말 좋아서 기분이 좋네요!",
"sentiment": "", # 아직 분석 전이므로 빈 값
"response": "", # 아직 응답 없음
"step_count": 0 # 시작은 0단계
}
상태 기계 실행
final_state = graph.invoke(initial_state)
===== 결과 확인 =====
print("\n📊 최종 상태:")
print(f" - 사용자 입력: {final_state['user_input']}")
print(f" - 감정 분석: {final_state['sentiment']}")
print(f" - 총 진행 단계: {final_state['step_count']}")
print(f"\n💬 AI 응답:\n{final_state['response']}")
===== 다른 입력으로 테스트 =====
test_inputs = [
"이产品价格实在太高了,无法接受", # 부정적 감정
"오늘 프로젝트 마감이 다가와서 스트레스 받아요", # 부정적
"LangGraph 배워서 AI 에이전트 만드는 중!", # 긍정적
]
print("\n" + "=" * 50)
print("🔄 추가 테스트 실행")
print("=" * 50)
for test_input in test_inputs:
result = graph.invoke({
"user_input": test_input,
"sentiment": "",
"response": "",
"step_count": 0
})
print(f"\n입력: {test_input}")
print(f"감정: {result['sentiment']}")
print(f"응답 미리보기: {result['response'][:50]}...")
5단계: 조건 분기가 있는 복잡한 상태 기계
실전에서는 "만약 ~라면 다른 길로 가라"는 분기가 필요해요. 조건 분기를 추가해볼게요.
from typing import Literal
class SmartAgentState(TypedDict):
"""좀 더 현실적인 에이전트 상태"""
user_input: str
intent: str # 의도 분류 결과
sentiment: str # 감정 분석
needs_human: bool # 사람 도움 필요 여부
response: str
conversation_turns: int
def classify_intent(state: SmartAgentState) -> SmartAgentState:
"""사용자 의도 분류"""
prompt = f"""다음 사용자 입력을 분석해서 의도를 분류해주세요.
가능한 의도: ask_question, make_complaint, request_refund, general_chat
입력: "{state['user_input']}"
의도:"""
result = llm.invoke(prompt)
intent = result.content.strip().lower().replace(" ", "_")
# 유효한 의도인지 확인
valid_intents = ["ask_question", "make_complaint", "request_refund", "general_chat"]
if intent not in valid_intents:
intent = "general_chat"
return {"intent": intent, "conversation_turns": state["conversation_turns"] + 1}
def handle_complaint(state: SmartAgentState) -> SmartAgentState:
"""불만 사항 처리"""
prompt = f"사용자가 불만을 표현했습니다. 공감하고 해결책을 제안해주세요:\n\n{state['user_input']}"
result = llm.invoke(prompt)
return {"response": result.content, "needs_human": False}
def handle_refund(state: SmartAgentState) -> SmartAgentState:
"""환불 요청 처리 - 복잡한 경우 사람에게 전달"""
return {"response": "환불 요청을 받았습니다. 담당자가 확인 후 联系드리겠습니다.", "needs_human": True}
def handle_question(state: SmartAgentState) -> SmartAgentState:
"""일반 질문 답변"""
prompt = f"친절하게 답변해주세요:\n\n{state['user_input']}"
result = llm.invoke(prompt)
return {"response": result.content, "needs_human": False}
def route_by_intent(state: SmartAgentState) -> Literal["handle_complaint", "handle_refund", "handle_question"]:
"""의도에 따라 다음 노드를 결정하는 라우터 함수"""
intent = state["intent"]
if "complaint" in intent:
return "handle_complaint"
elif "refund" in intent:
return "handle_refund"
else:
return "handle_question"
===== 조건 분기 그래프 구성 =====
builder = StateGraph(SmartAgentState)
노드 추가
builder.add_node("classify_intent", classify_intent)
builder.add_node("handle_complaint", handle_complaint)
builder.add_node("handle_refund", handle_refund)
builder.add_node("handle_question", handle_question)
기본 흐름
builder.add_edge(START, "classify_intent")
조건 분기: classify_intent에서 intent 값에 따라 다른 노드로 이동
builder.add_conditional_edges(
"classify_intent",
route_by_intent,
{
"handle_complaint": "handle_complaint",
"handle_refund": "handle_refund",
"handle_question": "handle_question"
}
)
모든 경로는 END로
builder.add_edge("handle_complaint", END)
builder.add_edge("handle_refund", END)
builder.add_edge("handle_question", END)
smart_graph = builder.compile()
===== 테스트 실행 =====
print("=" * 60)
print("🧠 조건 분기 상태 기계 테스트")
print("=" * 60)
test_cases = [
"배송이 2주째 안 와서 정말 화가 났어요!", # 불만
"구매한 상품을 환불하고 싶은데 어떻게 하나요?", # 환불 요청
"BloG 서비스 이용 방법을 알려주세요", # 질문
]
for user_input in test_cases:
result = smart_graph.invoke({
"user_input": user_input,
"intent": "",
"sentiment": "",
"needs_human": False,
"response": "",
"conversation_turns": 0
})
print(f"\n📝 입력: {user_input}")
print(f"🎯 분류된 의도: {result['intent']}")
print(f"👤 사람 연결 필요: {'예' if result['needs_human'] else '아니오'}")
print(f"💬 응답: {result['response']}")
6단계: 그래프 시각화하기
만든 상태 기계를 그림으로 보면 더 이해하기 쉬워요.
# 상태 기계 그래프를 이미지로 저장
try:
# Mermaid形式的文本图生成
diagram = smart_graph.get_graph().draw_ascii()
print("📊 상태 기계 구조:\n")
print(diagram)
# 또는 PNG 이미지로 저장 (선택사항)
# from langchain_core.runnables.graph import Edge
# png_data = smart_graph.get_graph().draw_mermaid_png()
# with open("state_machine.png", "wb") as f:
# f.write(png_data)
# print("\n✅ 'state_machine.png' 파일로 저장됨")
except Exception as e:
print(f"시각화 중 오류 발생: {e}")
print("그래프 구조를 텍스트로 확인하세요")
===== 상태 변경 이력 추적 =====
print("\n" + "=" * 60)
print("📈 상태 변경 과정 추적")
print("=" * 60)
state_updates를 사용하면 각 단계별 상태 변경을 볼 수 있음
for state_update in smart_graph.stream({
"user_input": "계정 비밀번호를 잊어버렸어요",
"intent": "",
"sentiment": "",
"needs_human": False,
"response": "",
"conversation_turns": 0
}):
print(f"\n🔄 상태 업데이트: {list(state_update.keys())}")
if "classify_intent" in state_update:
print(f" → 의도 분류 결과: {state_update['classify_intent']}")
if "handle_question" in state_update:
print(f" → 최종 응답 생성됨")
HolySheep AI 가격 정보 참고
LangGraph 에이전트 만들기를 실습하면서 실제 API 비용이 궁금하실 거예요. HolySheep AI의 최신 가격표입니다:
- GPT-4.1: $8.00 / 1M 토큰 — 가장 강력한 모델
- Claude Sonnet 4: $15.00 / 1M 토큰 — 컨텍스트 이해 강점
- Gemini 2.5 Flash: $2.50 / 1M 토큰 — 가성비 최고
- DeepSeek V3.2: $0.42 / 1M 토큰 — 비용 최적화의 왕
저는 실제로 여러 모델을 비교해봤는데, 감정 분석처럼 간단한 작업에는 Gemini 2.5 Flash로 충분히 좋고, 복잡한 reasoning이 필요하면 Claude Sonnet 4를 사용합니다. HolySheep AI는 단일 API 키로 모든 모델을切り替えて使えるので 정말 편리해요.
자주 발생하는 오류와 해결책
오류 1: API 키 인증 실패
# ❌ 잘못된 예시
os.environ["OPENAI_API_KEY"] = "YOUR_HOLYSHEEP_API_KEY" # 변수명 오류!
✅ 올바른 예시
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 엔드포인트
)
연결 확인
try:
llm.invoke("테스트")
print("✅ 연결 성공")
except Exception as e:
if "401" in str(e) or "authentication" in str(e).lower():
print("❌ API 키를 확인해주세요")
print(" → HolySheep 대시보드: https://www.holysheep.ai/dashboard")
else:
print(f"❌ 기타 오류: {e}")
오류 2: 노드 함수 반환 타입 오류
# ❌ 잘못된 예시 - dict를 바로 반환
def bad_node(state: AgentState):
return {"new_value": "something"} # 전체 state를 반환하지 않음
✅ 올바른 예시 - 업데이트할 필드만 반환 (불변성 유지)
def correct_node(state: AgentState) -> AgentState:
# state는 불변(immutable)이므로 새 dict로 반환
return {"new_value": "something", "step_count": state["step_count"] + 1}
또는 Annotated와Reducer 사용
from typing import TypedDict, Annotated
import operator
class ExtendedState(TypedDict):
messages: Annotated[list, operator.add] # messages에 추가만 함
step_count: int
def append_message(state: ExtendedState) -> ExtendedState:
return {"messages": ["새 메시지입니다"]} # 이전 messages에 자동 추가
오류 3: 조건 분기에서 None 반환 오류
# ❌ 잘못된 예시 - 예상하지 못한 값 처리 없음
def route(state: AgentState) -> str:
category = state["category"]
if category == "A":
return "node_a"
elif category == "B":
return "node_b"
# category가 "C"면? → None 반환 → 에러!
✅ 올바른 예시 - 기본값 항상 포함
from typing import Literal
def safe_route(state: AgentState) -> Literal["node_a", "node_b", "default_node"]:
category = state.get("category", "") # 기본값 처리
if category == "A":
return "node_a"
elif category == "B":
return "node_b"
else:
return "default_node" # 항상 어떤 값을 반환
그래프 구성 시
builder.add_conditional_edges(
"classifier",
safe_route, # 항상 유효한 노드 이름 반환
{
"node_a": "node_a",
"node_b": "node_b",
"default_node": "default_node" # 모든 경우 매핑 필수
}
)
오류 4: 상태 업데이트 누락
# ❌ 잘못된 예시 - 이전 상태값 복사 안 함
def broken_node(state: AgentState) -> AgentState:
# state의 다른 필드들을 명시적으로 포함해야 함
return {"step_count": 99} # user_input, sentiment 등이 사라짐!
✅ 올바른 예시 - 기존 값 유지하면서 일부만 업데이트
def working_node(state: AgentState) -> AgentState:
# 업데이트할 필드만 반환하면 기존 필드는 유지됨
return {
"step_count": state["step_count"] + 1,
"response": generate_response(state)
}
# user_input, sentiment 등은 자동으로 유지됨
실전 팁과 다음 단계
제가 실제로 LangGraph를 사용하면서 얻은 경험담을 공유드릴게요:
- 시작은 간단하게: 처음부터 복잡한 상태 기계를 만들지 마세요. 2-3개 노드부터 시작해서 점진적으로 확장하세요
- 상태 설계가 핵심: AgentState에 어떤 정보를 저장할지 잘 설계하면后半 개발이 수월해집니다
- 메모리 노드 활용: 대화 기록을 유지하려면 messages 필드를 추가하고 Annotated[list, operator.add]를 사용하세요
- 에러 처리 노드: 각 분기에서 예외 상황发生时 어떻게 대응할지 별도 노드로 설계하세요
다음에는 LangGraph Memory로 대화 기록을 유지하는 방법과 Tool Integration으로 외부 API를 호출하는 방법을 다루겠습니다.
정리
오늘 배운 내용을 정리하면:
- ✅ 상태 기계란 "상태"를 관리하면서 "전환"을 체계적으로 하는 패턴
- ✅ LangGraph StateGraph로 노드와 엣지를 정의하여 그래프 구성
- ✅ add_conditional_edges로 분기 로직 구현
- ✅ HolySheep AI의 통합 엔드포인트로 다양한 모델 사용
LangGraph 상태 기계는 복잡한 AI 워크플로우를 코드로 표현하는 강력한 방법입니다. 기본 개념을 이해하셨다면, 자신의 프로젝트에 맞게 자유롭게 확장해보세요!
HolySheep AI는 글로벌 AI API 게이트웨이로, 다양한 모델을 단일 API 키로 통합 관리할 수 있어 개발자들이 모델 교체나 비용 최적화에費い時間を节省할 수 있어요. 로컬 결제도 지원해서 해외 신용카드 없이 바로 시작할 수 있습니다.
👉 HolySheep AI 가입하고 무료 크레딧 받기