Agentic AI 구축/Agentic AI 아키텍처

State Machine 기반의 LangGraph 아키텍처

gksyb4235 2025. 11. 19. 13:47

LangGraph - State Machine


 

LangGraph는 LangChain에서 Blackbox의 Chain 형태로 표현된 Agent Workflow를 Graph와 State Machine으로 모델링한다.

핵심 구조를 정리하면 아래와 같다.

 

  • State: 그래프 전체의 현재 상태를 담은 데이터 구조 (예: 대화 히스토리, 중간 결과, 선택된 툴, 에러 정보, 등)
  • Node(노드): 실제 일을 하는 함수들 (LLM 호출, MCP Tool 호출, 룰 기반 로직 등)
  • Edge(엣지): “어떤 노드가 끝난 뒤, 다음에 어떤 노드가 실행될지”를 정하는 전이(transition) 규칙

 

이렇게 State Machine 구조를 도입함으로써 LangGraph는 LangChain에 비해 다음의 이점을 누릴 수가 있게 되었다.

 

5-1. 제어 흐름이 명시적이다

  • LangChain Agent:
    • “Tool 선택/순서를 LLM이 프롬프트 안에서 알아서 결정”
    • 제어 흐름이 LLM의 내적 추론에 묻혀 있음 → opaque
  • LangGraph:
    • 가능한 노드/경로가 그래프(상태머신) 구조로 정해져 있음
    • LLM은 필요할 때 Node 안에서 쓰일 뿐, 전체 워크플로우는 코드/그래프로 눈으로 보고 관리 가능

 

5-2. 복잡한 정책/제약을 “전이 규칙”으로 강제 가능

  • 예:
    • AUTH 노드가 성공해야만 CALL_API 노드로 전이
    • 에러 플래그 state["error"]가 True면, 항상 RECOVER 노드로 전이
    • 특정 툴은 하루 1회만 허용 → Edge에서 rate-limit 체크 후 전이 제어

이걸 프롬프트가 아니라, router 함수 + State 필드 + 그래프 구조로 강제하기 때문에 디버깅/테스트가 용이하고, “이 경로는 절대 안 일어나게 해라” 같은 걸 정형적으로 보장할 수 있다.

 

 

5-3. Multi-agent / Tool 그룹화를 상태머신 위에 자연스럽게 얹기 좋음

 

위의 그림처럼 LangGraph 팀이 공식 블로그에서 이야기하는 multi-agent 구조도 사실 “상태머신”의 다른 표현이다.

  • 각 Agent = 하나의 Node (또는 서브그래프)
  • Router = 상태 기반 전이함수
  • Shared Scratchpad / 개별 Scratchpad = State 필드 설계 방식

그래서:

  • Agent A → 작업 → State 업데이트
  • Router가 State 보고 “이제 Agent B로 넘어가자” 전이
  • 이런 식으로 multi-agent workflow = 상태머신으로 표현된다.

 

 

State의 구성


 

LangGraph가 State Machine 구조라는 것은 곧, 노드들 간의 정보 전달이 State 구조로 이루어진다는 뜻이다.

이때 각 노드는 Dictionary(key와 value 상) 형태의 State에 자신이 맡은 부분을 채워넣는 식으로 동작한다.

따라서 LangGraph의 State 정의는 다음과 같은 형태를 띤다.

 

from typing import Annotated, TypedDict

# 가장 단순한 GraphState 예시
class GraphState(TypedDict):
    question: Annotated[str, "UserQuestion"]      # 사용자의 질문
    context: Annotated[str, "RetrievedContext"]   # 검색된 정보
    answer: Annotated[str, "FinalAnswer"]         # 최종 답변

 

이 구조에서 User가 "What is LangGraph?"라는 메시지를 던졌다고 하자, 이게 우선 State의 Question에 저장된다.

state = GraphState(question="What is LangGraph?",  context="",  answer="")

Key Value
question What is LangGraph?
context (empty)
answer (empty)

 

 

이 State는 검색 노드에 들어가고, 검색 노드는 다음의 Retrieved Context를 추가한다.

state["context"] = "LangGraph is a state-machine framework for LLM orchestration."

Key Value
question What is LangGraph?
context LangGraph is a state-machine framework for LLM orchestration.
answer (empty)

 

 

현재 State에 Question과 Retrieved Context가 채워진 상태인데,

최종적으로 답변 노드가 이 State를 받고 최종 answer를 생성한다.

state["answer"] = "LangGraph is a framework that lets you build LLM agents using a deterministic state-machine architecture."

Key Value
question What is LangGraph?
context LangGraph is a state-machine framework for LLM orchestration.
answer LangGraph is a framework that lets you build LLM agents using a deterministic state-machine architecture.

 

 

 

Node의 구성


그럼 위 예시에서 question에 대한 문서를 검색하고, context를 반환하는 Node는 다음과 같이 구성될 수 있다.

def retrieve_document(state: GraphState) -> GraphState:
    retrieved_docs = pdf_retriever.invoke(state["question"])
    return GraphState(context=format_docs(retrieved_docs))

 

이때, 위와 같은 함수 전체가 LangGraph에서 하나의 Node가 된다.

LangGraph에서는 각 노드가 GraphState → GraphState 변환 함수이기 때문이다.

즉, 입력으로 GraphState("Question")을 받아서 내부 로직으로 문서를 검색하고, GraphState("context")를 출력하는 구조가 Node의 정의와 완전히 동일하다.

 

Node를 만드는 방법은 아래와 같다.

from langgraph.graph import END, StateGraph

# GraphState 타입을 기반으로 하는 stateful workflow 생성
# 이후에 노드를 붙여넣는 컨테이너 역할
workflow = StateGraph(GraphState)

# 노드 추가
workflow.add_node("retrieve", retrieve_document) # 정의한 retrieve_document
workflow.add_node("llm_answer", llm_answer) # 정의한 llm_answer

 

이처럼 노드를 추가하는 방법은 add_node를 사용한다.

왼쪽에는 노드 이름, 오른쪽에는 함수 명을 적어주면 된다.

 

이전 단계에서 retrieve_document라는 함수가 여기에 들어간 것. 그래서 retrieve_document 있고, llm_answer라는 함수가 있었다. 그럼 retrieve라는 노드의 이름을 지어주는 것이고, 오른쪽에는 함수를 넣어주는 것. 이를 통해 retrieve_document라는 노드와 llm_answer라는 노드가 생기는 것이다.

 

 

 

Edge의 구성


우리가 retrieve_document, 혹은 llm_answer라는 노드가 있다면, 어떤 노드에서 어떤 노드로 갈 지를 정의를 해줘야 한다.

이는 add_edge라는 함수로 가능하다.

workflow.set_entrypoint("retrieve")          # 시작 노드
workflow.add_edge("retrieve", "llm_answer")  # 노드 연결
workflow.add_edge("llm_answer", END)         # 종료

 

 

이를 통해 retrieve 노드가 question을 받아서 Context를 추가하고, 추가된 State가 llm_answer 노드로 들어가서 최종 답변을 생성하는 LangGraph Pipeline이 완성된다.

 

이때, 조건부 Edge를 통해 더 복잡한 분기를 만들 수 있다.

만약, relevance_check를 통해서 llm_answer 노드가 내놓은 답을 검증하고 싶다면?

relevance_check 노드를 만들고, 이를 llm_answer와 Node로 연결하는 것 까지는 동일한데, 

relevance_check을 통해 다시 llm_answer, 혹은 retrieve로 돌아가는 edge를 추가하면 된다.

 

조건부 Edge의 구조는 아래와 같다. (node3에서 조건부로 다른 노드로 routing되는 구조)