LangGraph ์ต์ ๊ธฐ์ค ์ด๋ณด์ ์๋ฒฝ ๊ฐ์ด๋: ํ์ ์ฌ์ ์ค์น, StateGraph ์ํคํ
์ฒ, TypedDict ์ํ ๊ด๋ฆฌ, ์กฐ๊ฑด๋ถ ๋ถ๊ธฐ(Conditional Edges), ReAct ๋ฃจํ ์์ด์ ํธ, MemorySaver ๋ํ ์ฒดํฌํฌ์ธํ
๋ฐ Human-in-the-loop(์ธ๊ฐ ๊ฐ์
) ์ค์ ์์
## 1. ํ์ ์ฌ์ ์ค์น ์๊ฑด & ๊ฐ๋ฐ ํ๊ฒฝ ๊ตฌ์ฑ (Prerequisites)
LangGraph๋ฅผ ์ค๋ฅ ์์ด ๊ตฌ๋ํ๊ณ ์ํ ๊ธฐ๋ฐ ์์ด์ ํธ๋ฅผ ์ค์ตํ๊ธฐ ์ํด ์ฌ์ ์ ์ค๋นํด์ผ ํ๋ ํ๊ฒฝ ์๊ตฌ์ฌํญ์
๋๋ค.
### ๐ 1. Python ๋ฒ์ & ๊ฐ์ํ๊ฒฝ (Virtual Environment)
LangGraph๋ **Python 3.9 ~ 3.12+ (๊ถ์ฅ: Python 3.11 ๋๋ 3.12)** ํ๊ฒฝ์ ์ง์ํฉ๋๋ค. ๊ธ๋ก๋ฒ ํ๊ฒฝ ์ค์ผ์ ๋ฐฉ์งํ๊ธฐ ์ํด ๊ฐ์ํ๊ฒฝ ์์ฑ์ด **ํ์**์
๋๋ค.
```bash
# 1. ํ์ค venv ๊ฐ์ํ๊ฒฝ ์์ฑ
python -m venv .venv
# 2. ๊ฐ์ํ๊ฒฝ ํ์ฑํ (Windows PowerShell)
# โป ์คํฌ๋ฆฝํธ ์คํ ์ค๋ฅ ์: Set-ExecutionPolicy Unrestricted -Scope Process
.venv\Scripts\Activate.ps1
# ๊ฐ์ํ๊ฒฝ ํ์ฑํ (macOS / Linux / WSL)
source .venv/bin/activate
# 3. ๋๋ ์ฐจ์ธ๋ ์ด๊ณ ์ ํจํค์ง ๊ด๋ฆฌ์ uv ์ฌ์ฉ ์ (๊ฐ๋ ฅ ๊ถ์ฅ)
pip install uv
uv venv
# Windows: .venv\Scripts\activate / macOS: source .venv/bin/activate
uv pip install langgraph langchain-core langchain-openai
```
### ๐ ๏ธ 2. OS๋ณ C/C++ ๋ค์ดํฐ๋ธ ๋น๋ ๋๊ตฌ (์ปดํ์ผ๋ฌ)
`tiktoken`, `pydantic-core` ๋ฑ C++/Rust ํ์ฅ ํจํค์ง ์ค์น ์ ๋น๋ ์ค๋ฅ๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํด ๊ถ์ฅ๋ฉ๋๋ค.
- **Windows**: [Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) ("C++๋ฅผ ์ฌ์ฉํ ๋ฐ์คํฌํฑ ๊ฐ๋ฐ" ์ํฌ๋ก๋ ์ฒดํฌ)
- **macOS**: `xcode-select --install`
- **Linux (Ubuntu/Debian)**: `sudo apt update && sudo apt install -y build-essential python3-dev`
### ๐ฆ 3. ํ์ ํต์ฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ผ๊ด ์ค์น
```bash
# 1. LangGraph ์ฝ์ด ๋ฐ ์ฌ์ ๊ตฌ์ถ ๋ชจ๋
pip install langgraph
# 2. ์ฐ๋ํ LLM ๋ฐ ๋๊ตฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
pip install langchain-core langchain-openai langchain-community
# 3. ํ๊ฒฝ๋ณ์ ๊ด๋ฆฌ ๋ฐ ๋ฐ์ดํฐ ๊ฒ์ฆ
pip install python-dotenv pydantic
```
### ๐ 4. ํ๊ฒฝ ๋ณ์ ์ค์ (`.env`)
ํ๋ก์ ํธ ํด๋ ๋ฃจํธ์ `.env` ํ์ผ์ ์์ฑํฉ๋๋ค:
```bash
# OpenAI API ํค
OPENAI_API_KEY="sk-proj-xxxxxxxxxxxxxxxxxxxx"
# LangSmith ์ค์๊ฐ ์๊ฐ์ ํธ๋ ์ด์ฑ (๊ฐ๋ ฅ ์ถ์ฒ - ๋ฌด๋ฃ ๊ฐ์
๊ฐ๋ฅ)
# ๊ทธ๋ํ์ ๊ฐ ๋
ธ๋ ์คํ ์ํ์ ๋ถ๊ธฐ ํ๋ฆ์ ๋ธ๋ผ์ฐ์ ๋์๋ณด๋์์ ์ค์๊ฐ ํ์ธ ๊ฐ๋ฅ
LANGCHAIN_TRACING_V2="true"
LANGCHAIN_API_KEY="lsv2_pt_xxxxxxxxxxxxxxxxxxxx"
LANGCHAIN_PROJECT="lucky-langgraph-prod"
```
์ฝ๋ ์์ ๋ถ๋ถ์ ํ๊ฒฝ๋ณ์๋ฅผ ๋ก๋ํฉ๋๋ค:
```python
from dotenv import load_dotenv
load_dotenv()
```
---
## 2. LangGraph ํต์ฌ ๊ฐ๋
(Mental Model)
๊ธฐ์กด LangChain(LCEL)์ด ๋จ๋ฐฉํฅ ํ์ดํ๋ผ์ธ(A โ B โ C)์ด์๋ค๋ฉด, **LangGraph**๋ **์ํ ๋ฃจํ(Cycle), ์กฐ๊ฑด๋ถ ๋ถ๊ธฐ(Branching), ์ํ ๊ธฐ์ต(State Persistence), ์ธ๊ฐ ๊ฐ์
(HITL)**์ ๊ฐ๋ฅํ๊ฒ ํด์ฃผ๋ ์ํ ๊ทธ๋ํ ์์ง์
๋๋ค.
```text
[LangGraph ์ํคํ
์ฒ ๋ฉํ ๋ชจ๋ธ]
โโโโโโโโโโโโโโโโโโ
โ START โ
โโโโโโโโโฌโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโ
โโโโโโบโ LLM Agent โ (Node: ์ํ๋ฅผ ์ฝ๊ณ ๋ค์ ํ๋ ๊ฒฐ์ )
โ โโโโโโโโโฌโโโโโโโโโ
โ โ
(Loop: โ โโโโโโโโโโโโโโโโ[์กฐ๊ฑด ๋ง์กฑ: ์ข
๋ฃ]โโโโโโโโโโโโโโโโ
์ฌํธ์ถ) โ โ (Conditional Edge) โ
โ โผ โผ
โ โโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโ
โโโโโโโค Tool Node โ (Node: ๋๊ตฌ ์คํ ํ ๊ฒฐ๊ณผ ๋ฐ์)โ END โ
โโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโ
```
### ๐ 4๋ ํต์ฌ ๊ตฌ์ฑ์์
1. **State (์ํ)**: ๋ชจ๋ ๋
ธ๋๊ฐ ๊ณต์ ํ๊ณ ์
๋ฐ์ดํธํ๋ ๋ฐ์ดํฐ ์คํค๋ง (`TypedDict` ๋๋ Pydantic).
2. **Node (๋
ธ๋)**: ํ์ฌ ์ํ๋ฅผ ์
๋ ฅ๋ฐ์ ์๋ก์ด ์ํ ๋ณ๊ฒฝ๋ถ์ ๋ฐํํ๋ Python ํจ์.
3. **Edge (์ฃ์ง)**: ๋
ธ๋์ ๋
ธ๋ ์ฌ์ด์ ์ด๋ ๊ฒฝ๋ก (์ผ๋ฐ ์ฃ์ง ๋ฐ ์กฐ๊ฑด๋ถ ์ฃ์ง).
4. **Checkpointer (์ฒดํฌํฌ์ธํฐ)**: ๋งค ์คํ
๋ง๋ค ์ํ๋ฅผ ์ค๋
์ท์ผ๋ก ์ ์ฅํ์ฌ ๋กค๋ฐฑ ๋ฐ ๋ํ ๋ณต์์ ๊ฐ๋ฅํ๊ฒ ํ๋ ๋ฉ๋ชจ๋ฆฌ.
---
## 3. [์ด๋ณด์ 1๋จ๊ณ] ๊ฐ์ฅ ๋จ์ํ 2-๋
ธ๋ ์์ฐจ ๊ทธ๋ํ
๋
ธ๋ 2๊ฐ๋ฅผ ์์ฑํ๊ณ ๋ฐ์ดํฐ๋ฅผ ์ ๋ฌํ๋ ๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ธ ํฌ๋ก์๋ ์์ ์
๋๋ค.
```python
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
# 1. ์ํ ์คํค๋ง ์ ์ (๋
ธ๋๋ค์ด ๊ณต์ ํ ๋ฐ์ดํฐ ๊ตฌ์กฐ)
class SimpleState(TypedDict):
user_name: str
message: str
# 2. ๋
ธ๋ ํจ์ ์ ์ (state๋ฅผ ์ธ์๋ก ๋ฐ์, ๋ณ๊ฒฝํ ๊ฐ๋ง ๋์
๋๋ฆฌ๋ก ๋ฐํ)
def greeting_node(state: SimpleState):
print(f"[๋
ธ๋ 1 ์คํ] ์
๋ ฅ๋ฐ์ ์ด๋ฆ: {state['user_name']}")
return {"message": f"์๋
ํ์ธ์, {state['user_name']}๋!"}
def append_cheer_node(state: SimpleState):
print(f"[๋
ธ๋ 2 ์คํ] ์ด์ ๋ฉ์์ง: {state['message']}")
return {"message": f"{state['message']} ์ค๋๋ ๋ฉ์ง ํ๋ฃจ ๋ณด๋ด์ธ์!"}
# 3. ๊ทธ๋ํ ์์ฑ ๋ฐ ๋
ธ๋ ๋ฑ๋ก
builder = StateGraph(SimpleState)
builder.add_node("greeting", greeting_node)
builder.add_node("cheer", append_cheer_node)
# 4. ์ฃ์ง ์ฐ๊ฒฐ (START โ greeting โ cheer โ END)
builder.add_edge(START, "greeting")
builder.add_edge("greeting", "cheer")
builder.add_edge("cheer", END)
# 5. ๊ทธ๋ํ ์ปดํ์ผ (์คํ ๊ฐ๋ฅํ Runnable ์ฑ ์์ฑ)
app = builder.compile()
# 6. ์คํ (์ด๊ธฐ ์ํ ์ฃผ์
)
final_state = app.invoke({"user_name": "ํ๊ธธ๋"})
print("\n์ต์ข
๊ฒฐ๊ณผ:", final_state["message"])
# ์ถ๋ ฅ: ์๋
ํ์ธ์, ํ๊ธธ๋๋! ์ค๋๋ ๋ฉ์ง ํ๋ฃจ ๋ณด๋ด์ธ์!
```
---
## 4. [์ด๋ณด์ 2๋จ๊ณ] ์กฐ๊ฑด๋ถ ๋ถ๊ธฐ (Conditional Edges)
์ฌ์ฉ์์ ์
๋ ฅ ์กฐ๊ฑด์ ๋ฐ๋ผ ์๋ก ๋ค๋ฅธ ๋
ธ๋๋ก ๋์ ๋ถ๊ธฐ(๋ผ์ฐํ
)ํ๋ ํจํด์
๋๋ค.
```python
from typing import TypedDict, Literal
from langgraph.graph import StateGraph, START, END
class RouteState(TypedDict):
query: str
intent: str
result: str
# 1. ์๋ ๋ถ๋ฅ ๋
ธ๋
def classify_intent_node(state: RouteState):
q = state["query"].lower()
if "๋ฐฐ์ก" in q or "์ฃผ๋ฌธ" in q:
intent = "delivery"
elif "ํ๋ถ" in q or "์ทจ์" in q:
intent = "refund"
else:
intent = "general"
return {"intent": intent}
# 2. ๋ถ๊ธฐ ์ฒ๋ฆฌ ๋
ธ๋๋ค
def delivery_node(state: RouteState):
return {"result": "๐ฆ ๋ฐฐ์ก ์กฐํ: ํ์ฌ ๊ณ ๊ฐ๋์ ์ํ์ด ์ถ๊ณ ์ค๋น ์ค์
๋๋ค."}
def refund_node(state: RouteState):
return {"result": "๐ฐ ํ๋ถ ์ ์: ๊ตฌ๋งค ํ 7์ผ ์ด๋ด ์ํ์ ์ ์ก ํ๋ถ ๊ฐ๋ฅํฉ๋๋ค."}
def general_node(state: RouteState):
return {"result": "๐ค ์ผ๋ฐ ์๋ด: ์๋ด์ ์ฐ๊ฒฐ์ ๋์๋๋ฆฌ๊ฒ ์ต๋๋ค."}
# 3. ๋ผ์ฐํ
์กฐ๊ฑด ํจ์ (๋ค์ ๋
ธ๋ ์ด๋ฆ์ ๋ฐํ)
def route_decision(state: RouteState) -> Literal["delivery", "refund", "general"]:
return state["intent"]
# 4. ๊ทธ๋ํ ์กฐ๋ฆฝ
builder = StateGraph(RouteState)
builder.add_node("classifier", classify_intent_node)
builder.add_node("delivery", delivery_node)
builder.add_node("refund", refund_node)
builder.add_node("general", general_node)
builder.add_edge(START, "classifier")
# ์กฐ๊ฑด๋ถ ์ฃ์ง ๋ฑ๋ก (classifier ๋
ธ๋ ์ข
๋ฃ ํ route_decision ํจ์์ ๊ฒฐ๊ณผ์ ๋ฐ๋ผ ์ด๋)
builder.add_conditional_edges(
"classifier",
route_decision,
{
"delivery": "delivery",
"refund": "refund",
"general": "general"
}
)
# ๋ชจ๋ ๋ถ๊ธฐ ๋
ธ๋๋ ์ต์ข
์ข
๋ฃ(END)๋ก ์ด๋
builder.add_edge("delivery", END)
builder.add_edge("refund", END)
builder.add_edge("general", END)
app = builder.compile()
# ํ
์คํธ ์คํ
print(app.invoke({"query": "๋ด ํ๋ฐฐ ๋ฐฐ์ก ์ธ์ ๋์ฐฉํด?"})["result"])
print(app.invoke({"query": "๊ฒฐ์ ํ ์ ๋ฐ ํ๋ถํ๊ณ ์ถ์ด์"})["result"])
```
---
## 5. [์ด๋ณด์ 3๋จ๊ณ] ๋ฃจํ(Cycle)๊ฐ ์๋ ReAct ์์ด์ ํธ
LLM์ด ํ์ํ ๋ ๋๊ตฌ๋ฅผ ํธ์ถํ๊ณ , ๋๊ตฌ ์คํ ๊ฒฐ๊ณผ๋ฅผ ๋ค์ ์ฝ์ ๋ค ์ต์ข
๋ต์ ๋ผ ๋๊น์ง ์ค์ค๋ก ๋ฃจํ๋ฅผ ๋๋ ์์ฑํ ์์ด์ ํธ ํจํด์
๋๋ค.
### ๐ก ๋ฉ์์ง ๋ฆฌ๋์ (`add_messages`)
- ์ํ ๋์
๋๋ฆฌ์ ๊ธฐ๋ณธ ๋์์ ๊ฐ ๋ฎ์ด์ฐ๊ธฐ(Overwrite)์
๋๋ค.
- ํ์ง๋ง ๋ํ ๊ธฐ๋ก์ ๋์ (Append)๋์ด์ผ ํ๋ฏ๋ก `Annotated[list, add_messages]` ๋ฆฌ๋์๋ฅผ ์ฌ์ฉํฉ๋๋ค.
```python
from typing import Annotated
from typing_extensions import TypedDict
from langchain_core.messages import BaseMessage, HumanMessage
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode, tools_condition
# 1. ๋ํ ๋์ ์ํ ์ ์
class AgentState(TypedDict):
messages: Annotated[list[BaseMessage], add_messages]
# 2. ์์ด์ ํธ๊ฐ ํ์ฉํ ๋๊ตฌ ์ค๋น
@tool
def get_weather(city: str) -> str:
"""๋์ ์ด๋ฆ์ ๋ฐ์ ํ์ฌ ๋ ์จ๋ฅผ ์กฐํํฉ๋๋ค."""
weather_db = {"์์ธ": "๋ง์, 21๋", "๋ถ์ฐ": "๋น, 18๋", "์ ์ฃผ": "๊ตฌ๋ฆ ์กฐ๊ธ, 23๋"}
return weather_db.get(city, f"{city}์ ๋ ์จ ์ ๋ณด๊ฐ ์์ต๋๋ค.")
tools = [get_weather]
# 3. ๋ชจ๋ธ์ ๋๊ตฌ ๋ฐ์ธ๋ฉ
llm = ChatOpenAI(model="gpt-4o", temperature=0)
llm_with_tools = llm.bind_tools(tools)
# 4. LLM ํธ์ถ ๋
ธ๋
def chatbot_node(state: AgentState):
response = llm_with_tools.invoke(state["messages"])
return {"messages": [response]}
# 5. ๊ทธ๋ํ ๊ตฌ์ถ
builder = StateGraph(AgentState)
builder.add_node("chatbot", chatbot_node)
builder.add_node("tools", ToolNode(tools)) # ์ฌ์ ์ ์๋ ๋๊ตฌ ์คํ ๋
ธ๋
builder.add_edge(START, "chatbot")
# ์กฐ๊ฑด๋ถ ๋ถ๊ธฐ: LLM์ด ๋๊ตฌ๋ฅผ ๋ถ๋ ์ผ๋ฉด 'tools'๋ก ๊ฐ๊ณ , ์ ๋ถ๋ ์ผ๋ฉด END๋ก ์ข
๋ฃ
builder.add_conditional_edges("chatbot", tools_condition)
# ๋๊ตฌ ์คํ ํ์๋ ๋ค์ LLM์ผ๋ก ๋ฃจํ(Cycle)!
builder.add_edge("tools", "chatbot")
app = builder.compile()
# ์คํ
inputs = {"messages": [HumanMessage(content="์์ธ ๋ ์จ๊ฐ ์ด๋?")]}
result = app.invoke(inputs)
print("์ต์ข
๋ต๋ณ:", result["messages"][-1].content)
```
---
## 6. [์ด๋ณด์ ์ถ์ฒ] 10์ค๋ก ๋ง๋๋ `create_react_agent`
์์ ๋ณต์กํ ๊ทธ๋ํ ๊ตฌ์กฐ๋ฅผ ๋จ ํ ์ค์ ๊ณ ์์ค ํจ์๋ก ๋ง๋ค์ด ์ฃผ๋ ๊ณต์ Prebuilt ๋ฐฉ์์
๋๋ค.
```python
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langgraph.prebuilt import create_react_agent
from langgraph.checkpoint.memory import MemorySaver
@tool
def calculate_vat(price: int) -> int:
"""์ํ ๊ฐ๊ฒฉ์ ๋ฐ์ 10% ๋ถ๊ฐ์ธ๋ฅผ ๊ณ์ฐํฉ๋๋ค."""
return int(price * 0.1)
tools = [calculate_vat]
llm = ChatOpenAI(model="gpt-4o", temperature=0)
# ์ธ๋ฉ๋ชจ๋ฆฌ ๋ํ ๊ธฐ์ต ์ฒดํฌํฌ์ธํฐ
memory = MemorySaver()
# ๋จ ํ ์ค๋ก ReAct ์์ด์ ํธ ์์ฑ ์๋ฃ
agent = create_react_agent(model=llm, tools=tools, checkpointer=memory)
# ์ธ์
ID๋ฅผ ๋ถ์ฌํ์ฌ ์คํ
config = {"configurable": {"thread_id": "user-session-001"}}
res1 = agent.invoke({"messages": [("user", "100,000์์ง๋ฆฌ ์์ผ์ ๋ถ๊ฐ์ธ๋ ์ผ๋ง์ผ?")]}, config=config)
print(res1["messages"][-1].content)
# ์ด์ ๋ํ๋ฅผ ๊ธฐ์ตํ๋ ํ์ ์ง๋ฌธ
res2 = agent.invoke({"messages": [("user", "๊ทธ๋ผ ๋ถ๊ฐ์ธ๋ฅผ ํฌํจํ ์ด ๊ฒฐ์ ๊ธ์ก์ ์ผ๋ง๊ฐ ๋ผ?")]}, config=config)
print(res2["messages"][-1].content)
```
---
## 7. ๋ํ ๊ธฐ์ต & ์์์ฑ (Persistence & Checkpointing)
LangGraph๋ ๋งค ์คํ
๋ง๋ค ์ํ๋ฅผ ์ฒดํฌํฌ์ธํฐ์ ์ค๋
์ท์ผ๋ก ์๊ตฌ ์ ์ฅํฉ๋๋ค. `thread_id`๋ง ์ ์งํ๋ฉด ์๋ฒ๊ฐ ์ฌ์์๋์ด๋ ๋ํ ํ๋ฆ์ ๋ณต์ํ ์ ์์ต๋๋ค.
```python
from langgraph.checkpoint.memory import MemorySaver
# 1. ์ฒดํฌํฌ์ธํฐ ์ค์
checkpointer = MemorySaver() # ์ค๋ฌด์์๋ SQLiteSaver๋ PostgresSaver ์ฌ์ฉ
# 2. ์ปดํ์ผ ์ ์ฒดํฌํฌ์ธํฐ ๋ฐ์ธ๋ฉ
app = builder.compile(checkpointer=checkpointer)
# 3. ๊ณ ์ ํ thread_id๋ก ๋ํ ์งํ
thread_kim = {"configurable": {"thread_id": "user-kim-777"}}
thread_lee = {"configurable": {"thread_id": "user-lee-888"}}
# ๊น ๊ณ ๊ฐ ๋ํ
app.invoke({"messages": [("user", "๋ด ์ด๋ฆ์ ๊น์ฒ ์์ผ.")]}, config=thread_kim)
# ์ด ๊ณ ๊ฐ ๋ํ
app.invoke({"messages": [("user", "๋ด ์ด๋ฆ์ ์ด์ํฌ์ผ.")]}, config=thread_lee)
# ๊น ๊ณ ๊ฐ ์ฌ๋ฐฉ๋ฌธ ํ์ธ
res = app.invoke({"messages": [("user", "๋ด ์ด๋ฆ์ด ๋ญ์์ง?")]}, config=thread_kim)
print("๊น์ฒ ์ ์ธ์
์๋ต:", res["messages"][-1].content) # ๊น์ฒ ์๋์ด๋ผ๊ณ ์ ํํ ๊ธฐ์ตํจ
```
---
## 8. ์ธ๊ฐ ๊ฐ์
(Human-in-the-loop: HITL)
๊ฒฐ์ ์น์ธ, DB ์ญ์ , ๋ฉ์ผ ๋ฐ์ก ๋ฑ ์ํํ ํ๋ ์ง์ ์ **์์ด์ ํธ ์คํ์ ์ผ์ ์ ์ง(Pause)**ํ๊ณ ๊ด๋ฆฌ์์ ์น์ธ์ ๋ฐ๋ ํจํด์
๋๋ค.
```python
# tools ๋
ธ๋ ์คํ ์ง์ ์ ๋ฉ์ถ๋๋ก ์ค์
app = builder.compile(
checkpointer=memory,
interrupt_before=["tools"] # 'tools' ๋
ธ๋ ์ง์
์ ์๋ ์ผ์ ์ ์ง
)
config = {"configurable": {"thread_id": "order-101"}}
# 1. ์ฌ์ฉ์ ์์ฒญ ์คํ โ ๋๊ตฌ ํธ์ถ ์ง์ ์ ์ ์ง๋จ!
app.invoke({"messages": [("user", "๊ด๋ฆฌ์ ๊ถํ์ผ๋ก ์๋ฒ ์ฌ๋ถํ
ํด์ค.")]}, config=config)
# 2. ํ์ฌ ์ ์ง๋ ์ํ ์กฐํ
current_state = app.get_state(config)
print("ํ์ฌ ์ ์ง๋ ๋
ธ๋ ์ง์ ๋จ๊ณ:", current_state.next) # ('tools',)
# 3. ๊ด๋ฆฌ์ ์น์ธ ํ ์คํ ์ฌ๊ฐ (None์ ๋๊ธฐ๋ฉด ๋ฉ์ท๋ ์ง์ ๋ถํฐ ์ด์ด์ ์คํ)
final_result = app.invoke(None, config=config)
print("์น์ธ ํ ์ต์ข
์คํ ๊ฒฐ๊ณผ:", final_result["messages"][-1].content)
```
---
## 9. ์ค์๊ฐ ์คํธ๋ฆฌ๋ฐ (Streaming)
์์ด์ ํธ์ ๋
ธ๋ ์งํ ๋จ๊ณ์ ์์ฑ ํ ํฐ์ ์ฌ์ฉ์ ํ๋ฉด์ ์ค์๊ฐ์ผ๋ก ์ถ๋ ฅํ๋ ๋ฐฉ๋ฒ์
๋๋ค.
```python
# ๋
ธ๋ ๋จ์ ์คํ ๊ฒฐ๊ณผ ์ค์๊ฐ ์คํธ๋ฆฌ๋ฐ
for event in app.stream({"messages": [("user", "๋ถ์ฐ ๋ ์จ ์๋ ค์ค")]}, config=config):
for node_name, state_update in event.items():
print(f"\n--- [๋
ธ๋ ์๋ฃ: {node_name}] ---")
if "messages" in state_update:
print("์ต์ ๋ฉ์์ง:", state_update["messages"][-1].content)
```
---
## 10. ํ๋ก๋์
๊ฐ๋ฐ ์ฒดํฌ๋ฆฌ์คํธ
| ์ ๊ฒ ํญ๋ชฉ | ๊ถ์ฅ ์ฌํญ | ์ด์ |
| :--- | :--- | :--- |
| **State ์ต์ํ** | ๊ผญ ํ์ํ ํ๋๋ง State์ ์ ์ธ | ๋งค ๋
ธ๋๋ง๋ค ์ํ ์ค๋
์ท์ด ์ง๋ ฌํ๋๋ฏ๋ก ํฌ๊ธฐ ์ ๊ฐ |
| **๋ฆฌ๋์ ์ฌ์ฉ** | ๋ํ ๊ธฐ๋ก์ `add_messages` ํ์ ์ ์ฉ | ๋ฎ์ด์ฐ๊ธฐ ๋ฐฉ์ง ๋ฐ ๋ฉํฐํด ๋ํ ๋ณด์กด |
| **DB ์์ํ** | ์ด์ ํ๊ฒฝ์์๋ `PostgresSaver` ๋์
| ์๋ฒ ์ฌ์์ ์์๋ ์์ญ๋ง ์ ์ ์ธ์
์ ์ง |
| **HITL ์น์ธ ์ ์ฐจ** | ๊ธ์ต ๊ฒฐ์ /DB ์์ ๋๊ตฌ๋ `interrupt_before` ์ง์ | LLM ํ๊ฐ(Hallucination)์ ์ํ ์ค์๋ ์์ฒ ์ฐจ๋จ |
| **ํธ๋ ์ด์ฑ ํ์ฑํ** | `LANGCHAIN_TRACING_V2=true` ์ค์ | LangSmith์์ ์ ์ฒด ๊ทธ๋ํ ๊ฒฝ๋ก์ ์ง์ฐ์๊ฐ ์๊ฐํ |
์๊ฒฌ ๋ฐ ์ง๋ฌธ
0์์ง ๋ฑ๋ก๋ ์๊ฒฌ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋๊ธ์ ๋จ๊ฒจ๋ณด์ธ์!
๋๊ธ ์์
๋๊ธ ์ญ์