LangGraph 소개

LangGraph 핵심 문법과 상태 기반 워크플로우 소개

LangGraph에서 자주 사용하는 Python 문법(TypedDict, Annotated, add_messages)과 상태(state) 설계 원칙을 설명한다. TypedDict의 사용 이유, Annotated의 메타데이터 활용, 그리고 LangGraph의 상태 관리와 메시지 리듀서 패턴을 예제로 제시한다.

Agent
LangGraph
Python
AI
저자

Kwangmin Kim

공개

2025년 07월 15일

1 LangGraph 에 자주 등장하는 Python 문법

1.1 TypedDict

  • dict: 일반적인 파이썬 딕셔너리로, 키와 값의 타입을 일반적으로 지정 (예: Dict[str, str]).
  • TypedDict: 클래스 형식으로 만들어 각 키에 대해 구체적인 타입을 지정할 수 있는 딕셔너리. 정적 타입 검사를 제공하여 코드의 안정성과 가독성을 높입니다. 즉, annotation이 붙어있는 dictionary (예, class MyDict(TypedDict): name: str; age: int)

1.1.1 TypedDict와 dict의 주요 차이점과 사용 이유

  • 타입 검사:
    • dict: 런타임에 타입 검사를 하지 않는다.
    • TypedDict: 정적 타입 검사를 제공한다. 즉, 코드 작성 시 IDE나 타입 체커가 오류를 미리 잡아낼 수 있다.
    • 만약 type hint와 다른 데이터 타입이 할당되면, 오류는 발생하지 않지만, 타입 체커(mypy 등)가 오류를 감지할 수 있다.
  • 키와 값의 타입:
    • dict: 키와 값의 타입을 일반적으로 지정한다 (예: Dict[str, str]).
    • TypedDict: 각 키에 대해 구체적인 타입을 지정할 수 있다.
  • 유연성:
    • dict: 런타임에 키를 추가하거나 제거할 수 있다.
    • TypedDict: 정의된 구조를 따라야 한다. 추가적인 키는 타입 오류를 발생시킨다.
# dict 예시
a = dict()
a['age'] = 30  # 어떤 타입이든 허용
print(a) # {'age': 30}
a['age'] = '30'  # 어떤 타입이든 허용
print(a) # {'age': '30'}

# TypedDict 예시
from typing import TypedDict
class Person(TypedDict): # 상속: Person class는 타입이 명시된 딕셔너리" 구조를 갖게 된다.
    name: str
    age: int    
b = Person(name="Alice", age=30)  # 올바른 타입
p = Person(name="Alice", age="30")  # 타입 불일치
print(b) # {'name': 'Alice', 'age': 30} 
print(p)  # {'name': 'Alice', 'age': '30'}  # 실행은 됨

1.1.2 TypedDictdict 대신 사용되는 이유

  • 타입 안정성: TypedDict는 더 엄격한 타입 검사를 제공하여 잠재적인 버그를 미리 방지할 수 있다.
  • 코드 가독성: TypedDict를 사용하면 딕셔너리의 구조를 명확하게 정의할 수 있어 코드의 가독성이 향상된다.
  • IDE 지원: TypedDict를 사용하면 IDE(VS code, Cursor, Copilot 등)에서 자동 완성 및 타입 힌트를 더 정확하게 제공받을 수 있다.
  • 문서화: TypedDict는 코드 자체가 문서의 역할을 하여 딕셔너리의 구조를 명확히 보여준다. TypedDict는 코드 자체가 문서의 역할을 하여 딕셔너리의 구조를 명확히 보여준다.
# TypedDict와 Dict의 차이점 예시
from typing import Dict, TypedDict

# 일반적인 파이썬 딕셔너리(dict) 사용
sample_dict: Dict[str, str] = {
    "name": "테디",
    "age": "30",  # 문자열로 저장 (dict 에서는 가능)
    "job": "개발자",
}


# TypedDict 사용
class Person(TypedDict):
    name: str
    age: int  # 정수형으로 명시
    job: str


typed_dict: Person = {"name": "셜리", "age": 25, "job": "디자이너"}
# dict의 경우
sample_dict["age"] = 35  # 문자열에서 정수로 변경되어도 오류 없음
sample_dict["new_field"] = "추가 정보"  # 새로운 필드 추가 가능

# TypedDict의 경우
typed_dict["age"] = 35  # 정수형으로 올바르게 사용
typed_dict["age"] = "35"  # 타입 체커가 오류를 감지함
typed_dict["new_field"] = (
    "추가 정보"  # 타입 체커가 정의되지 않은 키라고 오류를 발생시킴
)
  • 하지만 TypedDict의 진정한 가치는 정적 타입 검사기를 사용할 때 드러난다.
  • 런타임에는 TypedDict가 일반 딕셔너리처럼 동작하지만, 타입 검사 도구를 사용하면 타입 불일치나 정의되지 않은 키 추가와 같은 오류를 미리 잡아낼 수 있다.
  • 예를 들어, mypy와 같은 정적 타입 검사기를 사용하거나 PyCharm, VS Code 등의 IDE에서 타입 검사 기능을 활성화하면, 이러한 타입 불일치와 정의되지 않은 키 추가를 오류로 표시한다.
  • 정적 타입 검사기를 사용하면 다음과 같은 오류 메시지를 볼 수 있다.

1.2 Annotated

이 문법은 타입 힌트에 메타데이터를 추가할 수 있게 해준다.

1.2.1 Annotated를 사용 이유

추가 정보 제공(타입 힌트) / 문서화

  • 타입 힌트에 추가적인 정보를 포함시킬 수 있다. 이는 코드를 읽는 사람이나 도구에 더 많은 컨텍스트를 제공한다.
  • 코드에 대한 추가 설명을 타입 힌트에 직접 포함시킬 수 있다.
    • name: Annotated[str, "이름"] : name 변수는 문자열 타입이며, “이름”을 넣어달라는 의미를 가진다.
    • age: Annotated[int, "나이"] : age 변수는 정수 타입이며, “나이”를 넣어달라는 의미를 가진다.
  • Annotated 는 Python의 typing 모듈에서 제공하는 특별한 타입 힌트로, 기존 타입에 메타데이터를 추가할 수 있게 해준다.
  • Annotated 는 타입 힌트에 추가 정보를 포함시킬 수 있는 기능을 제공한다. 이를 통해 코드의 가독성을 높이고, 더 자세한 타입 정보를 제공할 수 있다.

1.2.2 Annotated 주요 기능(사용 이유)

  1. 추가 정보 제공: 타입 힌트에 메타데이터를 추가하여 더 상세한 정보를 제공한다.
  2. 문서화: 코드 자체에 추가 설명을 포함시켜 문서화 효과를 얻을 수 있다.
  3. 유효성 검사: 특정 라이브러리(예: Pydantic)와 함께 사용하여 데이터 유효성 검사를 수행할 수 있다.
  4. 프레임워크 지원: 일부 프레임워크(예: LangGraph)에서는 Annotated를 사용하여 특별한 동작을 정의한다.

기본 문법

  • Type: 기본 타입 (예: int, str, List[str] 등)
  • metadata1, metadata2, …: 추가하고자 하는 메타데이터
from typing import Annotated

variable: Annotated[Type, metadata1, metadata2, ...]

1.2.3 사용 예시

기본 사용

from typing import Annotated

name: Annotated[str, "사용자 이름"]
age: Annotated[int, "사용자 나이 (0-150)"]

Pydantic과 함께 사용

from typing import Annotated, List
from pydantic import Field, BaseModel, ValidationError


class Employee(BaseModel):
    id: Annotated[int, Field(..., description="직원 ID")] # `...`는 필수 필드임을 나타냄: id는 반드시 제공되어야 한다는 의미
    name: Annotated[str, Field(..., min_length=3, max_length=50, description="이름")] # `...`: name은 반드시 제공되어야 한다는 의미
    age: Annotated[int, Field(gt=18, lt=65, description="나이 (19-64세)")]
    salary: Annotated[
        float, Field(gt=0, lt=10000, description="연봉 (단위: 만원, 최대 10억)")
    ]
    skills: Annotated[
        List[str], Field(min_items=1, max_items=10, description="보유 기술 (1-10개)")
    ]


# 유효한 데이터로 인스턴스 생성
try:
    valid_employee = Employee(
        id=1, name="테디노트", age=30, salary=1000, skills=["Python", "LangChain"]
    )
    print("유효한 직원 데이터:", valid_employee)
except ValidationError as e:
    print("유효성 검사 오류:", e)

# 유효하지 않은 데이터로 인스턴스 생성 시도
try:
    invalid_employee = Employee(
        name="테디",  # 이름이 너무 짧음
        age=17,  # 나이가 범위를 벗어남
        salary=20000,  # 급여가 범위를 벗어남
        skills="Python",  # 리스트가 아님
    )
except ValidationError as e:
    print("유효성 검사 오류:")
    for error in e.errors():
        print(f"- {error['loc'][0]}: {error['msg']}")
유효한 직원 데이터: id=1 name='테디노트' age=30 salary=1000.0 skills=['Python', 'LangChain']
유효성 검사 오류:
- id: Field required
- name: String should have at least 3 characters
- age: Input should be greater than 18
- salary: Input should be less than 10000
- skills: Input should be a valid list

1.2.4 LangGraph에서의 사용(add_messages)

add_messages 는 LangGraph 에서 메시지를 리스트에 추가하는 함수

from typing import Annotated, TypedDict
from langgraph.graph import add_messages


class MyData(TypedDict):
    messages: Annotated[list, add_messages]
from typing import Annotated, TypedDict
from langgraph.graph import add_messages


class MyData(TypedDict):
    messages: Annotated[list, add_messages]

참고

  1. Annotated는 Python 3.9 이상에서 사용 가능
  2. 런타임에는 Annotated가 무시되므로, 실제 동작에는 영향을 주지 않는다.
  3. 타입 검사 도구나 IDE가 Annotated를 지원해야 그 효과를 볼 수 있다.

1.3 add_messages

messages 키는 add_messages 리듀서 함수로 주석이 달려 있으며, 이는 LangGraph에게 기존 목록에 새 메시지를 추가하도록 지시

주석이 없는 상태 키는 각 업데이트에 의해 덮어쓰여져 가장 최근의 값이 저장됨. 하지만 add_messages 리듀서를 사용하면, 새 메시지가 기존 목록에 추가되어 누적되는 방식으로 동작한다.

add_messages 함수는 2개의 인자(left, right)를 받으며 좌, 우 메시지를 병합하는 방식으로 동작

주요 기능
- 두 개의 메시지 리스트를 병합
- 기본적으로 “append-only” 상태를 유지
- 동일한 ID를 가진 메시지가 있을 경우, 새 메시지로 기존 메시지를 대체
동작 방식
- right의 메시지 중 left에 동일한 ID를 가진 메시지가 있으면, right의 메시지로 대체
- 그 외의 경우 right의 메시지가 left에 추가됨
매개변수
- left (Messages): 기본 메시지 리스트
- right (Messages): 병합할 메시지 리스트 또는 단일 메시지
반환값
- Messages: right의 메시지들이 left에 병합된 새로운 메시지 리스트

from langchain_core.messages import AIMessage, HumanMessage
from langgraph.graph import add_messages

# 기본 사용 예시
msgs1 = [HumanMessage(content="안녕하세요?", id="1")]
msgs2 = [AIMessage(content="반갑습니다~", id="2")]

result1 = add_messages(msgs1, msgs2)
print(result1)

다른 id(id=1, id=2)를 가진 메시지는 기존 메세지에 추가가 된다.

[HumanMessage(content='안녕하세요?', additional_kwargs={}, response_metadata={}, id='1'), AIMessage(content='반갑습니다~', additional_kwargs={}, response_metadata={}, id='2')]

동일한 ID 를 가진 Message 가 있을 경우 대체된다.

# 동일한 ID를 가진 메시지 대체 예시
msgs1 = [HumanMessage(content="안녕하세요?", id="1")]
msgs2 = [HumanMessage(content="반갑습니다~", id="1")]

result2 = add_messages(msgs1, msgs2)
print(result2)
[HumanMessage(content='반갑습니다~', additional_kwargs={}, response_metadata={}, id='1')]

Subscribe

Enjoy this blog? Get notified of new posts by email: