자바 개발자의 AI 입문기 (5편) - LangGraph, 상태 기반 멀티 에이전트
TL;DR
- LangGraph: 상태 기반 AI 워크플로우 프레임워크
- 핵 심 개념: 노드(작업) + 엣지(연결) + 상태(데이터)
- 사용 사례: 다단계 처리, 조건부 분기, 도구 사용 에이전트
- 자바 비유: Spring Batch의 Job/Step 구조와 유사
- 한계: 노드 수 증가에 따라 디버깅 복잡도 상승, 단순 선형 흐름에서는 체이닝이 더 적합
이전 편 요약
4편에서는 다음을 다뤘습니다:
- RAG 파이프라인 완성
- 문서 로딩 + 청킹 + 인덱싱
- Knowledge Base 검색 챗봇
이번 편에서는 LangGraph를 배웁니다. 시리즈의 마지막 편이에요.
LangGraph가 뭔가요?
한 줄 요약: AI 호출을 상태 머신처럼 관리하는 프레임워크
4편에서 만든 RAG 챗봇은 단순했어요:
질문 → 검색 → 답변근데 실제 서비스는 더 복잡해요:
질문 분석
→ 카테고리 분류 (인사? 기술? 일반?)
→ 해당 DB에서 검색
→ 답변 생성
→ 답변 검증 (이상하면 다시 생성)
→ 최종 응답이런 복잡한 흐름을 깔끔하게 관리하려면 LangGraph가 필요합니다.
자바 개발자를 위한 비유
| LangGraph | Java 비유 |
|---|---|
| Graph | Spring Batch Job |
| Node | Step, Tasklet |
| Edge | Flow, Transition |
| State | ExecutionContext |
| Conditional Edge | FlowExecutionStatus 분기 |
Spring Batch 써보셨다면 익숙할 거예요. Job이 여러 Step으로 구성되고, Step 결과에 따라 다음 Step이 결정되는 구조요.
LangGraph 설치
pip install langgraph핵심 개념: 노드, 엣지, 상태
노드 (Node)
“하나의 작업 단위”입니다.
def analyze_question(state):
"""질문을 분석하는 노드"""
question = state["question"]
# 분석 로직...
return {"category": "hr", "question": question}엣지 (Edge)
“노드 간 연결”입니다.
# A 노드 실행 후 B 노드로 이동
graph.add_edge("A", "B")
# 조건부 이동
graph.add_conditional_edges("A", route_function, {
"hr": "hr_search",
"tech": "tech_search"
})상태 (State)
“노드 간 공유 데이터”입니다.
from typing import TypedDict
class State(TypedDict):
question: str
category: str
search_results: list
answer: str실습: 카테고리별 검색 챗봇
간단한 예제로 시작해봅시다.
요구사항:
- 질문을 분석해서 카테고리 분류 (인사 / 기술 / 기타)
- 카테고리에 맞는 검색 수행
- 검색 결과 기반 답변 생성
1단계: 상태 정의
from typing import TypedDict, Literal
class ChatState(TypedDict):
question: str
category: Literal["hr", "tech", "general"]
context: str
answer: str2단계: 노드 함수 정의
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from dotenv import load_dotenv
load_dotenv()
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
def classify_question(state: ChatState) -> ChatState:
"""질문을 카테고리로 분류"""
prompt = ChatPromptTemplate.from_messages([
("system", """질문을 분류하세요.
- hr: 휴가, 급여, 복지, 인사 관련
- tech: 기술, 개발, 도구 관련
- general: 그 외
반드시 hr, tech, general 중 하나만 출력하세요."""),
("user", "{question}")
])
chain = prompt | llm | StrOutputParser()
category = chain.invoke({"question": state["question"]}).strip().lower()
# 유효한 카테고리인지 확인
if category not in ["hr", "tech", "general"]:
category = "general"
return {"category": category}
def search_hr(state: ChatState) -> ChatState:
"""인사 관련 검색 (시뮬레이션)"""
# 실제로는 Vector DB 검색
hr_docs = {
"휴가": "연차 15일, 3년차부터 20일",
"급여": "매월 25일 지급",
"복지": "점심 식대 지원, 자기계발비 연 100만원"
}
question = state["question"]
context = next((v for k, v in hr_docs.items() if k in question),
"관련 인사 정책을 찾을 수 없습니다.")
return {"context": context}
def search_tech(state: ChatState) -> ChatState:
"""기술 관련 검색 (시뮬레이션)"""
tech_docs = {
"배포": "GitHub Actions + ArgoCD 사용",
"모니터링": "Datadog 대시보드 참고",
"코드리뷰": "PR 올리면 2명 이상 승인 필요"
}
question = state["question"]
context = next((v for k, v in tech_docs.items() if k in question),
"관련 기술 문서를 찾을 수 없습니다.")
return {"context": context}
def search_general(state: ChatState) -> ChatState:
"""일반 검색"""
return {"context": "일반 문의는 총무팀에 연락해주세요."}
def generate_answer(state: ChatState) -> ChatState:
"""최종 답변 생성"""
prompt = ChatPromptTemplate.from_messages([
("system", """참고 정보를 바탕으로 친절하게 답변하세요.
참고 정보:
{context}"""),
("user", "{question}")
])
chain = prompt | llm | StrOutputParser()
answer = chain.invoke({
"context": state["context"],
"question": state["question"]
})
return {"answer": answer}3단계: 그래프 구성
from langgraph.graph import StateGraph, END
def route_by_category(state: ChatState) -> str:
"""카테고리에 따라 다음 노드 결정"""
category = state.get("category", "general")
return category
# 그래프 생성
workflow = StateGraph(ChatState)
# 노드 추가
workflow.add_node("classify", classify_question)
workflow.add_node("hr", search_hr)
workflow.add_node("tech", search_tech)
workflow.add_node("general", search_general)
workflow.add_node("answer", generate_answer)
# 시작점 설정
workflow.set_entry_point("classify")
# 조건부 엣지: 분류 결과에 따라 검색 노드 선택
workflow.add_conditional_edges(
"classify",
route_by_category,
{
"hr": "hr",
"tech": "tech",
"general": "general"
}
)
# 검색 노드들 → 답변 노드로
workflow.add_edge("hr", "answer")
workflow.add_edge("tech", "answer")
workflow.add_edge("general", "answer")
# 답변 노드 → 종료
workflow.add_edge("answer", END)
# 컴파일
app = workflow.compile()4단계: 실행
# 테스트
questions = [
"연차 며칠 받아요?",
"배포는 어떻게 하나요?",
"회사 주소가 어디예요?"
]
for q in questions:
result = app.invoke({"question": q})
print(f"Q: {q}")
print(f"카테고리: {result['category']}")
print(f"A: {result['answer']}\n")결과:
Q: 연차 며칠 받아요?
카테고리: hr
A: 연차 휴가는 입사 1년 후 15일이 부여되고, 3년차부터는 20일로 늘어납니다.
Q: 배포는 어떻게 하나요?
카테고리: tech
A: 배포는 GitHub Actions와 ArgoCD를 사용합니다. 자세한 내용은 기술 문서를 참고해주세요.
Q: 회사 주소가 어디예요?
카테고리: general
A: 일반 문의는 총무팀에 연락해주세요.질문 종류에 따라 다른 경로를 타고, 적절한 답변이 생성됐어요.
그래프 시각화
LangGraph는 그래프를 시각화할 수 있어요.
from IPython.display import Image, display
# Mermaid 다이어그램 생성
print(app.get_graph().draw_mermaid())출력:
graph TD
__start__ --> classify
classify --> hr
classify --> tech
classify --> general
hr --> answer
tech --> answer
general --> answer
answer --> __end__Spring Batch의 Job 흐름도와 비슷하죠?
실전 활용: 재시도 로직 추가
답변이 이상하면 다시 생성하는 로직을 추가해봅시다.
def validate_answer(state: ChatState) -> str:
"""답변이 유효한지 검증"""
answer = state.get("answer", "")
# 답변이 너무 짧거나 "모르겠다"로 끝나면 재시도
if len(answer) < 20 or "모르겠" in answer:
return "retry"
return "done"
# 재시도 노드
def regenerate_answer(state: ChatState) -> ChatState:
"""더 구체적인 프롬프트로 재생성"""
# ... 재생성 로직
return {"answer": "재생성된 답변..."}
# 조건부 엣지 추가
workflow.add_conditional_edges(
"answer",
validate_answer,
{
"retry": "regenerate",
"done": END
}
)이런 식으로 복잡한 워크플로우를 깔끔하게 표현할 수 있어요.
LangGraph vs LangChain 체이닝
| 상황 | 추천 |
|---|---|
| 단순한 순차 처리 | LangChain 체이닝 (| 연산자) |
| 조건부 분기 필요 | LangGraph |
| 반복/재시도 로직 | LangGraph |
| 여러 에이전트 협업 | LangGraph |
| 상태 관리 필요 | LangGraph |
4편에서 만든 RAG 챗봇은 체이닝으로 충분했어요. 근데 “카테고리별 분기”, “답변 검증”, “재시도” 같은 게 필요하면 LangGraph가 적합합니다.
시리즈 마무리
5편에 걸쳐 AI 개발의 기초를 다뤘습니다:
| 편 | 주제 | 배운 것 |
|---|---|---|
| 1편 | 환경 세팅 | Python, Cursor, OpenAI API |
| 2편 | LangChain | 프롬프트 템플릿, 체이닝 |
| 3편 | RAG 기초 | Embedding, Vector DB |
| 4편 | RAG 실전 | 문서 로딩, 청킹, 챗봇 |
| 5편 | LangGraph | 상태 기반 워크플로우 |
5편의 내용을 따라왔다면 아래 것들도 충분히 도전해볼 만해요:
- 사내 문서 검색 챗봇
- 코드 리뷰 자동화 도구
- FAQ 자동 응답 시스템
- 복잡한 AI 워크플로우
다음 단계는?
이 시리즈는 “입문”이에요. 더 깊게 가려면:
추천 학습 경로
- RAG 심화: Hybrid Search, Re-ranking, Query Expansion
- Agent 심화: Tool Use, Function Calling, 멀티 에이전트
- 프로덕션화: 캐싱, 로깅, 모니터링, 비용 최적화
- Fine-tuning: 도메인 특화 모델 학습 (Level 5)
추천 자료
- LangChain 공식 튜토리얼
- “Building LLM Apps” (온라인 강의)
- 한국어 AI 커뮤니티 (디스코드, 슬랙 등)
마치며
솔직히 처음엔 “이거 자바로 못하나?”라는 생각이 컸어요.
근데 Python으로 해보니까 생각보다 쉬웠습니다. Cursor IDE 덕분에 문법 몰라도 바이브코딩으로 어떻게든 되더라고요.
자바 개발자라고 AI 개발 못할 거 없어요. 오히려 백엔드 경험이 있으니까:
- 시스템 설계 감각 있음
- API 구조 이해 빠름
- 프로덕션 배포 경험 있음
이런 게 다 도움됩니다.
이 시리즈가 AI 입문의 시작점이 됐으면 좋겠습니다.
실습 체크리스트
따라하고 나서 아래 항목들을 점검해보세요:
-
langgraph설치 확인 (pip install langgraph) -
classify노드가 hr/tech/general 중 하나만 반환하는지 확인 - 미분류 카테고리도
general로 fallback 처리됐는지 확인 -
workflow.compile()후app.invoke()정상 실행 확인 -
app.get_graph().draw_mermaid()출력으로 그래프 구조 확인
참고:
LangGraph 공식 문서: https://langchain-ai.github.io/langgraph/
LangGraph 튜토리얼: https://python.langchain.com/docs/langgraph
Agent 개념: https://python.langchain.com/docs/concepts/agents
