Skip to content

자바 개발자의 AI 입문기 (5편) - LangGraph, 상태 기반 멀티 에이전트





TL;DR

  • LangGraph: 상태 기반 AI 워크플로우 프레임워크
  • 핵심 개념: 노드(작업) + 엣지(연결) + 상태(데이터)
  • 사용 사례: 다단계 처리, 조건부 분기, 도구 사용 에이전트
  • 자바 비유: Spring Batch의 Job/Step 구조와 유사
  • 한계: 노드 수 증가에 따라 디버깅 복잡도 상승, 단순 선형 흐름에서는 체이닝이 더 적합




이전 편 요약

4편에서는 다음을 다뤘습니다:

  • RAG 파이프라인 완성
  • 문서 로딩 + 청킹 + 인덱싱
  • Knowledge Base 검색 챗봇

이번 편에서는 LangGraph를 배웁니다. 시리즈의 마지막 편이에요.





LangGraph가 뭔가요?

한 줄 요약: AI 호출을 상태 머신처럼 관리하는 프레임워크

4편에서 만든 RAG 챗봇은 단순했어요:

질문 → 검색 → 답변

근데 실제 서비스는 더 복잡해요:

질문 분석 
  → 카테고리 분류 (인사? 기술? 일반?)
  → 해당 DB에서 검색
  → 답변 생성
  → 답변 검증 (이상하면 다시 생성)
  → 최종 응답

이런 복잡한 흐름을 깔끔하게 관리하려면 LangGraph가 필요합니다.



자바 개발자를 위한 비유

LangGraphJava 비유
GraphSpring Batch Job
NodeStep, Tasklet
EdgeFlow, Transition
StateExecutionContext
Conditional EdgeFlowExecutionStatus 분기

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. 질문을 분석해서 카테고리 분류 (인사 / 기술 / 기타)
  2. 카테고리에 맞는 검색 수행
  3. 검색 결과 기반 답변 생성

1단계: 상태 정의

from typing import TypedDict, Literal

class ChatState(TypedDict):
    question: str
    category: Literal["hr", "tech", "general"]
    context: str
    answer: str

2단계: 노드 함수 정의

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 워크플로우




다음 단계는?

이 시리즈는 “입문”이에요. 더 깊게 가려면:

추천 학습 경로

  1. RAG 심화: Hybrid Search, Re-ranking, Query Expansion
  2. Agent 심화: Tool Use, Function Calling, 멀티 에이전트
  3. 프로덕션화: 캐싱, 로깅, 모니터링, 비용 최적화
  4. 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




읽어주셔서 감사합니다.🖐


Ramsbaby
Written byRamsbaby
이 블로그는 직접 개발/운영하는 블로그이므로 당신을 불쾌하게 만드는 불필요한 광고가 없습니다.

#My Github#소개 페이지#Blog OpenSource Github#Blog OpenSource Demo Site