[๊ฐ์๋ ธํธ] LangChain Academy : Introduction to LangGraph (Module 2)
์๋ณธ ๊ฒ์๊ธ: https://velog.io/@euisuk-chung/LangChain-Academy-Introduction-to-LangGraph-Module-2
๋ญ์ฒด์ธ(LangChain)๊ณผ ๋ญ๊ทธ๋ํ(LangGraph)๋ ๋๊ท๋ชจ ์ธ์ด ๋ชจ๋ธ(LLM)์ ํ์ฉํ ์ ํ๋ฆฌ์ผ์ด์ ๊ฐ๋ฐ์ ์ํ ๋๊ตฌ๋ค์ ๋๋ค. ์ ๊ฐ์๋ LangChain์์ ์ด์ํ๋ LangChain Academy์์ ์ ์ํ โIntroduction to LangGraphโ ๊ฐ์์ ๋ด์ฉ์ ์ ๋ฆฌ ๋ฐ ์ถ๊ฐ ์ค๋ช ํ ๋ด์ฉ์ ๋๋ค.
- ๊ฐ์ ๋งํฌ : https://youtu.be/29XE10U6ooc
- ๋ญ์ฒด์ธ : https://www.langchain.com/
์ด๋ฒ ํฌ์คํธ๋ โModule2โ๋ด์ฉ์ ๋ค๋ฃน๋๋ค:
๋ชฉ์ฐจ
- Lesson 1: State Schema
- Lesson 2: State Reducers
- Lesson 3: Multiple Schemas
- Lesson 4: Trim and Filter Messages
- Lesson 5: Chatbot w/ Summarizing Messages and Memory
- Lesson 6: Chatbot w/ Summarizing Messages and External Memory
Lesson 1: State Schema
LangGraph์ ์ฃผ์ ๊ฐ๋ ๊ณผ ๊ตฌ์ฑ ์์
-
๊ทธ๋ํ ๊ตฌ์กฐ:
- LangGraph๋ ์์ด์ ํธ ์ํฌํ๋ก์ฐ๋ฅผ ๊ทธ๋ํ๋ก ๋ชจ๋ธ๋งํฉ๋๋ค.
- ์ฃผ์ ๊ตฌ์ฑ ์์: State(์ํ), Nodes(๋ ธ๋), Edges(์ฃ์ง)
-
StateGraph์ MessageGraph:
- StateGraph: ์ฌ์ฉ์ ์ ์ State ๊ฐ์ฒด๋ก ๋งค๊ฐ๋ณ์ํ๋ ์ฃผ์ ๊ทธ๋ํ ํด๋์ค
- MessageGraph: State๊ฐ ๋ฉ์์ง ๋ฆฌ์คํธ๋ก๋ง ๊ตฌ์ฑ๋ ํน์ํ ๊ทธ๋ํ ์ ํ
-
์ํ ๊ด๋ฆฌ:
- TypedDict๋ Pydantic ๋ชจ๋ธ์ ์ฌ์ฉํ์ฌ ์ํ ์คํค๋ง ์ ์
- Reducer ํจ์๋ฅผ ํตํ ์ํ ์ ๋ฐ์ดํธ ๋ฐฉ์ ์ ์
- ๋ค์ค ์คํค๋ง ์ง์: ๋ด๋ถ ๋ ธ๋ ํต์ , ์ ์ถ๋ ฅ ์คํค๋ง ๋ถ๋ฆฌ ๋ฑ
-
๋ ธ๋์ ์ฃ์ง:
- ๋ ธ๋: ๊ทธ๋ํ์ ๋ก์ง์ ๊ตฌํํ๋ Python ํจ์
- ์ฃ์ง: ๋ ธ๋ ๊ฐ ์ฐ๊ฒฐ๊ณผ ์คํ ํ๋ฆ์ ์ ์
- ์กฐ๊ฑด๋ถ ์ฃ์ง๋ฅผ ํตํ ๋์ ๋ผ์ฐํ
-
๋ฉ์์ง ์ฒ๋ฆฌ:
- add_messages ํจ์๋ฅผ ์ฌ์ฉํ ํจ์จ์ ์ธ ๋ฉ์์ง ๊ด๋ฆฌ
- MessagesState๋ฅผ ํตํ ๊ฐํธํ ๋ฉ์์ง ์ํ ๊ด๋ฆฌ
-
๊ทธ๋ํ ์คํ:
- ์ปดํ์ผ ๊ณผ์ ์ ํตํ ๊ทธ๋ํ ๊ตฌ์กฐ ๊ฒ์ฆ
- โsuper-stepsโ๋ฅผ ํตํ ์ด์ฐ์ ์คํ ๋ฐฉ์
-
์ ์ฐ์ฑ๊ณผ ํ์ฅ์ฑ:
- ๋ค์ํ ์ํ ์คํค๋ง ๋ฐ ์ ๋ฐ์ดํธ ๋ฐฉ์ ์ง์
- ๋ณต์กํ ์ํฌํ๋ก์ฐ ๊ตฌํ ๊ฐ๋ฅ
State Schema
๋ Langraph์์ ์์ด์ ํธ๊ฐ ์ฒ๋ฆฌํ๋ ๋ฐ์ดํฐ์ ๊ตฌ์กฐ์ ํ์
์ ์ ์ํ๋ ๋ฐฉ์์
๋๋ค. ์ด ์คํค๋ง๋ ๋ฐ์ดํฐ์ ํ์์ ์ง์ ํ์ฌ, ๊ทธ๋ํ ๋ด์์ ๋ฐ์ดํฐ๊ฐ ์ด๋ป๊ฒ ์ฒ๋ฆฌ๋๊ณ ์
๋ฐ์ดํธ๋๋์ง๋ฅผ ์ ์ดํฉ๋๋ค.
-
Langraph์์๋
Typedict
,Python Data Classes
,Pydantic
๋ฑ ๋ค์ํ ๋ฐฉ๋ฒ์ผ๋ก ์คํค๋ง๋ฅผ ์ ์ํ ์ ์์ต๋๋ค. ๊ฐ๊ฐ์ ๋ฐฉ๋ฒ์ ๋ํด์ ์ดํด๋ณด๊ฒ ์ต๋๋ค.- Typedict: ์ฃผ๋ก ์ฌ์ฉ๋๋ ๋ฐฉ์์ผ๋ก, Python์ ๋์
๋๋ฆฌ ํ์์ ๊ธฐ๋ฐ์ผ๋ก ํ์ฌ ํค(key)์ ํด๋น ๊ฐ์ ํ์
์ ์ง์ ํฉ๋๋ค.
- ํ์ง๋ง Typedict๋ ๋ฐํ์์์ ํ์ ๊ฒ์ฌ๋ฅผ ํ์ง ์์, ์๋ชป๋ ๊ฐ์ด ํ ๋น๋์ด๋ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ง ์๋ ๋ฌธ์ ๊ฐ ์์ต๋๋ค.
-
์์ Module 1์์ ๋์๋ ์์๋ก ์๋์ ๊ฐ์ ์์๊ฐ ์์์ต๋๋ค:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
# Module 1 - TypeDict์์ import random from IPython.display import Image, display from langgraph.graph import StateGraph, START, END def node_1(state): print("---Node 1---") return {"name": state['name'] + " is ... "} def node_2(state): print("---Node 2---") return {"mood": "happy"} def node_3(state): print("---Node 3---") return {"mood": "sad"} def decide_mood(state) -> Literal["node_2", "node_3"]: # Here, let's just do a 50 / 50 split between nodes 2, 3 if random.random() < 0.5: # 50% of the time, we return Node 2 return "node_2" # 50% of the time, we return Node 3 return "node_3" # Build graph builder = StateGraph(TypedDictState) builder.add_node("node_1", node_1) builder.add_node("node_2", node_2) builder.add_node("node_3", node_3) # Logic builder.add_edge(START, "node_1") builder.add_conditional_edges("node_1", decide_mood) builder.add_edge("node_2", END) builder.add_edge("node_3", END) # Add graph = builder.compile() # View display(Image(graph.get_graph().draw_mermaid_png()))
(์ฐธ๊ณ ) ์ผ๋ฐ ํด๋์ค
- ๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ธ ๊ฐ์ฒด ์งํฅ ํ๋ก๊ทธ๋๋ฐ์ ๊ตฌ์กฐ์ ๋๋ค.
- ์์ฑ๊ณผ ๋ฉ์๋๋ฅผ ์์ ๋กญ๊ฒ ์ ์ํ ์ ์์ต๋๋ค.
- init() ๋ฑ์ ๋ฉ์๋๋ฅผ ์ง์ ๊ตฌํํด์ผ ํฉ๋๋ค.
- ์บก์ํ, ์์, ๋คํ์ฑ ๋ฑ์ OOP ๊ฐ๋ ์ ์์ ํ ํ์ฉํ ์ ์์ต๋๋ค.
(์ฐธ๊ณ ) TypedDict
-
Python 3.8๋ถํฐ ์ ์์ผ๋ก ๋์ ๋์์ต๋๋ค
(์ด์ ๋ฒ์ ์์๋ typing_extensions ๋ชจ๋์ ํตํด ์ฌ์ฉ ๊ฐ๋ฅ).
- ๋์ ๋๋ฆฌ์ ํค์ ๊ฐ์ ๋ํ ํ์ ์ ์ง์ ํ ์ ์๊ฒ ํด์ค๋๋ค.
- ๋ฐํ์์๋ ์ผ๋ฐ ๋์ ๋๋ฆฌ์ฒ๋ผ ๋์ํฉ๋๋ค.
- ํค์ ๋ํ ์ ๊ทผ์ ๋์
๋๋ฆฌ ๋ฌธ๋ฒ์ ์ฌ์ฉํฉ๋๋ค (์:
obj["key"]
). - ์ฃผ๋ก ์ ์ ํ์ ๊ฒ์ฌ๋ฅผ ์ํด ์ฌ์ฉ๋ฉ๋๋ค.
(์ฐธ๊ณ ) Dataclass
- Python 3.7๋ถํฐ ๋์ ๋ ๊ธฐ๋ฅ์ ๋๋ค.
- ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๊ธฐ ์ํ ํด๋์ค๋ฅผ ์ฝ๊ฒ ๋ง๋ค ์ ์๊ฒ ํด์ค๋๋ค.
- ์๋์ผ๋ก init(), repr(), eq() ๋ฑ์ ๋ฉ์๋๋ฅผ ์์ฑํฉ๋๋ค.
- ํ์ ํํ ์ ์ง์ํฉ๋๋ค.
- ์์ฑ์ ์ง์ ์ ๊ทผ ๊ฐ๋ฅํฉ๋๋ค (์: obj.attribute).
- ๋ถ๋ณ์ฑ(immutability)์ ์ฝ๊ฒ ๊ตฌํํ ์ ์์ต๋๋ค.
(์ฐธ๊ณ ) TypedDict vs Dataclass
-
์ฃผ์ ์ฐจ์ด์ ์ผ๋ก๋ ์์ฑ ์ ๊ทผ ๋ฐฉ์์ด ๋ค๋ฆ ๋๋ค.
- TypedDict: ๋์ ๋๋ฆฌ ์คํ์ผ๋ก ์ ๊ทผ (state[โnameโ])
- dataclass: ๊ฐ์ฒด ์์ฑ ์คํ์ผ๋ก ์ ๊ทผ (state.name)
- Python Data Classes: ๋ฐ์ดํฐ ํด๋์ค๋ ๋ ๊ฐ๊ฒฐํ ๊ตฌ๋ฌธ์ ์ ๊ณตํ๋ฉฐ, ์์ฑ์ ์ ๊ทผํ ๋
dict
๋์dot(.)
์ฐ์ฐ์๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค.- ํ์ง๋ง ์ญ์ ํ์ ํํธ๊ฐ ๋ฐํ์์ ๊ฐ์ ๋์ง ์์ ์๋ชป๋ ๊ฐ์ ํ ๋นํด๋ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ง ์์ต๋๋ค.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
def node_1(state): print("---Node 1---") return {"name": state.name + " is ... "} # Build graph builder = StateGraph(DataclassState) builder.add_node("node_1", node_1) builder.add_node("node_2", node_2) builder.add_node("node_3", node_3) # Logic builder.add_edge(START, "node_1") builder.add_conditional_edges("node_1", decide_mood) builder.add_edge("node_2", END) builder.add_edge("node_3", END) # Add graph = builder.compile() # View display(Image(graph.get_graph().draw_mermaid_png()))
- Pydantic: ๋ฐํ์์์ ๋ฐ์ดํฐ ์ ํจ์ฑ์ ๊ฒ์ฌํ๋ ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค.
- Pydantic์ Python์ ํ์ ํํ ์ ์ฌ์ฉํ์ฌ ๋ฐ์ดํฐ ๊ฒ์ฆ๊ณผ ์ค์ ๊ด๋ฆฌ๋ฅผ ์ ๊ณตํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋๋ค. LangGraph์์ ์ํ ์คํค๋ง๋ฅผ ์ ์ํ ๋ ํนํ ์ ์ฉํฉ๋๋ค.
- Pydantic ์ฃผ์ ํน์ง
- ๋ฐํ์ ๋ฐ์ดํฐ ๊ฒ์ฆ
- ๋ณต์กํ ๋ฐ์ดํฐ ๊ตฌ์กฐ ์ง์
- ์๋ ๋ฌธ์ํ
- ์ค์ ๊ด๋ฆฌ
- JSON ์คํค๋ง ์์ฑ
-
์๋๋ ์ค์ Pydantic ํด๋์ค๋ฅผ ์ ์ธํ๊ณ validation์ ์ํํ๋ ์ฝ๋์ ๋๋ค:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
from pydantic import BaseModel, field_validator, ValidationError # BaseModel ์์ class PydanticState(BaseModel): name: str mood: str # "happy" or "sad" @field_validator('mood') @classmethod def validate_mood(cls, value): # Ensure the mood is either "happy" or "sad" if value not in ["happy", "sad"]: raise ValueError("Each mood must be either 'happy' or 'sad'") return value try: state = PydanticState(name="John Doe", mood="mad") except ValidationError as e: print("Validation Error:", e)
a. BaseModel ์์:
PydanticState(BaseModel)
: PydanticState ํด๋์ค๋ Pydantic์ BaseModel์์์
๋ฐ์ ์ ์๋ฉ๋๋ค.- ์ด๋ฅผ ํตํด ์๋ ๊ฒ์ฆ, ์ง๋ ฌํ, ์ญ์ง๋ ฌํ ๊ธฐ๋ฅ์ ์ฌ์ฉํ ์ ์์ต๋๋ค.
b. ํ๋ ์ ์:
name
๊ณผmood
ํ๋๋ฅผ str ํ์ ์ผ๋ก ์ ์ํฉ๋๋ค.
c. @field_validator ๋ฐ์ฝ๋ ์ดํฐ:
@field_validator ๋ฐ์ฝ๋ ์ดํฐ
: Pydantic ๋ผ์ด๋ธ๋ฌ๋ฆฌ์์ ์ ๊ณตํ๋ ๊ธฐ๋ฅ์ ๋๋ค. Pydantic์ ํ์ด์ฌ์์ ๋ฐ์ดํฐ ๋ชจ๋ธ์ ์ ์ํ๊ณ ๊ฒ์ฆํ๋ ๋ฐ ์ฌ์ฉ๋ฉ๋๋ค.@field_validator
๋ ํน์ ํ๋์ ๊ฐ์ด ๋ง๋์ง ํ์ธํ๋ ๋ก์ง์ ์ง์ ๋ง๋ค ์ ์๊ฒ ํด์ค๋๋ค.- ์ฆ, ์ด๋ค ํ๋์ ๊ฐ์ด ๋ค์ด์์ ๋ ๊ทธ ๊ฐ์ด ์ฐ๋ฆฌ๊ฐ ์ํ๋ ์กฐ๊ฑด์ ๋ง๋์ง ์๋์ผ๋ก ๊ฒ์ฌํด์ฃผ๋ ๊ธฐ๋ฅ์ด์์.
- ์๋ฅผ ๋ค์ด,
@field_validator('mood')
๋ผ๋ฉด, โmood
โ๋ผ๋ ํ๋์ ๊ฐ์ด ๋ค์ด์ฌ ๋, ๊ทธ ๊ฐ์ด โhappyโ๋ โsadโ ์ค ํ๋์ธ์ง ํ์ธํ๊ณ ์ ํ๋ ๋ชฉ์ ์ผ๋ก ์ฌ์ฉ๋์๋ค๊ณ ๋ณด๋ฉด ๋ฉ๋๋ค.
d. @classmethod:
- ํด๋์ค ์์ฒด์ ๋ํด ์์ ์ ํ ์ ์๊ฒ ๋ง๋ค์ด์ฃผ๋ ๋ฐ์ฝ๋ ์ดํฐ์ ๋๋ค.
- ์ผ๋ฐ์ ์ผ๋ก ๋ฉ์๋๋ฅผ ๋ง๋ค๋ฉด ์ธ์คํด์ค(๊ฐ์ฒด)๋ฅผ ์ฌ์ฉํด์ ํธ์ถํด์ผ ํ๋๋ฐ, ํด๋์ค ๋ฉ์๋๋ ์ธ์คํด์ค๊ฐ ์๋ ํด๋์ค ์์ฒด์ ์ฐ๊ฒฐ๋์ด ์คํ๋ฉ๋๋ค.
- Pydantic์์ ํ๋ ๊ฒ์ฆ์ ํ ๋, ๊ฒ์ฆ ๋ฉ์๋๋ฅผ ํด๋์ค ๋ ๋ฒจ์์ ์ฌ์ฉํด์ผ ํ๊ธฐ ๋๋ฌธ์ @classmethod๊ฐ ํ์๋กํ๊ฒ ๋ฉ๋๋ค.
- ์ฆ, ์ธ์คํด์ค๋ฅผ ๋ง๋ค๊ธฐ ์ ์ ํ๋ ๊ฐ์ ํ์ธํด์ผ ํ๊ธฐ ๋๋ฌธ์ ํด๋์ค ์ ์ฒด์์ ๊ทธ ๊ฒ์ฆ ๋ก์ง์ ์ ๊ทผํ ์ ์๋๋ก @classmethod๋ก ๋ฉ์๋๋ฅผ ์ ์ํ๋ ๊ฒ์ด์ฃ .
e. ์์ธ ์ฒ๋ฆฌ:
- ์ ํจํ์ง ์์ ๋ฐ์ดํฐ๋ก ์ธ์คํด์ค๋ฅผ ์์ฑํ๋ ค๊ณ ํ๋ฉด ValidationError๊ฐ ๋ฐ์ํฉ๋๋ค.
-
์๋ ์์์์๋ โmadโ๋ผ๋ ์ ํจํ์ง ์์ mood ๊ฐ์ ์ฌ์ฉํ์ฌ ์ค๋ฅ๋ฅผ ๋ฐ์์ํต๋๋ค.
- Typedict: ์ฃผ๋ก ์ฌ์ฉ๋๋ ๋ฐฉ์์ผ๋ก, Python์ ๋์
๋๋ฆฌ ํ์์ ๊ธฐ๋ฐ์ผ๋ก ํ์ฌ ํค(key)์ ํด๋น ๊ฐ์ ํ์
์ ์ง์ ํฉ๋๋ค.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Build graph
builder = StateGraph(PydanticState)
builder.add_node("node_1", node_1)
builder.add_node("node_2", node_2)
builder.add_node("node_3", node_3)
# Logic
builder.add_edge(START, "node_1")
builder.add_conditional_edges("node_1", decide_mood)
builder.add_edge("node_2", END)
builder.add_edge("node_3", END)
# Add
graph = builder.compile()
# View
display(Image(graph.get_graph().draw_mermaid_png()))
Lesson 2: State Reducers
์์ Module 1์์ ์ค๋ช
ํ๋ State Reducer์ ๋ํด์ ๋์ฑ ์์ธํ๊ฒ ์ค๋ช
ํฉ๋๋ค. ์๋ ์์๋ TypeDict
๋ฅผ ์ฌ์ฉํ ๋ ๋ฐ์ํ ์ ์๋ ์ํ ์
๋ฐ์ดํธ ์ถฉ๋ ๋ฌธ์ ์
๋๋ค.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class State(TypedDict):
foo: int
def node_1(state):
print("---Node 1---")
return {"foo": state['foo'] + 1}
def node_2(state):
print("---Node 2---")
return {"foo": state['foo'] + 1}
def node_3(state):
print("---Node 3---")
return {"foo": state['foo'] + 1}
# Build graph
builder = StateGraph(State)
builder.add_node("node_1", node_1)
builder.add_node("node_2", node_2)
builder.add_node("node_3", node_3)
# Logic
builder.add_edge(START, "node_1")
builder.add_edge("node_1", "node_2")
builder.add_edge("node_1", "node_3")
builder.add_edge("node_2", END)
builder.add_edge("node_3", END)
# Add
graph = builder.compile()
# View
display(Image(graph.get_graph().draw_mermaid_png()))
์ ๊ทธ๋ฆผ๋ฅผ ํด์ํด๋ณด๋ฉด node_1์ node_2์ node_3์ผ๋ก ๋ถ๊ธฐ๋ฉ๋๋ค. node_2์ node_3๋ ๋ณ๋ ฌ๋ก ์คํ๋ฉ๋๋ค. ์๋ ์ฝ๋๋ฅผ ์คํํด๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ์ ์๋ฌ๊ฐ ๋ฐ์ํฉ๋๋ค.
1
2
3
4
5
from langgraph.errors import InvalidUpdateError
try:
graph.invoke({"foo" : 1})
except InvalidUpdateError as e:
print(f"InvalidUpdateError occurred: {e}")
์ํ ์
๋ฐ์ดํธ ๋ฌธ์
:
- ๊ฐ ๋ ธ๋๋ โfooโ ๊ฐ์ 1์ฉ ์ฆ๊ฐ์ํค๋ ค ํฉ๋๋ค.
- node_2์ node_3๊ฐ ๋์์ ์คํ๋๋ฉด์ ๋ ๋ค โfooโ ๊ฐ์ ์ ๋ฐ์ดํธํ๋ ค ํฉ๋๋ค.
์ถฉ๋ ๋ฐ์
:
- ๋ณ๋ ฌ ์คํ ์ค ๋ ๋ ธ๋๊ฐ ๊ฐ์ ํค(โfooโ)๋ฅผ ๋์์ ์ ๋ฐ์ดํธํ๋ ค ํฉ๋๋ค.
- LangGraph๋ ์ด๋ฐ ์ํฉ์์ ์ด๋ค ์ ๋ฐ์ดํธ๋ฅผ ์ ์ฉํด์ผ ํ ์ง ๊ฒฐ์ ํ ์ ์์ต๋๋ค.
-
์ด๋ฌํ ์ถฉ๋ ์ํฉ์์ LangGraph๋
InvalidUpdateError
๋ฅผ ๋ฐ์์ํต๋๋ค.1 2 3 4 5 6
We see a problem! Node 1 branches to nodes 2 and 3. Nodes 2 and 3 run in parallel, which means they run in the same step of the graph. They both attempt to overwrite the state *within the same step*. This is ambiguous for the graph! Which state should it keep?
- ์ด๋ฌํ ์ถฉ๋์ ํด๊ฒฐํ๊ธฐ ์ํด์๋ ์ํ ์ ๋ฐ์ดํธ ๋ฐฉ์์ ๋ช ํํ ์ ์ํด์ผ ํฉ๋๋ค.
์ด๋ ์ฌ์ฉ๋๋ ๊ฒ์ด ๋ฐ๋ก ๊ธฐ๋ณธ Reducer ํจ์์ ๋๋ค.
-
Reducer๋ LangGraph์์ ์ํ(State) ์ ๋ฐ์ดํธ๋ฅผ ์ฒ๋ฆฌํ๋ ํจ์์ ๋๋ค.
์ ๋ฐ์ดํธ ๋ช ์์ ์ผ๋ก ์ ์
: ๋ ธ๋์์ ๋ฐํ๋ ์ ๋ฐ์ดํธ๋ฅผ ํ์ฌ ์ํ์ ์ด๋ป๊ฒ ์ ์ฉํ ์ง ์ ์ํฉ๋๋ค.๋ฐ์ํ ์ ์๋ ์ถฉ๋์ ํด๊ฒฐ
: ์ฌ๋ฌ ๋ ธ๋์์ ๋์์ ์ํ๋ฅผ ์ ๋ฐ์ดํธํ ๋ ๋ฐ์ํ ์ ์๋ ์ถฉ๋์ ํด๊ฒฐํฉ๋๋ค.
-
Python์
typing ๋ชจ๋
์์ ์ ๊ณตํ๋Annotated
๋ฅผ ์ฌ์ฉํ์ฌ reducer ํจ์๋ฅผ ์ง์ ํฉ๋๋ค.-
Annotated๋ Python์ typing ๋ชจ๋์์ ์ ๊ณตํ๋ ํน๋ณํ ํ์ ํํธ์ ๋๋ค.
Annotated[Type, metadata1, metadata2, ...]
-
-
Python์ ๋ด์ฅ
operator ๋ชจ๋
์add
ํจ์๋ฅผ reducer๋ก ์ฌ์ฉํฉ๋๋ค.-
LangGraph์์ operator.add๋ฅผ reducer๋ก ์ฌ์ฉํ ๋๋ ์ฃผ๋ก ๋ฆฌ์คํธ ์ฐ๊ฒฐ ๊ธฐ๋ฅ์ ํ์ฉํฉ๋๋ค. ์ด๋ฅผ ํตํด ์๋ก์ด ์ํ ์ ๋ฐ์ดํธ๋ฅผ ๊ธฐ์กด ๋ฆฌ์คํธ์ ์ถ๊ฐํ ์ ์์ต๋๋ค.
โ operator.add
- Python์ ๋ด์ฅ
operator ๋ชจ๋
์์ ์ ๊ณตํ๋ ์ผ๋ฐ์ ์ธ ๋ง์ ์ฐ์ฐ ํจ์์ ๋๋ค. ํ์ง๋ง ์ด ํจ์๋ ๋จ์ํ ์ซ์๋ฅผ ๋ํ๋ ๊ฒ ์ด์์ ๊ธฐ๋ฅ์ ํฉ๋๋ค:- ์ซ์ ๋ง์
:
- ๋ ์ซ์๋ฅผ ๋ํฉ๋๋ค. ์:
operator.add(1, 2)
๋3
์ ๋ฐํํฉ๋๋ค.
- ๋ ์ซ์๋ฅผ ๋ํฉ๋๋ค. ์:
- ๋ฌธ์์ด ์ฐ๊ฒฐ:
- ๋ ๋ฌธ์์ด์ ์ฐ๊ฒฐํฉ๋๋ค. ์:
operator.add("Hello", "World")
๋"HelloWorld"
๋ฅผ ๋ฐํํฉ๋๋ค.
- ๋ ๋ฌธ์์ด์ ์ฐ๊ฒฐํฉ๋๋ค. ์:
- ๋ฆฌ์คํธ ์ฐ๊ฒฐ:
- ๋ ๋ฆฌ์คํธ๋ฅผ ์ฐ๊ฒฐํฉ๋๋ค. ์:
operator.add([1, 2], [3, 4])
๋[1, 2, 3, 4]
๋ฅผ ๋ฐํํฉ๋๋ค.
- ๋ ๋ฆฌ์คํธ๋ฅผ ์ฐ๊ฒฐํฉ๋๋ค. ์:
- ์ซ์ ๋ง์
:
1
- Python์ ๋ด์ฅ
-
-
๊ทธ๋ ๋ค๋ฉด ์ด์ ๊ธฐ๋ณธ Reducer๋ฅผ ์ฌ์ฉํ ์ฝ๋๋ก ์ ๋ฐ์ดํธ๋ฅผ ์ํํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
from operator import add from typing import Annotated class State(TypedDict): foo: Annotated[list[int], add] def node_1(state): print("---Node 1---") return {"foo": [state['foo'][-1] + 1]} def node_2(state): print("---Node 2---") return {"foo": [state['foo'][-1] + 1]} def node_3(state): print("---Node 3---") return {"foo": [state['foo'][-1] + 1]} # Build graph builder = StateGraph(State) builder.add_node("node_1", node_1) builder.add_node("node_2", node_2) builder.add_node("node_3", node_3) # Logic builder.add_edge(START, "node_1") builder.add_edge("node_1", "node_2") builder.add_edge("node_1", "node_3") builder.add_edge("node_2", END) builder.add_edge("node_3", END) # Add graph = builder.compile() # View display(Image(graph.get_graph().draw_mermaid_png()))
-
์ฝ๋๋ฅผ ๋ณ๊ฒฝํ๋๋ ์๋ฌ ์์ด ๋์๊ฐ๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค. ํ์ง๋ง ๊ทธ๋ํ์ ๋ ธ๋๋ค์ด None ๊ฐ์ ์ฒ๋ฆฌํ๋๋ก ์ค๊ณ๋์ง ์์๋ค๋ฉด, TypeError๊ฐ ๋ฐ์ํ ๊ฐ๋ฅ์ฑ์ด ๋์ต๋๋ค.
-
์ด๋ ์ฌ์ฉ๋๋ ๊ฒ์ด ๋ฐ๋ก ์ปค์คํ Reducer ํจ์์ ๋๋ค.
- ์ด reduce_list ํจ์๋ ์ฌ์ฉ์ ์ ์ ๋ฆฌ๋์(custom reducer)๋ก, ๋ ๋ฆฌ์คํธ๋ฅผ ์์ ํ๊ฒ ๊ฒฐํฉํ๋ ์ญํ ์ ํฉ๋๋ค.
- ์ด ํจ์์ ์ฃผ์ ํน์ง๊ณผ None ์ฒ๋ฆฌ ๋ฐฉ์์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค:
- ๋ ๊ฐ์ ๋ฆฌ์คํธ๋ฅผ ์ ๋ ฅ๋ฐ์ ํ๋์ ๋ฆฌ์คํธ๋ก ๊ฒฐํฉํฉ๋๋ค.
- None ๊ฐ์ ํฌํจํ ๋ค์ํ ์ ๋ ฅ ์ํฉ์ ์์ ํ๊ฒ ์ฒ๋ฆฌํฉ๋๋ค.
- ์ด ํจ์์ ์ฃผ์ ํน์ง๊ณผ None ์ฒ๋ฆฌ ๋ฐฉ์์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def reduce_list(left: list | None, right: list | None) -> list:
"""Safely combine two lists, handling cases where either or both inputs might be None.
Args:
left (list | None): The first list to combine, or None.
right (list | None): The second list to combine, or None.
Returns:
list: A new list containing all elements from both input lists.
If an input is None, it's treated as an empty list.
"""
if not left:
left = []
if not right:
right = []
return left + right
-
์์ ๊ฐ์ด ๊ตฌํํจ์ผ๋ก์จ None์ด ๋ค์ด์๋ ์ ์์ ์ผ๋ก ๊ฐ์ ๋ฐํํ๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
SUMMARY
- ๊ธฐ๋ณธ Reducer: ์ํ๋ฅผ ๋ฎ์ด์ฐ๋ ๋ฐฉ์์ ๋๋ค. ์๋ฅผ ๋ค์ด, ๊ทธ๋ํ์ ๋ ๋ ธ๋๊ฐ ๋์ผํ ํค๋ฅผ ๋์์ ์ ๋ฐ์ดํธํ๋ฉด, ๋ง์ง๋ง ์ ๋ฐ์ดํธ๋ ๊ฐ์ด ์ต์ข ๊ฐ์ผ๋ก ์ ์ฅ๋ฉ๋๋ค.
- ์ปค์คํ Reducer: ์ํ๋ฅผ ๋ฎ์ด์ฐ์ง ์๊ณ ๊ฐ์ ๋ณํฉํ๊ฑฐ๋ ๋ฆฌ์คํธ์ ์ถ๊ฐํ๋ ๋ฐฉ์์ผ๋ก ๋์ํฉ๋๋ค. ์๋ฅผ ๋ค์ด, ํค๊ฐ ๋ฆฌ์คํธ๋ก ์ ์๋ ๊ฒฝ์ฐ Reducer๋ฅผ ์ฌ์ฉํด ์๋ก์ด ๊ฐ์ ๋ฆฌ์คํธ์ ์ถ๊ฐํ ์ ์์ต๋๋ค.
์๋ฌ ์ฒ๋ฆฌ
: ์ํ ์ ๋ฐ์ดํธ ์ ๊ฐ์ด None๊ณผ ๊ฐ์ ์ ํจํ์ง ์์ ๊ฐ์ด ์ ๋ ฅ๋ ์ ์์ต๋๋ค. ์ด๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํด ์ปค์คํ Reducer๋ฅผ ์ ์ํ์ฌ ์ด๋ฌํ ๊ฒฝ์ฐ์๋ ์์ ํ๊ฒ ๊ฐ์ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค.
Messages
-
CustomMessagesState
: TypedDict๋ฅผ ์์๋ฐ์ ์ปค์คํ ๋์ ๋๋ฆฌ ํ์ ์ ์ ์ํฉ๋๋ค.messages ํค๋ฅผ ๋ช ์์ ์ผ๋ก ์ ์ํ๊ณ , Annotated๋ฅผ ์ฌ์ฉํ์ฌ add_messages ๋ฆฌ๋์๋ฅผ ์ฐ๊ฒฐํฉ๋๋ค.
- ์ถ๊ฐ์ ์ธ ํค๋ค(added_key_1, added_key_2 ๋ฑ)์ ์ง์ ์ ์ํฉ๋๋ค.
-
ExtendedMessagesState
: MessagesState๋ฅผ ์์๋ฐ์ต๋๋ค. ์ด ํด๋์ค๋ ์ด๋ฏธ messages ํค์ add_messages ๋ฆฌ๋์๊ฐ ๊ตฌํ๋์ด ์์ต๋๋ค.- ์ถ๊ฐ์ ์ธ ํค๋ค(added_key_1, added_key_2 ๋ฑ)๋ง ์ ์ํฉ๋๋ค.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from typing import Annotated
from langgraph.graph import MessagesState
from langchain_core.messages import AnyMessage
from langgraph.graph.message import add_messages
# Define a custom TypedDict that includes a list of messages with add_messages reducer
class CustomMessagesState(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]
added_key_1: str
added_key_2: str
# etc
# Use MessagesState, which includes the messages key with add_messages reducer
class ExtendedMessagesState(MessagesState):
# Add any keys needed beyond messages, which is pre-built
added_key_1: str
added_key_2: str
# etc
MessagesState
์ ๋ด์ฅ๋messages
key๊ฐ ์๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.CustomMessagesState
๋ ๋ณ๋๋กmessages
key๋ฅผ ์ ์ํด์ค์ผ ํฉ๋๋ค.
LangGraph์์๋ ๋ฉ์์ง๋ฅผ ํจ๊ณผ์ ์ผ๋ก ์ฒ๋ฆฌ/๊ด๋ฆฌํ๋ ๋ฐฉ๋ฒ
LangGraph์์๋ ๋ฉ์์ง๋ฅผ ํจ๊ณผ์ ์ผ๋ก ์ฒ๋ฆฌ/๊ด๋ฆฌํ๊ธฐ ์ํ
๋ ๊ฐ์ง ์ฃผ์ ๋ฐฉ๋ฒ
์ ์ ๊ณตํฉ๋๋ค.
add_messages ๋ฆฌ๋์
: add_messages ๋ฆฌ๋์๋ ์ํ์ messages ํค์ ์ ๋ฉ์์ง๋ฅผ ์ถ๊ฐํ๋ ๊ธฐ๋ฅ์ ํฉ๋๋ค. ์ด๋ฅผ ์ฌ์ฉํ๋ฉด ๋ฉ์์ง ๋ชฉ๋ก์ ์ฝ๊ฒ ์ ๋ฐ์ดํธํ ์ ์์ต๋๋ค.MessagesState ํด๋์ค
: MessagesState๋ messages ํค์ add_messages ๋ฆฌ๋์๊ฐ ๋ฏธ๋ฆฌ ๊ตฌํ๋ ํธ๋ฆฌํ ํด๋์ค์ ๋๋ค. ์ด๋ฅผ ์์ํ์ฌ ์ถ๊ฐ ํค๋ฅผ ํฌํจํ๋ ์ปค์คํ ์ํ ํด๋์ค๋ฅผ ์ฝ๊ฒ ๋ง๋ค ์ ์์ต๋๋ค.
add_messages ๋ฆฌ๋์์ ๋ค์ํ ๊ธฐ๋ฅ
- ๋ฉ์์ง ๋ฎ์ด์ฐ๊ธฐ ๊ธฐ๋ฅ
- ๋์ผํ ID๋ฅผ ๊ฐ์ง ๋ฉ์์ง๋ฅผ ์ถ๊ฐํ๋ฉด ๊ธฐ์กด ๋ฉ์์ง๋ฅผ ๋ฎ์ด์๋๋ค.
- ์ด๋ฅผ ํตํด ํน์ ๋ฉ์์ง๋ฅผ ์ ๋ฐ์ดํธํ ์ ์์ต๋๋ค.
1 2 3 4 5 6 7 8 # Initial state initial_messages = [AIMessage(content="Hello! How can I assist you?", name="Model", id="1"), HumanMessage(content="I'm looking for information on marine biology.", name="HUMAN", id="2") ] # New message to add new_message = HumanMessage(content="I'm looking for information on whales, specifically", name="HUMAN", id="2") # Test add_messages(initial_messages , new_message)
- ๋ฉ์์ง ์ ๊ฑฐ ๊ธฐ๋ฅ
- RemoveMessage ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํ์ฌ ํน์ ID์ ๋ฉ์์ง๋ฅผ ์ ๊ฑฐํ ์ ์์ต๋๋ค1
- ์ด ๊ธฐ๋ฅ์ ๋ ์ด์ ํ์ํ์ง ์์ ๋ฉ์์ง๋ฅผ ์ํ์์ ์ ๊ฑฐํ ๋ ์ ์ฉํฉ๋๋ค.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # Message list messages = [AIMessage("Hello how can I help you.", name="MODEL", id="1")] messages.append(HumanMessage("Hi. I am researching ocean mammals", name="HUMAN", id="2")) messages.append(AIMessage("That's great! Ocean mammals, also known as marine mammals. How can I help you?", name="MODEL", id="3")) messages.append(HumanMessage("Yes, I know about whales. But what others should I learn about?", name="HUMAN", id="4")) print("ORIGINAL MESSAGES:") print(messages) print('---------------') # Isolate messages to delete delete_messages = [RemoveMessage(id=m.id) for m in messages[:-2]] print("DELETED MESSAGES:") print(delete_messages) print('---------------') # Apply the deletion updated_messages = add_messages(messages, delete_messages) print("UPDATED MESSAGES:") print(updated_messages) print('---------------')
Lesson 3: Multiple Schemas
-
Langraph์์ ๊ทธ๋ํ์ ๋ ธ๋ ๊ฐ์๋ ์ฌ๋ฌ ์คํค๋ง๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค.
- ์ด๋
์ ๋ ฅ(Input)
์คํค๋ง์์ถ๋ ฅ(Output)
์คํค๋ง๋ฅผ ๊ตฌ๋ถํ์ฌ ๊ทธ๋ํ ๋ด์์ ๊ฐ๊ธฐ ๋ค๋ฅธ ํ์์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ์ฒ๋ฆฌํ ์ ์๊ฒ ํฉ๋๋ค. - ์ด๋ฌํ ๋ค์ค ์คํค๋ง ์ ๊ทผ ๋ฐฉ์์ ๋ณต์กํ ์ํฌํ๋ก์ฐ๋ฅผ ๋ ํจ๊ณผ์ ์ผ๋ก ๊ด๋ฆฌํ๊ณ , ๋ฐ์ดํฐ์ ํ๋ฆ์ ์ ๋ฐํ๊ฒ ์ ์ดํ ์ ์๊ฒ ํด์ค๋๋ค.
- ์ด๋
-
์ฃผ์ ์คํค๋ง ์ ํ
:InputState (์ ๋ ฅ ์คํค๋ง)
: ๊ทธ๋ํ์ ์ ๋ฌ๋๋ ์ด๊ธฐ ๋ฐ์ดํฐ ๊ตฌ์กฐ๋ฅผ ์ ์ํฉ๋๋ค.OutputState (์ถ๋ ฅ ์คํค๋ง)
: ๊ทธ๋ํ๊ฐ ์ต์ข ์ ์ผ๋ก ๋ฐํํ๋ ๋ฐ์ดํฐ ๊ตฌ์กฐ๋ฅผ ์ ์ํฉ๋๋ค.OverallState (์ ์ฒด ์ํ ์คํค๋ง)
: ๊ทธ๋ํ ๋ด๋ถ์์ ์ฌ์ฉ๋๋ ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ํฌํจํ๋ ํฌ๊ด์ ์ธ ์คํค๋ง์ ๋๋ค.PrivateState (๋น๊ณต๊ฐ ์ํ ์คํค๋ง)
: ํน์ ๋ ธ๋ ๊ฐ์๋ง ๊ณต์ ๋๋ ์์ ๋๋ ์ค๊ฐ ๋ฐ์ดํฐ๋ฅผ ์ ์ํฉ๋๋ค.User-defined schema (์ฌ์ฉ์ ์ ์ ์คํค๋ง)
: ์ฌ์ฉ์๊ฐ ํน์ ์๊ตฌ์ฌํญ์ ๋ง์ถฐ ์ง์ ์ ์ํ๋ ์คํค๋ง์ ๋๋ค
๐ค (์ถ๊ฐ) ์๋ฅผ ๋ค์ด, ๊ณ ๊ฐ ์๋น์ค ์ฑ๋ด์ ๋ง๋ ๋ค๊ณ ๊ฐ์ ํด ๋ณด๊ฒ ์ต๋๋ค.
๐ค ์ด ์ฑ๋ด์ ๊ณ ๊ฐ์ ์ง๋ฌธ์ ๋ฐ์ ์ ์ ํ ๋ต๋ณ์ ์ ๊ณตํ๊ณ , ํ์ํ ๊ฒฝ์ฐ ๋ด๋ถ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ๊ฒ์ํฉ๋๋ค. ์ด ๊ฒฝ์ฐ ๋ค์๊ณผ ๊ฐ์ ์ฌ๋ฌ ์คํค๋ง๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค:
-
InputState (์ ๋ ฅ ์คํค๋ง): ๊ทธ๋ํ์ ์ ๋ฌ๋๋ ์ด๊ธฐ ๋ฐ์ดํฐ ๊ตฌ์กฐ๋ฅผ ์ ์
1 2 3
class InputState(TypedDict): customer_query: str customer_id: str
-
OutputState (์ถ๋ ฅ ์คํค๋ง): ๊ทธ๋ํ๊ฐ ์ต์ข ์ ์ผ๋ก ๋ฐํํ๋ ๋ฐ์ดํฐ ๊ตฌ์กฐ๋ฅผ ์ ์
1 2 3
class OutputState(TypedDict): response: str satisfaction_score: int
-
OverallState (์ ์ฒด ์ํ ์คํค๋ง): ๊ทธ๋ํ ๋ด๋ถ์์ ์ฌ์ฉ๋๋ ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ํฌํจํ๋ ํฌ๊ด์ ์ธ ์คํค๋ง
1 2 3 4 5 6 7
class OverallState(TypedDict): customer_query: str customer_id: str response: str satisfaction_score: int internal_notes: str database_results: dict
-
PrivateState (๋น๊ณต๊ฐ ์ํ ์คํค๋ง): ํน์ ๋ ธ๋ ๊ฐ์๋ง ๊ณต์ ๋๋ ์์ ๋๋ ์ค๊ฐ ๋ฐ์ดํฐ๋ฅผ ์ ์
1 2 3
class PrivateState(TypedDict): sentiment_analysis: str priority_level: int
-
์ด์ ์ด ์คํค๋ง๋ค์ ์ฌ์ฉํ๋ ๊ทธ๋ํ๋ฅผ ๊ตฌ์ฑํด ๋ณด๊ฒ ์ต๋๋ค:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
from langgraph.graph import StateGraph, START, END def query_analyzer(state: InputState) -> PrivateState: # ๊ณ ๊ฐ ์ง๋ฌธ ๋ถ์ return {"sentiment_analysis": "positive", "priority_level": 2} def database_searcher(state: OverallState) -> OverallState: # ๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ฒ์ return {"database_results": {"product_info": "..."}} def response_generator(state: OverallState) -> OutputState: # ์๋ต ์์ฑ return {"response": "Here's the information you requested...", "satisfaction_score": 8} graph = StateGraph(OverallState, input=InputState, output=OutputState) graph.add_node("query_analyzer", query_analyzer) graph.add_node("database_searcher", database_searcher) graph.add_node("response_generator", response_generator) graph.add_edge(START, "query_analyzer") graph.add_edge("query_analyzer", "database_searcher") graph.add_edge("database_searcher", "response_generator") graph.add_edge("response_generator", END) compiled_graph = graph.compile()
์ด ์์์์:
- ๊ทธ๋ํ๋
InputState
๋ก ์์ํ์ฌ ๊ณ ๊ฐ์ ์ง๋ฌธ์ ๋ฐ์ต๋๋ค. query_analyzer
๋ ธ๋๋PrivateState
๋ฅผ ์์ฑํ์ฌ ๋ด๋ถ์ ์ผ๋ก ์ฌ์ฉํฉ๋๋ค.database_searcher
์response_generator
๋ ธ๋๋OverallState
๋ฅผ ์ฌ์ฉํ์ฌ ์์ ํฉ๋๋ค.- ์ต์ข
์ ์ผ๋ก ๊ทธ๋ํ๋
OutputState
๋ฅผ ๋ฐํํ์ฌ ๊ณ ๊ฐ์๊ฒ ์๋ต์ ์ ๊ณตํฉ๋๋ค.
์ด๋ ๊ฒ ํจ์ผ๋ก์จ:
- ์ ๋ ฅ๊ณผ ์ถ๋ ฅ์ ๋ช ํํ ์ ํํ ์ ์์ต๋๋ค.
- ๋ด๋ถ ์์ ์ ํ์ํ ์ถ๊ฐ ๋ฐ์ดํฐ๋ฅผ ๊ด๋ฆฌํ ์ ์์ต๋๋ค.
- ํน์ ๋ ธ๋ ๊ฐ์๋ง ๊ณต์ ๋๋ ๋น๊ณต๊ฐ ๋ฐ์ดํฐ๋ฅผ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค.
์ด ๊ตฌ์กฐ๋ฅผ ํตํด ๋ณต์กํ ์ํฌํ๋ก์ฐ๋ฅผ ๊ด๋ฆฌํ๋ฉด์๋ ๊ฐ ๋จ๊ณ์์ ํ์ํ ์ ๋ณด๋ง์ ๋ ธ์ถํ๊ณ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค.
๊ฐ๊ฐ์ State๋ณ๋ก ์์ธํ๊ฒ ์ดํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค:
PrivateState ์์
- OverallState (์ ์ฒด ์ํ ์คํค๋ง): ๊ทธ๋ํ ๋ด๋ถ์์ ์ฌ์ฉ๋๋ ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ํฌํจํ๋ ํฌ๊ด์ ์ธ ๋ฐ์ดํฐ ๊ตฌ์กฐ๋ฅผ ์ ์ํฉ๋๋ค.
- PrivateState (๋น๊ณต๊ฐ ์ํ ์คํค๋ง): ํน์ ๋
ธ๋ ๊ฐ์๋ง ๊ณต์ ๋๋ ์์ ๋๋ ์ค๊ฐ ๋ฐ์ดํฐ๋ฅผ ์ ์ํฉ๋๋ค.
- ํน์ ๋ ธ๋ ๊ฐ์๋ง ๊ณต์ ๋๋ ์์ ๋๋ ์ค๊ฐ ๋ฐ์ดํฐ๋ฅผ ์ ์ํ ๋ ์ฌ์ฉํฉ๋๋ค.
- ์๋ ์ฝ๋์์๋ ์ค๊ฐ์ ์๋ Private ๋ ธ๋์์ ์ฌ์ฉ๋ State๋ ์ธ๋ถ๋ก ๋๋ฌ๋์ง ์๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from typing_extensions import TypedDict
from IPython.display import Image, display
from langgraph.graph import StateGraph, START, END
class OverallState(TypedDict):
foo: int
class PrivateState(TypedDict):
baz: int
def node_1(state: OverallState) -> PrivateState:
print("---Node 1---")
return {"baz": state['foo'] + 1}
def node_2(state: PrivateState) -> OverallState:
print("---Node 2---")
return {"foo": state['baz'] + 1}
# Build graph
builder = StateGraph(OverallState)
builder.add_node("node_1", node_1)
builder.add_node("node_2", node_2)
# Logic
builder.add_edge(START, "node_1")
builder.add_edge("node_1", "node_2")
builder.add_edge("node_2", END)
# Add
graph = builder.compile()
# View
display(Image(graph.get_graph().draw_mermaid_png()))
baz
is only included inPrivateState
.node_2
usesPrivateState
as input, but writes out toOverallState
.
Input / Output Schema
- InputState (์ ๋ ฅ ์คํค๋ง, ์ง๋ฌธ๋ง ํฌํจ): ๊ทธ๋ํ์ ์ ๋ฌ๋๋ ์ด๊ธฐ ๋ฐ์ดํฐ ๊ตฌ์กฐ๋ฅผ ์ ์ํฉ๋๋ค.
- OutputState (์ถ๋ ฅ ์คํค๋ง, ๋ต๋ณ๋ง ํฌํจ): ๊ทธ๋ํ๊ฐ ์ต์ข ์ ์ผ๋ก ๋ฐํํ๋ ๋ฐ์ดํฐ ๊ตฌ์กฐ๋ฅผ ์ ์ํฉ๋๋ค.
- OverallState (์ ์ฒด ์ํ ์คํค๋ง, ์ง๋ฌธ/๋ต๋ณ/๋ ธํธ๋ฅผ ๋ชจ๋ ํฌํจ): ๊ทธ๋ํ ๋ด๋ถ์์ ์ฌ์ฉ๋๋ ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ํฌํจํ๋ ํฌ๊ด์ ์ธ ๋ฐ์ดํฐ ๊ตฌ์กฐ๋ฅผ ์ ์ํฉ๋๋ค.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class InputState(TypedDict):
question: str
class OutputState(TypedDict):
answer: str
class OverallState(TypedDict):
question: str
answer: str
notes: str
def thinking_node(state: InputState):
return {"answer": "bye", "notes": "... his is name is Chung"}
def answer_node(state: OverallState) -> OutputState:
return {"answer": "bye Chung"}
graph = StateGraph(OverallState, input=InputState, output=OutputState)
graph.add_node("answer_node", answer_node)
graph.add_node("thinking_node", thinking_node)
graph.add_edge(START, "thinking_node")
graph.add_edge("thinking_node", "answer_node")
graph.add_edge("answer_node", END)
graph = graph.compile()
# View
display(Image(graph.get_graph().draw_mermaid_png()))
graph.invoke({"question":"hi"})
Lesson 4: Trim and Filter Messages
-
๋ณต์ต:
-
from langchain_core.messages import AIMessage, HumanMessage
: ์ฐ๋ฆฌ๋ Langchain์ Messages ๊ธฐ๋ฅ์ ์ฌ์ฉํ์ฌ AIMessage, HumanMessage๋ฅผ ์ ์ํ๊ณ ์ด๋ฅผ LLM์ ์ ๋ฌํด์ค ์ ์์ต๋๋ค.1 2 3 4 5 6
from langchain_core.messages import AIMessage, HumanMessage messages = [AIMessage(f"๋น์ ์ด ํด์ ํฌ์ ๋ฅ๋ฅผ ์ฐ๊ตฌํ๊ณ ์๋ค๊ณ ํ์ จ๋์?", name="AI")] messages.append(HumanMessage(f"๋ค, ์ ๋ ๊ณ ๋์ ๋ํด ์๊ณ ์์ด์. ํ์ง๋ง ๋ค๋ฅธ ์ด๋ค ๋๋ฌผ์ ๋ํด ๋ฐฐ์์ผ ํ ๊น์?", name="HUMAN")) for m in messages: m.pretty_print()
-
์ด์ ์ ๋๋ด๋ ๋ํ๋ค์ ๋ฐํ์ผ๋ก LLM์ด ๋ต๋ณ์ ํ๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
1 2 3 4 5 6
from langchain_openai import ChatOpenAI llm = ChatOpenAI(model="gpt-4o") messages.append(llm.invoke(messages)) for m in messages: m.pretty_print()
-
์ด๋ฌํ ๊ธฐ๋ฅ์
from langgraph.graph import MessagesState
์ ์ ์๋์ด ์์ต๋๋ค.- โจ ์์์ ๋ค๋ค๋ ๊ฒ์ฒ๋ผ
MessagesState
์๋ ๋ด์ฅ๋ โmessagesโ ํค๊ฐ ์กด์ฌํฉ๋๋ค. ๋ํ ์ด ํค์๋ ๋ด์ฅ๋add_messages
๋ฆฌ๋์๊ฐ ์กด์ฌํฉ๋๋ค.1 2 3 4
from typing import Annotated from langgraph.graph import MessagesState from langchain_core.messages import AnyMessage from langgraph.graph.message import add_messages
1 2 3 4 5 6
# add_messages ๋ฆฌ๋์๋ก ๋ฉ์์ง ๋ฆฌ์คํธ๋ฅผ ํฌํจํ๋ ์ฌ์ฉ์ ์ ์ TypedDict ์ ์ class CustomMessagesState(TypedDict): messages: Annotated[list[AnyMessage], add_messages] added_key_1: str added_key_2: str # ๋ฑ
1 2 3 4 5 6
# MessagesState ์ฌ์ฉ, ์ด๋ฏธ add_messages ๋ฆฌ๋์๊ฐ ์๋ messages ํค ํฌํจ class ExtendedMessagesState(MessagesState): # messages ์ธ์ ํ์ํ ํค ์ถ๊ฐ, messages๋ ๋ฏธ๋ฆฌ ๊ตฌ์ถ๋์ด ์์ added_key_1: str added_key_2: str # etc
- โจ ์์์ ๋ค๋ค๋ ๊ฒ์ฒ๋ผ
messages ํค
: AnyMessage ๊ฐ์ฒด์ ๋ฆฌ์คํธ๋ฅผ ์ ์ฅํฉ๋๋ค. (list[AnyMessage
])AnyMessage
๋HumanMessage
,AIMessage
,SystemMessage
๋ฑ ๋ค์ํ ๋ฉ์์ง ํ์ ์ ํฌํจํ ์ ์์ต๋๋คadd_messages
๋ฆฌ๋์๊ฐ ๊ธฐ๋ณธ์ผ๋ก ํฌํจ๋์ด ์์ต๋๋ค. ์ด ๋ฆฌ๋์๋ ์ ๋ฉ์์ง๋ฅผ ๊ธฐ์กด ๋ฉ์์ง ๋ฆฌ์คํธ์ ์ถ๊ฐํฉ๋๋ค.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
from IPython.display import Image, display from langgraph.graph import MessagesState from langgraph.graph import StateGraph, START, END # Node def chat_model_node(state: MessagesState): return {"messages": llm.invoke(state["messages"])} # Build graph builder = StateGraph(MessagesState) builder.add_node("chat_model", chat_model_node) builder.add_edge(START, "chat_model") builder.add_edge("chat_model", END) graph = builder.compile() # View display(Image(graph.get_graph().draw_mermaid_png()))
(์ฐธ๊ณ )
add_messages
๋ฆฌ๋์ Output ์์-
add_messages
๋ฅผ ์ฌ์ฉํ๋ฉด ๊ทธ๋ฅstr
์ผ๋ก ๋ฉ์ธ์ง๋ฅผ ์ ๋ฌํด์ค๋HumanMessage
๋ก ์ ๋ฌ๋๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค. ๋ฐ๋จ์ ๊ทธ๋ฅappend
๋ฅผ ํ ๊ฒฝ์ฐ, ์ผ๋ฐlist append
๊ฐ ์ํ๋๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค. -
๊ทธ๋ ๋ค๋ฉด append๋ก
HumanMessage
๋AIMessage
๋ฅผ ๋ฃ์ด์ค ์ ์๋ ๋ฐฉ๋ฒ์ ์์๊น์? ๋ฌผ๋ก ์์ต๋๋ค!
-
-
์ฅ๊ธฐ ๋ํ์์ ๋ฉ์์ง ํ์คํ ๋ฆฌ๊ฐ ๊ธธ์ด์ง ๊ฒฝ์ฐ, ํ ํฐ ์ฌ์ฉ๋์ด ๊ธ์ฆํ ์ ์์ต๋๋ค.
-
์ด๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํด Langraph์์๋ ๋ฉ์์ง ํ์คํ ๋ฆฌ๋ฅผ ๊ด๋ฆฌํ๋ ์๋์ ๊ฐ์ ๋ฐฉ๋ฒ๋ค์ ์ ๊ณตํฉ๋๋ค. ๊ฐ ๊ธฐ๋ฒ์ ๋ํด์ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
-
ํํฐ๋ง: ๋ฉ์์ง ํ์คํ ๋ฆฌ์์ ํ์ํ ์ต๊ทผ ๋ฉ์์ง๋ง ๋จ๊ธฐ๊ณ ์ด์ ๋ฉ์์ง๋ฅผ ์ญ์ ํ๋ ๋ฐฉ์์ ๋๋ค.
- (๋ณต์ต) ์ด์ ์๊ฐ์ ๋ฐฐ์ด
remove_messages
๋ฆฌ๋์ ํจ์๋ ํน์ ๋ฉ์์ง ID๋ฅผ ๊ธฐ์ค์ผ๋ก ๋ฉ์์ง๋ฅผ ์ญ์ ํ๋ ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค. -> ์ด๋ ๊ทธ๋ํ state์์๋ ํด๋น ์ ๋ณด๊ฐ ์ฌ๋ผ์ง๊ฒ ๋ฉ๋๋ค - ํ์ง๋ง ์ด๋ฒ์ ๋ฐฐ์ธ
ํํฐ๋ง
์ ๊ฐ๋ ์ ์ด์๋ ์กฐ๊ธ ๋ค๋ฅธ ๊ฐ๋ ์ ๋๋ค. ์๋ํ๋ฉด ๋ชจ๋ธ์ ๋ค์ด๊ฐ๊ฒ๋๋ ์ ๋ณด๋ ํ์ ์ ์ด์ง๋ง, ๊ทธ๋ํ์ state์๋ ๋ชจ๋ ์ ๋ณด๊ฐ ๋ค์ด๊ฐ์์ ๊ฒ์ด๊ธฐ ๋๋ฌธ์ ๋๋ค. ```
Node
def chat_model_node(state: MessagesState): return {โmessagesโ: [llm.invoke(state[โmessagesโ][-1:])]} ```
-
์๋ ๊ทธ๋ฆผ ๋ณด๋ฉด message์๋ ๋ชจ๋ ์ ๋ณด๊ฐ ๋ค ๋ด๊ฒจ ์๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
-
๋ฐ๋๋ก ์ค์ llm์ ๋์ด๊ฐ ์ง์๋ฌธ์ โํด๋ง์ ๋ํด์ ์๋ ค์๋ ค์คโ๋ผ๋ ์ง๋ฌธ๋ง์ด ๋์ด๊ฐ ๊ฒ์ ๋ณผ ์ ์์ต๋๋ค.
- (๋ณต์ต) ์ด์ ์๊ฐ์ ๋ฐฐ์ด
-
ํธ๋ฆฌ๋ฐ: ๋ฉ์์ง๋ฅผ ํ ํฐ ์ ๊ธฐ์ค์ผ๋ก ์๋ผ๋ด๋ ๋ฐฉ๋ฒ(
trim_messages
)์ ๋๋ค. ์์ํํฐ
๋ง์ด ์์ด์ ํธ ๊ฐ์ ๋ฉ์์ง์ ์ฌํ ํ์ ์งํฉ๋ง ๋ฐํํ๋ ๋ฐ๋ฉด,ํธ๋ฆฌ๋ฐ
์ ์ฑํ ๋ชจ๋ธ์ด ์๋ตํ๋ ๋ฐ ์ฌ์ฉํ ์ ์๋ ํ ํฐ ์๋ฅผ ์ ํํฉ๋๋ค.1 2 3 4 5 6 7 8 9 10 11 12 13
from langchain_core.messages import trim_messages # Node def chat_model_node(state: MessagesState): messages = trim_messages( state["messages"], max_tokens=100, strategy="last", token_counter=ChatOpenAI(model="gpt-4o"), allow_partial=False, ) return {"messages": [llm.invoke(messages)]}
state["messages"]
: ํธ๋ฆฌ๋ฐํ ๋ฉ์์ง ๋ชฉ๋ก์ ๋๋ค.max_tokens=100
: ๊ฒฐ๊ณผ ๋ฉ์์ง์ ์ต๋ ํ ํฐ ์๋ฅผ 100์ผ๋ก ์ ํํฉ๋๋ค.strategy="last"
: ๋ง์ง๋ง ๋ฉ์์ง๋ถํฐ ์์ํด ์ญ์์ผ๋ก ๋ฉ์์ง๋ฅผ ํฌํจ์ํต๋๋ค.token_counter=ChatOpenAI(model="gpt-4")
: ํ ํฐ ์๋ฅผ ๊ณ์ฐํ๋ ๋ฐ ์ฌ์ฉํ ๋ชจ๋ธ์ ์ง์ ํฉ๋๋ค.-
allow_partial=False
: ๋ฉ์์ง๋ฅผ ๋ถ๋ถ์ ์ผ๋ก ์๋ฅด๋ ๊ฒ์ ํ์ฉํ์ง ์์ต๋๋ค. -
์ฌ๊ธฐ๋ ์์ ๋ง์ฐฌ๊ฐ์ง๋ก LLM ๋ชจ๋ธ์ ๋ค์ด๊ฐ๋ ๊ฒ์ token ์กฐ๊ฑด์ ํด๋นํ๋ ์ง์๋ฌธ๋ง ๋์ด๊ฐ๊ฒ ๋ฉ๋๋ค.
-
๊ทธ๋ฆฌ๊ณ langraph ์์๋ ์ ์ฒด์ message history๋ ๊ทธ๋๋ก ์ ์ง๋๊ฒ ๋ฉ๋๋ค.
-
Lesson 5: Chatbot w/ Summarizing Messages and Memory
์ด๋ฒ ๋ด์ฉ์ ๋ํํ ์ฑ๋ด์์ ๋ํ๋ฅผ ์์ฝํ๋ ๋ฐฉ๋ฒ์ ์ค๋ช
ํฉ๋๋ค. ์์ฝ ๊ธฐ๋ฅ
์ ๋ํ๋ฅผ ์ค๋ ์ ์งํ๋ฉด์๋ ๋ฉ์์ง์ ์ ์ฒด ๊ธฐ๋ก์ ๋จ๊ธฐ์ง ์๊ณ ๋ํ๋ฅผ ์์ถํ๋ ๊ธฐ์ ๋ก, ์์์ ๋ค๋ฃฌ ํํฐ๋ง์ด๋ ๋ฉ์์ง ์ญ์ ๋ณด๋ค ๋ ๋ง์ ์ ๋ณด๋ฅผ ๋ณด์กดํ๋ ค๋ ์๋์
๋๋ค.
- ์ด ๊ธฐ๋ฅ์ LLMs(Large Language Models)๋ฅผ ์ฌ์ฉํ์ฌ ๋ํ์ ์ฃผ์ ๋ด์ฉ์ ์ค์๊ฐ์ผ๋ก ์์ฝํ๋ ๋ฐฉ๋ฒ์ ์ค๋ช ํฉ๋๋ค.
1. ๊ธฐ๋ณธ ์ค์ ๋ฐ ๋ฉ์์ง ์ํ ๊ตฌ์ฑ
message state
์๋ ๊ธฐ๋ณธ์ ์ผ๋ก ๋ฉ์์ง ๋ชฉ๋ก์ ์ ์ฅํ๋messages
ํค๊ฐ ์์ง๋ง, ์ฌ๊ธฐ์ ๋ํ ์์ฝ์ ์ ์ฅํ ์ ์๋ ์๋ก์ดsummary
ํค๋ฅผ ์ถ๊ฐํฉ๋๋ค.
1
2
3
from langgraph.graph import MessagesState
class State(MessagesState):
summary: str
call model
์ด๋ผ๋ ๋ ธ๋์์๋ ๊ธฐ์กด ์์ฝ์ด ์์ผ๋ฉด ๋ฉ์์ง ๋ชฉ๋ก์ ์ถ๊ฐํ๊ณ ๋ชจ๋ธ์ ํธ์ถํฉ๋๋ค.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from langchain_core.messages import SystemMessage, HumanMessage, RemoveMessage
# Define the logic to call the model
def call_model(state: State):
# Get summary if it exists
summary = state.get("summary", "")
# If there is summary, then we add it
if summary:
# Add summary to system message
system_message = f"Summary of conversation earlier: {summary}"
# Append summary to any newer messages
messages = [SystemMessage(content=system_message)] + state["messages"]
else:
messages = state["messages"]
response = model.invoke(messages)
return {"messages": response}
2. ์์ฝ ๋ ธ๋ ๊ตฌ์ฑ
- ๋ํ๋ฅผ ์์ฝํ๋ summarize_conversation ๋
ธ๋๋ฅผ ์ถ๊ฐํ์ฌ ๋ํ ์ํ๋ฅผ ๋ฐ์ ์์ฝ์ ์์ฑํฉ๋๋ค.
- ๋ง์ฝ ๊ธฐ์กด ์์ฝ์ด ์๋ค๋ฉด ์ด๋ฅผ ์๋ก์ด ์์ฝ์ ํฌํจํ์ฌ ์ ๋ฐ์ดํธํฉ๋๋ค.
- ๊ธฐ์กด ์์ฝ์ด ์๋ค๋ฉด ์๋ก ์์ฝ์ ์์ฑํฉ๋๋ค.
- ์์ฝ์ ์์ฑํ ํ ์ํ๋ฅผ ํํฐ๋งํ๊ธฐ ์ํด
RemoveMessage
๋ฅผ ์ฌ์ฉํ์ฌ ๊ฐ์ฅ ์ต๊ทผ ๋ฉ์์ง ๋ ๊ฐ๋ง ๋จ๊ธฐ๊ณ ๋๋จธ์ง ๋ฉ์์ง๋ฅผ ์ญ์ ํฉ๋๋ค.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def summarize_conversation(state: State):
# First, we get any existing summary
summary = state.get("summary", "")
# Create our summarization prompt
if summary:
# A summary already exists
summary_message = (
f"This is summary of the conversation to date: {summary}\n\n"
"Extend the summary by taking into account the new messages above:"
)
else:
summary_message = "Create a summary of the conversation above:"
# Add prompt to our history
messages = state["messages"] + [HumanMessage(content=summary_message)]
response = model.invoke(messages)
# Delete all but the 2 most recent messages
delete_messages = [RemoveMessage(id=m.id) for m in state["messages"][:-2]]
return {"summary": response.content, "messages": delete_messages}
3. ๋ฉ์์ง ์ญ์ ์ ์์ฝ ์ ๋ฐ์ดํธ
- ๋ํ ๊ธธ์ด์ ๋ฐ๋ผ ์์ฝ์ ์์ฑํ ์ง ๊ฒฐ์ ํ๋ ์กฐ๊ฑด๋ถ ์ฃ์ง๋ฅผ ์ถ๊ฐํฉ๋๋ค.
len(messages) > 6
: ๊ธธ์ด๊ฐ 6๋ณด๋ค ๊ธธ๋ฉด ์์ฝ์ ์ ๊ณตํฉ๋๋ค.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from langgraph.graph import END
# Determine whether to end or summarize the conversation
def should_continue(state: State):
"""Return the next node to execute."""
messages = state["messages"]
# If there are more than six messages, then we summarize the conversation
if len(messages) > 6:
return "summarize_conversation"
# Otherwise we can just end
return END
4.์ฒดํฌํฌ์ธํฐ๋ก ์ํ ์ ์ง
- ์ํ๋ ์ผ์์ ์ด๊ธฐ ๋๋ฌธ์ ๋ํ๊ฐ ๊ธธ์ด์ง๋ฉด ์ํ๋ฅผ ๋ณด์กดํ๊ธฐ ์ํด
MemorySaver
์checkpointer ๊ธฐ๋ฅ
์ ์ฌ์ฉํฉ๋๋ค. - ์ด ์ฒดํฌํฌ์ธํฐ๋ ๋ฉ๋ชจ๋ฆฌ์ ์ ์ฅ๋ ์ํ๋ฅผ ๋ณด์กดํ๋ฉฐ, ์ด๋ฅผ ํตํด ์ฌ๋ฌ ๋ฒ์ ๋ํ์์ ์ํ๋ฅผ ์ ์งํ ์ ์์ต๋๋ค.
- ์์ฝ ํ LLM์ ๋ํ๋ฅผ ์์ฒญํ๋ ๊ฒ์ด ์๋๋ผ, ๊ฒฐ๊ณผ๋ฅผ ์ป์ ํ output์ ์ฐ์ถํ๊ธฐ ์ ์ ๋์ ๋ ๋ํ๊ฐ ๋ง๋ค๋ฉด ์์ฝ์ ์ํํ๋ ํํ์ ๊ทธ๋ํ
- memory ๊ธฐ๋ฅ์ด ์์ด ๋ค์ ๋ํ๋ฅผ ์์ํ๋ ๋จ๊ณ์์ ๋์ ๋ ์์ ๋ฐ๋ผ ์์ฝ ์ฌ๋ถ ๊ฒฐ์ ํจ
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from IPython.display import Image, display
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph, START
# Define a new graph
workflow = StateGraph(State)
workflow.add_node("conversation", call_model)
workflow.add_node(summarize_conversation)
# Set the entrypoint as conversation
workflow.add_edge(START, "conversation")
workflow.add_conditional_edges("conversation", should_continue)
workflow.add_edge("summarize_conversation", END)
# Compile
memory = MemorySaver()
graph = workflow.compile(checkpointer=memory)
display(Image(graph.get_graph().draw_mermaid_png()))
Threads
์ฒดํฌํฌ์ธํฐ
๋ ๊ฐ ๋จ๊ณ์์ ์ํ๋ฅผ ์ฒดํฌํฌ์ธํธ๋ก ์ ์ฅํฉ๋๋ค. ์ด๋ ๊ฒ ์ ์ฅ๋ ์ฒดํฌํฌ์ธํธ๋ ๋ํ์ ์ค๋ ๋
๋ก ๊ทธ๋ฃนํ๋ ์ ์์ต๋๋ค.
์์
: ChatGPT์์ ์ฌ์ฉ์์ AI ์ฌ์ด์ ๋ํ๋ ์ค๋ ๋๋ก ๊ตฌ์ฑ๋ฉ๋๋ค. ๊ฐ ์ค๋ ๋๋ ํ๋์ ์ฃผ์ ๋ ๋ํ ํ๋ฆ์ ๋ํ๋ ๋๋ค.-
์ค๋ ๋์ ํน์ง
:- ๊ฐ ๋ฉ์์ง๋ ์ค๋ ๋ ๋ด์ ์ฒดํฌํฌ์ธํธ์ ๊ฐ์ต๋๋ค.
- ์๋ก์ด ๋ํ๋ฅผ ์์ํ๋ฉด ์ ์ค๋ ๋๊ฐ ์์ฑ๋ฉ๋๋ค.
- ํ๋์ ์ค๋ ๋ ์์์ ๋ํ์ ๋งฅ๋ฝ์ด ์ ์ง๋ฉ๋๋ค.
- ChatGPT ์ธํฐํ์ด์ค์์๋ ์ผ์ชฝ ์ฌ์ด๋๋ฐ์ ๊ฐ ์ค๋ ๋๊ฐ ํ์๋ฉ๋๋ค. ์ฌ์ฉ์๋ ์ด๋ฅผ ํตํด:
- ์ด์ ๋ํ ์ค๋ ๋๋ก ์ฝ๊ฒ ๋์๊ฐ ์ ์์ต๋๋ค.
- ์ฌ๋ฌ ์ฃผ์ ์ ๋ํ๋ฅผ ๋์์ ๊ด๋ฆฌํ ์ ์์ต๋๋ค.
- ํน์ ๋ํ ๋ด์ฉ์ ๋น ๋ฅด๊ฒ ์ฐพ์ ์ ์์ต๋๋ค.
Langraph์์๋ ์ด์ ๋น์ทํ ๊ฐ๋
์ผ๋ก configurable
์ ์ฌ์ฉํ์ฌ ์ค๋ ๋ ID๋ฅผ ์ค์ ํฉ๋๋ค.
์๋ ๊ทธ๋ฆผ๊ณผ ๊ฐ์ด config = {"configurable": {"thread_id": "1"}}
์ ์ธ ํ invoke ์์ config
์ผ๋ก ๋ฃ์ด์ฃผ๊ฒ ๋๋ฉด ๋ฐ๋ก๋ฐ๋ก ์คํํด๋ message์ ๊ธฐ๋ก์ด ์ ๋๋ก ๋จ๊ณ ์๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
์์ง๊น์ง 6๊ฐ์ ๋ํ๊ฐ ๋์ง ์์๊ธฐ ๋๋ฌธ์ ์์ฝ(summary)๊ฐ ์ํ๋์ง ์์ ๊ฒ์ ๋ณผ ์ ์์ต๋๋ค.
- ํ์ง๋ง ํ๋์ ๋ํ๋ฅผ ์ถ๊ฐํจ์ผ๋ก์จ 6๊ฐ๊ฐ ๋์ด์ ์์ฝ์ด ๋ฐ์ํ๊ณ
should_continue
=>summarize_conversation
์ด ๋ฐ์ํ๊ณ , ๊ฐ์ฅ ์ต๊ทผ 2๊ฐ์ ๋ํ๋ง์ด ๋จ๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
- ๋ํ, ์ด์ ๊ณผ๋ ๋ค๋ฅด๊ฒ Summary๋ ์์ฑ์ด ๋์ด์๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
๐ก (SUMMARY) ์์ ์ ์ํ ๊ตฌ์กฐ๋ ๋ค์๊ณผ ๊ฐ์ด ์๋ํฉ๋๋ค:
- ์์ฝ์ด ์์ฑ๋๋ฉด State ๊ฐ์ฒด์ summary ํ๋์ ์ ์ฅ๋ฉ๋๋ค.
- ์๋ก์ด ์ฌ์ฉ์ ๋ฉ์์ง๊ฐ ๋ค์ด์ค๋ฉด
call_model
ํจ์๊ฐ ํธ์ถ๋ฉ๋๋ค.call_model
ํจ์ ๋ด์์:
- ์ ์ฅ๋ ์์ฝ์ด ์๋์ง ํ์ธํฉ๋๋ค.
- ์์ฝ์ด ์๋ค๋ฉด, ์ด๋ฅผ ์์คํ ๋ฉ์์ง๋ก ๋ณํํฉ๋๋ค.
- ์ด ์์คํ ๋ฉ์์ง๋ฅผ ๋ํ ํ์คํ ๋ฆฌ์ ๋งจ ์์ ์ถ๊ฐํฉ๋๋ค.
- ๊ทธ ๋ค์์ ์๋ก์ด ์ฌ์ฉ์ ๋ฉ์์ง๋ฅผ ํฌํจํ ์ต๊ทผ ๋ฉ์์ง๋ค์ด ์ถ๊ฐ๋ฉ๋๋ค.
- ์ด๋ ๊ฒ ๊ตฌ์ฑ๋ ๋ฉ์์ง ๋ฆฌ์คํธ(์์ฝ + ์ต๊ทผ ๋ฉ์์ง๋ค)๊ฐ LLM์ ์ ๋ฌ๋ฉ๋๋ค.
Lesson 6: Chatbot w/ Summarizing Messages and External Memory
Langraph๋ ์ธ๋ถ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์ฐ๋
์ ํตํด ๋ ์์์ ์ธ ๋ฉ๋ชจ๋ฆฌ ์ ์ฅ ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค.
- SQL Lite ์ฌ์ฉ: Langraph๋ SQL Lite์ ๊ฐ์ ์ธ๋ถ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ง์ํ์ฌ ๋ฉ๋ชจ๋ฆฌ๋ฅผ ์๊ตฌ์ ์ผ๋ก ์ ์ฅํ ์ ์์ต๋๋ค. ์ด ๋ฐฉ์์ ์ฅ๊ธฐ ๋ํ๋ฅผ ์ง์์ ์ผ๋ก ์ ์งํ๊ณ ๊ด๋ฆฌํ ์ ์๋ ํ๊ฒฝ์ ์ ๊ณตํฉ๋๋ค.
๐ SQLite (https://www.sqlite.org/)
- SQLite๋ ๊ฐ๋ณ๊ณ ์ค์ ์ด ํ์ ์๋ ์๋ฒ๋ฆฌ์ค RDBMS๋ก, ๋ค์ํ ํ๊ฒฝ์์ ์์ฝ๊ฒ ์ฌ์ฉํ ์ ์์ต๋๋ค.
- Python์์๋ sqlite3 ๋ชจ๋์ ํตํด ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ๋ถํฐ ์ฟผ๋ฆฌ ์คํ๊น์ง ๊ฐ๋จํ๊ฒ ๊ตฌํํ ์ ์์ต๋๋ค.
- ์ ์ฒด ๋ฐ์ดํฐ๊ฐ ํ๋์ ํ์ผ์ ์ ์ฅ๋์ด ๊ด๋ฆฌ์ ๋ฐฐํฌ๊ฐ ์ฉ์ดํฉ๋๋ค.
- ํธ๋์ญ์ ๊ด๋ฆฌ์ SQL ํ์ค ์ง์์ผ๋ก ์์ ์ ์ธ ๋ฐ์ดํฐ ์ฒ๋ฆฌ๊ฐ ๊ฐ๋ฅํฉ๋๋ค.
-
LangChain์์์ SqliteSaver ์ฒดํฌํฌ์ธํฐ: ์ธ๊ธ๋ SqliteSaver ์ฒดํฌํฌ์ธํฐ๋ LangChain์์ ์ ๊ณตํ๋
์ฒดํฌํฌ์ธํธ ์ ์ฅ ๋ฐฉ์
์ค ํ๋๋ก, ๋ํ๋ ์ํ๋ฅผ SQLite ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅํ๋ ๋ฐฉ์์ ๋๋ค. ์ด๋ฅผ ํตํด ๋ํ์ ์ํ๋ ์งํ ์ํฉ์ ๋ฉ๋ชจ๋ฆฌ๋ ํ์ผ์ ์ ์ฅํ์ฌ ๋ณต๊ตฌํ ์ ์์ต๋๋ค.-
์ฒดํฌํฌ์ธํฐ ์ญํ : ์์คํ ์ด ์ค๋จ๋๊ฑฐ๋ ๋ํ๊ฐ ๊ธธ์ด์ก์ ๋, ์ํ๋ฅผ ๋ณด์กดํ๊ฑฐ๋ ๊ด๋ฆฌํ ์ ์๋๋ก ๋์ต๋๋ค. SQLite๋ ์ด๋ฌํ ์ฒดํฌํฌ์ธํฐ๋ก ์ ํฉํ ๊ฒฝ๋ํ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋๋ค. ๋ฉ๋ชจ๋ฆฌ๋ฅผ SQL Lite์ ์ ์ฅํจ์ผ๋ก์จ ์ธ์ ์ด ์ข ๋ฃ๋๋๋ผ๋ ๋ํ ๊ธฐ๋ก์ ์ ์งํ ์ ์์ต๋๋ค. ์๋ก์ด ์ธ์ ์ด ์์๋๋๋ผ๋ ์ด์ ๋ํ๋ฅผ ๋ถ๋ฌ์ ์ด์ด์ ์งํํ ์ ์์ต๋๋ค.
-
๋ฉ๋ชจ๋ฆฌ ๋ด SQLite ๋ฐ์ดํฐ๋ฒ ์ด์ค
1 2 3 4
import sqlite3 # In-memory database conn = sqlite3.connect(":memory:", check_same_thread=False)
:memory:
: SQLite๋"memory"
๋ฌธ์์ด์ ์ ๊ณตํ๋ฉด, ๋ฉ๋ชจ๋ฆฌ ๋ด์์ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์์ฑํฉ๋๋ค. ์ด๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค๊ฐ ํ๋ฐ์ฑ ๋ฉ๋ชจ๋ฆฌ(RAM)์์ ๋์ํ๋ฉฐ, ์ ํ๋ฆฌ์ผ์ด์ ์ด ์ข ๋ฃ๋๊ฑฐ๋ ์ฐ๊ฒฐ์ด ๋์ด์ง๋ฉด ๋ฐ์ดํฐ๊ฐ ์ฌ๋ผ์ง๋๋ค. ๋ฐ๋ผ์ ํ ์คํธ, ์์ ๋ฐ์ดํฐ ์ฒ๋ฆฌ, ๋น ๋ฅธ ์ฐ์ฐ์ด ํ์ํ ์ํฉ์์ ๋งค์ฐ ์ ์ฉํฉ๋๋ค.check_same_thread=False
: SQLite๋ ๊ธฐ๋ณธ์ ์ผ๋ก ๋ฉํฐ์ค๋ ๋์์์ ๋์ ์์ ์ ํ์ฉํ์ง ์์ง๋ง, ์ด ์ต์ ์ ์ค์ ํ๋ฉด ๋ค์ค ์ค๋ ๋ ํ๊ฒฝ์์๋ ํ๋์ ์ฐ๊ฒฐ์ ๊ณต์ ํ ์ ์์ต๋๋ค.
-
๋ก์ปฌ SQLite ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ ์ฅ
1 2 3 4
# ๋ก์ปฌ db์ ์ ์ฅ db_path = "state_db/example.db" conn = sqlite3.connect(db_path, check_same_thread=False)
- ๋ก์ปฌ ํ์ผ ๊ธฐ๋ฐ SQLite: ์ ์์์์๋
db_path = "state_db/example.db"
์ ๊ฐ์ด, ๋ก์ปฌ ํ์ผ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ ์ฅํ๋ ๋ฐฉ์์ ๋ณด์ฌ์ค๋๋ค. SQLite๋ ํ์ผ ๊ธฐ๋ฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ก, ํ๋์ ํ์ผ์ ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํฉ๋๋ค. ์ด ํ์ผ์ ๋ค๋ฅธ ํ๊ฒฝ์ผ๋ก ์ฝ๊ฒ ์ด๋ํ๊ฑฐ๋ ๋ฐฑ์ ํ ์ ์์ต๋๋ค.
- ๋ก์ปฌ ํ์ผ ๊ธฐ๋ฐ SQLite: ์ ์์์์๋
-
-
SQLite ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค์ ๋ฐ ์ฒดํฌํฌ์ธํฐ ์์ฑ:
1 2 3
from langgraph.checkpoint.sqlite import SqliteSaver memory = SqliteSaver(conn)
SqliteSaver
๋ฅผ ์ฌ์ฉํ์ฌ SQLite ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ฒดํฌํฌ์ธํฐ๋ก ์ค์ ํฉ๋๋ค. ์ด๋ฅผ ํตํด ๋ํ ์ํ๊ฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์๊ตฌ์ ์ผ๋ก ์ ์ฅ๋๋ฉฐ, ๋์ค์ ์ํ๋ฅผ ๋ถ๋ฌ์ฌ ์ ์์ต๋๋ค.
-
์ด์ ์ฑํฐ์์ ๋ง๋ Agent ์ฌ-์์ฑ
- (๋ณต์ต)
call_model
,summarize_conversation
,should_continue
๊ธฐ๋ฐ ๋ด์ฉ ์์ฝ ๋ฐ ์ง์๋ฅผ ์ํํ๋ LLM agent => Agent ์ค๋ช - (๋ชฉ์ ) ์ด์ ์๋ in-memory๋ก ์๋ํ์ง๋ง, ์ด๋ฒ์์ค๋ external DB์ ์ฐ๋์ ํ๊ณ ์ถ์. ```
from langchain_openai import ChatOpenAI from langchain_core.messages import SystemMessage, HumanMessage, RemoveMessage
from langgraph.graph import END from langgraph.graph import MessagesState
model = ChatOpenAI(model=โgpt-4oโ,temperature=0)
class State(MessagesState): summary: str
๋ชจ๋ธ์ ํธ์ถํ๋ ๋ก์ง ์ ์
def call_model(state: State):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
# ์์ฝ์ด ์๋ค๋ฉด ๊ฐ์ ธ์ต๋๋ค summary = state.get("summary", "") # ์์ฝ์ด ์๋ค๋ฉด ์ถ๊ฐํฉ๋๋ค if summary: # ์์คํ ๋ฉ์์ง์ ์์ฝ ์ถ๊ฐ system_message = f"์ด์ ๋ํ์ ์์ฝ: {summary}" # ์๋ก์ด ๋ฉ์์ง์ ์์ฝ ์ถ๊ฐ messages = [SystemMessage(content=system_message)] + state["messages"] else: messages = state["messages"] response = model.invoke(messages) return {"messages": response}
def summarize_conversation(state: State):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
# ๋จผ์ ๊ธฐ์กด ์์ฝ์ ๊ฐ์ ธ์ต๋๋ค summary = state.get("summary", "") # ์์ฝ ํ๋กฌํํธ ์์ฑ if summary: # ์์ฝ์ด ์ด๋ฏธ ์กด์ฌํจ summary_message = ( f"์ง๊ธ๊น์ง์ ๋ํ ์์ฝ์ ๋๋ค: {summary}\n\n" "์์ ์๋ก์ด ๋ฉ์์ง๋ฅผ ๊ณ ๋ คํ์ฌ ์์ฝ์ ํ์ฅํ์ธ์:" ) else: summary_message = "์์ ๋ํ๋ฅผ ์์ฝํ์ธ์:" # ํ๋กฌํํธ๋ฅผ ํ์คํ ๋ฆฌ์ ์ถ๊ฐ messages = state["messages"] + [HumanMessage(content=summary_message)] response = model.invoke(messages) # ๊ฐ์ฅ ์ต๊ทผ 2๊ฐ์ ๋ฉ์์ง๋ง ๋จ๊ธฐ๊ณ ๋ชจ๋ ์ญ์ delete_messages = [RemoveMessage(id=m.id) for m in state["messages"][:-2]] return {"summary": response.content, "messages": delete_messages}
๋ํ๋ฅผ ์ข ๋ฃํ ์ง ์์ฝํ ์ง ๊ฒฐ์
def should_continue(state: State):
1 2 3 4 5 6 7 8 9 10
"""๋ค์์ ์คํํ ๋ ธ๋๋ฅผ ๋ฐํํฉ๋๋ค.""" messages = state["messages"] # ๋ฉ์์ง๊ฐ 6๊ฐ ์ด์์ด๋ฉด ๋ํ๋ฅผ ์์ฝํฉ๋๋ค if len(messages) > 6: return "summarize_conversation" # ๊ทธ๋ ์ง ์์ผ๋ฉด ์ข ๋ฃํฉ๋๋ค return END ```
- ์ฐจ์ด์ :
- In-Memory:
from langgraph.checkpoint.memory import MemorySaver
- External-DB:
from langgraph.checkpoint.sqlite import SqliteSaver
๊ทธ ์ธ์๋ ๊ณต์ ์ง์๋๋ ์ธ๋ถ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฒดํฌํฌ์ธํฐ๋กfrom langgraph.checkpoint.postgres import PostgresSaver
์ด ์์ต๋๋ค.
- In-Memory:
- (๋ณต์ต)
-
๋ํ ์ํ ๋ก๋/๋ณต๊ตฌ: SQLite๋ฅผ ์ฌ์ฉํ๋ฉด ๋ํ ์ํ๊ฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅ๋๋ฏ๋ก, ๋ํ๊ฐ ๊ธธ์ด์ง๊ฑฐ๋ ์ธ์ ์ด ์ข ๋ฃ๋๋๋ผ๋ ๋ฐ์ดํฐ๋ฅผ ๋ณต๊ตฌํ ์ ์์ต๋๋ค.
- ์๋ฅผ ๋ค์ด, ๋ ธํธ๋ถ ์ปค๋์ ์ฌ์์ํ๋๋ผ๋ ๋์ผํ SQLite DB๋ฅผ ์ฌ์ฉํ์ฌ ์ํ๋ฅผ ๋ค์ ๋ถ๋ฌ์ฌ ์ ์์ต๋๋ค. ```
config = {โconfigurableโ: {โthread_idโ: โ1โ}} graph_state = graph.get_state(config) graph_state
1 2 3 4
+ `config`: ๋ํ์ ์ค๋ ๋ ID๋ฅผ ์ง์ ํ๋ ์ค์ ์ ๋๋ค. ์ฌ๊ธฐ์๋ "thread\_id": "1"์ด๋ผ๋ ์ค๋ ๋์ ํด๋นํ๋ ๋ํ ์ํ๋ฅผ ๊ฐ์ ธ์ค๋ ์ค์ ์ ํ๊ณ ์์ต๋๋ค. + `graph.get_state(config)`: ์ด ๋ฉ์๋๋ ์ฃผ์ด์ง config์ ๋ง๋ ์ค๋ ๋์ ๋ํ ์ํ๋ฅผ SQLite ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ๋ถ๋ฌ์ค๋ ์ญํ ์ ํฉ๋๋ค. 
```
- ์ ์ฉํ CODE-SNIPPETS: SQLite DB๋ฅผ ํธ์ถํ๊ณ ๋์, ์๋์ ๊ฐ์ ํจ์๋ค์ ํตํด ์ ๋ณด๋ฅผ ํธ์ถํ ์ ์์ต๋๋ค:
1
2
3
4
5
6
7
8
# StateSnapshot์์ ๋ฉ์์ง ๋ถ๋ถ์ ์ถ์ถํ์ฌ ๋ณด๊ธฐ ์ข๊ฒ ์ถ๋ ฅ
for message in graph_state.values['messages']:
print(f"Message ID: {message.id}")
print(f"Message Type: {'Human' if isinstance(message, HumanMessage) else 'AI'}")
print(f"Content: {message.content}")
print('-' * 40)
-
์์ ์ถ๋ ฅ:
1
2
3
4
# ์์ฝ ์ถ๋ ฅ
summary = graph_state.values.get('summary', 'No summary available')
print(f"Conversation Summary: {summary}")
-
์์ ์ถ๋ ฅ: