빅데이타 & 머신러닝/생성형 AI (ChatGPT etc)

고급 Agent를 위한 Langgraph 개념 이해 #1-State

Terry Cho 2025. 7. 8. 07:52

먼저 Langgraph의 개념을 이해하려면 3가지 State, Node, Edge에 대해서 알아야 한다. 

각 컴포넌트의 개념과 세부 문법에 대한 이해가 없으면 어떻게 Langgraph가 작동하는지에 대해서 이해가 매우 어렵기 때문에, 이 글에서는 개념과 문법에 대해서 설명하도록 한다. 

 

그래프는 Node로 구성이 되어 있는데, State를 받아서, 작업을 하고, State를 출력하는 역할을 한다. 

Node를 실행한 후에, 다른 Node를 실행하기 위해서는 Edge를 따라서, Node를 이동하게 된다. 

이 개념을 그림으로 표현하면 아래와 같다. 사각형이 Node이고, Node들을 연결하는 선이 Edge, 그리고, Node들이 입력으로 받고, 업데이트 하는 것이 State (상태 정보)이다. 

<그림. Lang Graph상에서 Graph의 개념적인 구조>

1. State

위의 그림상에서는 이해를 돕기 위해서 State를 Edge를 타고 다니는 변수처럼 표현했는데, 사실을 Graph에서 단일하게 존재하는 현재의 상태 정보이다.  

사실 Input/Output으로 생각해도 되는 것이 Node에 Input 인자가 State이고, 리턴값 역시 변경된 State이기 때문에, 사실상 Input/Output이 맞지만, 그 Input/Output이 전역변수와 같은 개념으로 State에 저장된다.

  • State는 보통 TypedDict 타입이나 Pydantic BaseModel을 사용해서 구현한다. Value 값만 가지고 있는 Value Object와 같은 개념으로 생각하면 된다. 

아래는 간단한 예제로 사용자 이름과,나이, 그리고 질의(query)를 state로 저장하는 예시이다. 

class State(TypedDict):
    user: str,
    age : int,
    query: str

 

State가 중요한 이유는 통상적으로 Node에서 LLM이나 Tool을 호출하여 답변을 만들어내기 위한 작업을 하는데, 이때 주요하게 참고하는 input이 State정보이다. State에 얼마나 필요한 Context를 포함하느냐에 따라서, 답변을 정확하게 유도할 수 있다. 

Reducer를 이용한 상태 정보 변경

Reducer는 State에 있는 필드를 변경할때 어떻게 변경할지를 정하는 함수이다. Default로는 별도의 Reducer를 정의하지 않으면, 해당 필드를 업데이트 한다. 

Default Reducer

아래는 State 클래스를 정의한 예제로 별도의 Reducer를 정의하지 않았다.이 경우 Default reducer가 사용이 된다.

from typing_extensions import TypedDict

class State(TypedDict):
    foo: int
    bar: list[str]

State 정보를 변경하는 방법은 Node에서 return 값으로 State 스키마에 맞는 새로운 State 정보를 Dictionary 형태로 리턴하면 된다. 예를 들어 위의 State값을 업데이트 하는 Node를 만든다면 다음과 같은 형태가 된다. 

def update_values(state: State) -> State: 
  :
   updated_state = {"foo": new_foo, "bar": new_bar} 
return updated_state

Dictionary로 {foo,bar}키에 있는 값을 새로운 값으로 업데이트 하였다. 

만약에 전체 필드를 업데이트하지 않고, 일부 필드만 업데이트 했을 경우에는 업데이트된 필드의 값만 변경이 되고, 업데이트 되지 않은 필드가 없어지는 것이 아니라 기존 값이 그대로 유지된다.  아래 예제의 경우에는 State에서 foo 필드만 업데이트 하는 예제로, bar의 값은 변경 없이 유지 된다. 

def update_values(state: State) -> State: 
  :
   updated_state = {"foo": new_foo} 
return updated_state

Reducer를 지정하는 방법

Reducer로 사용할 함수를 지정하기 위해서는 일반적으로 Annotated를 사용한다. 

Annotated에서는 해당 필드에 대한 데이터 형과, 데이터를 업데이트 할때는 Behavior를 두번째 인자로 지정한다. 아래 예제를 보자. 

from typing import Annotated
from typing_extensions import TypedDict
from operator import add

class State(TypedDict):
   foo: int
   bar: Annotated[list[str], add]

bar 필드는 string 타입의 list로, update가 발생할 시에, 해당 필드를 업데이트하지 않고, add라는 operator를 이용해서, 리스트에 계속해서 데이터를 append하게 된다. 

예를 들어

  • 처음에 State를 foo=10, bar=”hello”로 업데이트하면, 해당 State는 foo=10, bar=[“hello”] 상태가 된다.
  • 다음 State를 foo=20, bar=”world”로 업데이트 하면, 해당 State는 foo=20, bar=[“hello”,”world”]로, bar에서 “world”가 리스트에 업데이트 된다. 

Message를 State로 사용

Node는 State 정보를 기반으로 LLM 모델을 호출하는데, LLM 모델을 호출하는데 가장 유용한 정보는 사용자로 부터 받은 message들과 LLM이 내부적으로 생성한 message들이 LLM 호출의 중요한 정보로 사용될 수 있다. 즉 message history가 LLM 호출의 중요한 컨텍스트로 사용될 수 있기 때문에, State에 message history를 저장해놓으면 Node에서 LLM 호출시 유용하게 사용할 수 있다. 

 

Message는 Human Message와 AIMessage 그리고 Tool의 응답으로 사용되는 ToolMessage등 여러 타입이 있기 때문에, 이 모든 메시지 타입을 포함하기 위해서 State의 필드 타입은 AnyMessage로 하고, 이 Message들을 모두 append 해서 저장하기 때문에, 타입은 list로 한다. 

그리고, LangGraph에서는 이 list에 메시지를 append하기 위한 Reducer로 addmessages라는 function을 제공한다. 

 

그래서 message를 저장하는 State의 필드 구조는 다음과 같다. 

from langchain_core.messages import AnyMessage
from langgraph.graph.message import add_messages
from typing import Annotated
from typing_extensions import TypedDict
class GraphState(TypedDict):
   messages: Annotated[list[AnyMessage], add_messages]

Messages는 Annotated를 이용해서 list[AnyMessage] 타입으로 지정된것을 확인할 수 있고,  reducer로는 add_messages가 지정된것을 확인할 수 있다.  

 

다음글에서는 두번째 개념인 Node에 대해서 알아보도록 한다.