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 TypedDict가 dict 대신 사용되는 이유
- 타입 안정성:
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 주요 기능(사용 이유)
- 추가 정보 제공: 타입 힌트에 메타데이터를 추가하여 더 상세한 정보를 제공한다.
- 문서화: 코드 자체에 추가 설명을 포함시켜 문서화 효과를 얻을 수 있다.
- 유효성 검사: 특정 라이브러리(예: Pydantic)와 함께 사용하여 데이터 유효성 검사를 수행할 수 있다.
- 프레임워크 지원: 일부 프레임워크(예: LangGraph)에서는
Annotated를 사용하여 특별한 동작을 정의한다.
기본 문법
Type: 기본 타입 (예:int,str,List[str]등)metadata1,metadata2, …: 추가하고자 하는 메타데이터
1.2.3 사용 예시
기본 사용
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]참고
Annotated는 Python 3.9 이상에서 사용 가능
- 런타임에는
Annotated가 무시되므로, 실제 동작에는 영향을 주지 않는다. - 타입 검사 도구나 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')]