๐ฏ LangGraph์ Human-in-the-Loop ํจํด์ ํ์ตํ๊ณ ์ ์กฐ/์๋์ง ํ์ฅ ํ์ง ๊ด๋ฆฌ์ ์ ์ฉํ ํ๋ก์ ํธ
- ๊ฐ์
- ์ฃผ์ ๊ธฐ๋ฅ
- ์ค์น ๋ฐ ์คํ
- ์ฌ์ฉ ์์
- ํต์ฌ ๊ฐ๋
- ์ค์ ์ ์ฉ ์๋๋ฆฌ์ค
- ๊ธฐ์ ์คํ
- ํ์ต ๋ด์ฉ
- ๋ค์ ๋จ๊ณ
- ์ฐธ๊ณ ์๋ฃ
์ด ํ๋ก์ ํธ๋ LangGraph์ HITL(Human-in-the-Loop) ํจํด์ ํ์ตํ๊ณ , ์ด๋ฅผ ์ ์กฐ/์๋์ง ํ์ฅ์ ํ์ง ๊ด๋ฆฌ(QA) ์์คํ ์ ์ ์ฉํ ์ค์ต ์ฝ๋์ ๋๋ค.
ํ๋ ์ ์กฐ ํ์ฅ์์๋ AI๊ฐ ๋น ๋ฅด๊ฒ ๋ฐ์ดํฐ๋ฅผ ๋ถ์ํ์ง๋ง, ์ค์ํ ๊ฒฐ์ ์ ์ฌ์ ํ ์ธ๊ฐ ์ ๋ฌธ๊ฐ์ ํ๋จ์ด ํ์ํฉ๋๋ค. ํนํ:
- ๐ญ ๊ฐ๋ ์ค์ง ๊ฒฐ์ : AI๊ฐ ๊ณผ์ด์ ๊ฐ์งํ์ด๋, ์ ๋ฌธ๊ฐ๊ฐ ์ต์ข ์น์ธ
โ ๏ธ ์์ ํ๋กํ ์ฝ: ์ํํ ์กฐ์น๋ ๋ฐ๋์ ์ธ๊ฐ ๊ฒํ ํ์- ๐ง ์ ์ง๋ณด์ ์ค์ผ์ค๋ง: AI ๊ถ์ฅ ์กฐ์น๋ฅผ ์ ๋ฌธ๊ฐ๊ฐ ํ์ฅ ์ํฉ์ ๋ง๊ฒ ์กฐ์
1. โ
LangGraph์ `interrupt()` ๋ฐ `Command` ํจํด ์ดํด
2. โ
๊ธฐ๋ณธ ์น์ธ ์ํฌํ๋ก์ฐ ๊ตฌํ (`lg_approval.py`)
3. โ
์ค์ ์ ์กฐ ํ์ฅ ์๋๋ฆฌ์ค์ ์ ์ฉ (`lg_app_qa.py`)
4. โ
๋๊ธฐ/๋น๋๊ธฐ ์คํ ๋ชจ๋ ๋น๊ต ํ์ต
# ์น์ธ/๊ฑฐ๋ถ์ ๋ฐ๋ฅธ ์กฐ๊ฑด๋ถ ๋ผ์ฐํ
approved = interrupt({"question": "์ด ์์
์ ์น์ธํ์๊ฒ ์ต๋๊น?"})
if approved:
return Command(goto="do", update={"status": "approved"})
else:
return Command(goto="cancel", update={"status": "rejected"})ํน์ง:
- ๐ก
interrupt()๋ฅผ ํตํ ์ฌ์ฉ์ ์ ๋ ฅ ๋๊ธฐ - ๐
Command๋ก ๋์ ๋ผ์ฐํ (์น์ธ โ do, ๊ฑฐ๋ถ โ cancel) - โก ๋๊ธฐ/๋น๋๊ธฐ ์คํ ๋ชจ๋ ์ง์
- ๐ ๋น๋๊ธฐ ์คํธ๋ฆฌ๋ฐ ๋๋ฒ๊น ํฌํจ
# ์ํ๋์ ๋ฐ๋ฅธ ์กฐ๊ฑด๋ถ ์น์ธ
if risk_level == "LOW":
return Command(goto="execute_action", update={"human_approval": True})
else:
# HIGH/CRITICAL์ ์ ๋ฌธ๊ฐ ๊ฒํ ํ์
approval_data = interrupt({
"type": "expert_approval_required",
"risk_level": risk_level,
"sensor_data": sensor_data
})ํน์ง:
- ๐ค AI ์ผ์ ๋ถ์: ์จ๋/์๋ ฅ/์ง๋/์ ๋/์ ๋ ฅ ๋ชจ๋ํฐ๋ง
- ๐ ์ํ๋ ์๋ ๋ถ๋ฅ: LOW/HIGH/CRITICAL
- ๐ค ์กฐ๊ฑด๋ถ ์ ๋ฌธ๊ฐ ๊ฒํ :
- LOW โ ์๋ ์น์ธ (์ ๋ฌธ๊ฐ ๊ฐ์ ๋ถํ์)
- HIGH/CRITICAL โ ์ ๋ฌธ๊ฐ ๊ฒํ ํ์
- โ๏ธ ์ ๋ฌธ๊ฐ Override: AI๊ฐ "๊ธด๊ธ ์ค์ง"๋ฅผ ๊ถ์ฅํด๋ ์ ๋ฌธ๊ฐ๊ฐ "๋ถํ ๊ฐ์"๋ก ๋ณ๊ฒฝ ๊ฐ๋ฅ
- ๐ฌ 4๊ฐ์ง ์๋๋ฆฌ์ค: ์ ์/๊ณผ์ด/์๋ ฅ๊ธ์ฆ/์ง๋์ด์
- ๐ ์์ ํ ๊ฐ์ฌ ์ถ์ : ํ์์คํฌํ, ์ ๋ฌธ๊ฐ ์๊ฒฌ, ์คํ ์กฐ์น ๊ธฐ๋ก
# Python 3.10 ์ด์ ํ์
python --version
# ๊ฐ์ํ๊ฒฝ ์์ฑ (๊ถ์ฅ)
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
# ์์กด์ฑ ์ค์น
pip install langgraph langchain-core typing-extensionspython lg_approval.py๋ํํ ํ๋กฌํํธ:
์คํ ๋ชจ๋๋ฅผ ์ ํํ์ธ์:
1. ๋น๋๊ธฐ ์คํธ๋ฆฌ๋ฐ ๋ฐฉ์ (๊ถ์ฅ)
2. ๋๊ธฐ ๋ฐฉ์ (๊ฐ๋จ)
์ ํ (1 or 2): 2
=== ์ฒซ ๋ฒ์งธ ์คํ ===
์๋ก์ด ์ธ์
ID: abc123...
์ง๋ฌธ: ์ด ์์
์ ์น์ธํ์๊ฒ ์ต๋๊น?
์์
: ์๋ฒ ์ฌ๋ถํ
์น์ธํ์๊ฒ ์ต๋๊น? (yes/no): yes
=== 'True'๋ก ์ฌ๊ฐ ===
--- ์์
์คํ: ์๋ฒ ์ฌ๋ถํ
---
์ต์ข
์ํ: completed
```bash
python lg_app_qa.py
```
๋ํํ ํ๋กฌํํธ:
๐ญ ์ ์กฐ/์๋์ง ํ์ฅ HITL ๋ชจ๋ํฐ๋ง ์์คํ
์๋๋ฆฌ์ค๋ฅผ ์ ํํ์ธ์:
1. normal - ์ ์ ์๋
2. overheating - ๊ณผ์ด ๊ฐ์ง
3. pressure_spike - ์๋ ฅ ๊ธ์ฆ
4. vibration_anomaly - ์ง๋ ์ด์
์ ํ (1-4): 2
์คํ ๋ชจ๋:
1. ๋๊ธฐ ๋ฐฉ์ (๊ถ์ฅ)
2. ๋น๋๊ธฐ ๋ฐฉ์
์ ํ (1-2): 1
############################################################
๐ญ ์ค๋น ๋ชจ๋ํฐ๋ง ์์
############################################################
์ค๋น ID: PLANT-A1B2C3D4
์๋๋ฆฌ์ค: overheating
############################################################
๐ค AI ๋ถ์ ์๋ฃ - 2025-02-10 14:30:25
============================================================
์ํ๋: CRITICAL
๋ถ์ ๊ฒฐ๊ณผ:
๐จ ๊ธด๊ธ ์ํฉ ๊ฐ์ง!
์จ๋ ์ํ: 95.8ยฐC (์๊ณ๊ฐ: 90.0ยฐC)
๊ถ์ฅ ์กฐ์น: IMMEDIATE_SHUTDOWN
============================================================
โธ๏ธ ์ ๋ฌธ๊ฐ ๊ฒํ ๋๊ธฐ ์ค...
์น์ธํ์๊ฒ ์ต๋๊น? (yes/no): yes
์๊ฒฌ์ ์
๋ ฅํ์ธ์: ํ์ฅ ํ์ธ ์๋ฃ, ์ฆ์ ๊ฐ๋ ์ค์ง ์น์ธ
โ๏ธ ์กฐ์น ์คํ: IMMEDIATE_SHUTDOWN
๐ ๊ธด๊ธ ๊ฐ๋ ์ค์ง ์คํ
์ ๋ฌธ๊ฐ ์๊ฒฌ: ํ์ฅ ํ์ธ ์๋ฃ, ์ฆ์ ๊ฐ๋ ์ค์ง ์น์ธ
############################################################
๐ ์ต์ข
๋ฆฌํฌํธ
############################################################
์ค๋น ID: PLANT-A1B2C3D4
์ํ๋: CRITICAL
์ ๋ฌธ๊ฐ ์น์ธ: โ
์
์ ๋ฌธ๊ฐ ์๊ฒฌ: ํ์ฅ ํ์ธ ์๋ฃ, ์ฆ์ ๊ฐ๋ ์ค์ง ์น์ธ
์คํ๋ ์กฐ์น: IMMEDIATE_SHUTDOWN
ํ์์คํฌํ: 2025-02-10T14:30:25.123456
############################################################
```
---
## ๐ก ์ฌ์ฉ ์์
### ์์ 1: ์ ์ ์๋ (์๋ ์น์ธ)
```bash
# ์๋๋ฆฌ์ค 1 ์ ํ
์ ํ (1-4): 1
๐ค AI ๋ถ์: ์จ๋ 72.5ยฐC โ ์ํ๋ LOW
โ
์ํ๋ ๋ฎ์ - ์๋ ์น์ธ
๐๏ธ ๋ชจ๋ํฐ๋ง ๊ณ์
์ ๋ฌธ๊ฐ ์๊ฒฌ: ์๋ ์น์ธ (์ ์ ๋ฒ์)
โ
์ ์ ์๋ - ์ ๋ฌธ๊ฐ ๊ฐ์
๋ถํ์
```
### ์์ 2: ๊ณผ์ด ๊ฐ์ง (์ ๋ฌธ๊ฐ ์น์ธ)
```bash
# ์๋๋ฆฌ์ค 2 ์ ํ
์ ํ (1-4): 2
๐ค AI ๋ถ์: ์จ๋ 95.8ยฐC โ ์ํ๋ CRITICAL
โธ๏ธ ์ ๋ฌธ๊ฐ ๊ฒํ ๋๊ธฐ ์ค...
์น์ธํ์๊ฒ ์ต๋๊น? (yes/no): yes
์๊ฒฌ: ๊ธด๊ธ ๊ฐ๋ ์ค์ง ์น์ธ
โ๏ธ ์กฐ์น ์คํ: IMMEDIATE_SHUTDOWN
๐ ๊ธด๊ธ ๊ฐ๋ ์ค์ง ์คํ
```
### ์์ 3: ์ ๋ฌธ๊ฐ Override
```bash
# ์๋๋ฆฌ์ค 2 ์ ํ
์ ํ (1-4): 2
๐ค AI ๋ถ์: ์จ๋ 95.8ยฐC โ ์ํ๋ CRITICAL
AI ๊ถ์ฅ ์กฐ์น: IMMEDIATE_SHUTDOWN
์น์ธํ์๊ฒ ์ต๋๊น? (yes/no): yes
์๊ฒฌ: ๋ถํ๋ง ๊ฐ์ํ์ฌ ์ ๊ฒ
์กฐ์น๋ฅผ ์์ ํ์๊ฒ ์ต๋๊น? (yes/no): yes
๊ฐ๋ฅํ ์กฐ์น:
1. IMMEDIATE_SHUTDOWN
2. CONTROLLED_SHUTDOWN
3. REDUCE_LOAD
4. MAINTENANCE_ALERT
5. CONTINUE_MONITORING
์ ํ: 3
โ ๏ธ ์ ๋ฌธ๊ฐ๊ฐ AI ๊ถ์ฅ ์กฐ์น๋ฅผ ์์ ํ์ต๋๋ค
์๋ ๊ถ์ฅ ์กฐ์น: IMMEDIATE_SHUTDOWN
์์ ๋ ์กฐ์น: REDUCE_LOAD
โ๏ธ ์กฐ์น ์คํ: REDUCE_LOAD
๐ ๋ถํ ๊ฐ์ ์คํ
```
---
## ๐ง ํต์ฌ ๊ฐ๋
### 1. `interrupt()` vs `interrupt_before`
| ํน์ง | `interrupt()` | `interrupt_before` |
|------|---------------|-------------------|
| ์์น | ๋
ธ๋ ๋ด๋ถ | ์ปดํ์ผ ์ ์ค์ |
| ์กฐ๊ฑด๋ถ ์คํ | โ
๊ฐ๋ฅ | โ ๋ถ๊ฐ๋ฅ |
| ๋ฐ์ดํฐ ์ ๋ฌ | โ
๊ฐ๋ฅ | โ ์ ํ์ |
| ์ฌ์ฉ ๋์ด๋ | โ ๏ธ ์ค๊ธ | โ
์ฌ์ |
**`interrupt()` ์์ (๋ณธ ํ๋ก์ ํธ):**
```python
def expert_approval_node(state):
if state["risk_level"] == "LOW":
return Command(goto="execute", update={...}) # ์๋ ์น์ธ
# HIGH/CRITICAL๋ง interrupt
approval = interrupt({"question": "์น์ธ?"})
return Command(goto="execute", update={...})
interrupt_before ์์:
graph = builder.compile(
checkpointer=memory,
interrupt_before=["approval_node"] # ํญ์ ์ด ๋
ธ๋ ์ ์ ๋ฉ์ถค
)def approval_node(state):
decision = interrupt({"question": "์น์ธ?"})
# ์กฐ๊ฑด์ ๋ฐ๋ผ ๋ค๋ฅธ ๋
ธ๋๋ก ์ด๋
if decision["approved"]:
return Command(goto="execute", update={"status": "approved"})
elif decision.get("override"):
return Command(goto="override", update={"action": decision["override"]})
else:
return Command(goto="cancel", update={"status": "rejected"})# ์ํ๋๊ฐ ๋ฎ์ผ๋ฉด ์๋ ์น์ธ, ๋์ผ๋ฉด ์ ๋ฌธ๊ฐ ๊ฒํ
if risk_level == "LOW":
return auto_approve()
else:
return request_expert_approval()**์ํฉ:**
- AI๊ฐ ์ค๋น ๊ณผ์ด ๊ฐ์ง (95.8ยฐC)
- ์ ์ ๋ฒ์: 70-80ยฐC
- ์๊ณ๊ฐ: 90ยฐC
**์ํฌํ๋ก์ฐ:**
1. ๐ค AI ์ผ์ ๋ถ์ โ CRITICAL ์ํ๋ ๊ฐ์ง
2. โธ๏ธ ๊ทธ๋ํ ์ค๋จ, ์ ๋ฌธ๊ฐ ์๋ฆผ ์ ์ก
3. ๐ค ์ ๋ฌธ๊ฐ ํ์ฅ ํ์ธ
4. โ
์ ๋ฌธ๊ฐ ์น์ธ โ ๊ธด๊ธ ๊ฐ๋ ์ค์ง ์คํ
5. ๐ ๊ฐ์ฌ ๊ธฐ๋ก: "์ ๋ฌธ๊ฐ A, 2025-02-10 14:30, ์น์ธ, ๊ธด๊ธ ๊ฐ๋ ์ค์ง"
**์ํฉ:**
- AI๊ฐ ์๋ ฅ ๊ธ์ฆ ๊ฐ์ง (125.5 kPa)
- AI ๊ถ์ฅ: IMMEDIATE_SHUTDOWN
**์ํฌํ๋ก์ฐ:**
1. ๐ค AI ๋ถ์ โ HIGH ์ํ๋
2. โธ๏ธ ์ ๋ฌธ๊ฐ ๊ฒํ ์์ฒญ
3. ๐ค ์ ๋ฌธ๊ฐ ํ๋จ: "์ฆ์ ์ค์ง๋ณด๋ค ๋ถํ ๊ฐ์๊ฐ ์ ์ "
4. โ๏ธ ์ ๋ฌธ๊ฐ Override โ REDUCE_LOAD ์ ํ
5. ๐ ๋ถํ ๊ฐ์ ์คํ
6. ๐ ๊ฐ์ฌ ๊ธฐ๋ก: "AI ๊ถ์ฅ ์์ , ์ ๋ฌธ๊ฐ B, ๋ถํ ๊ฐ์"
**์ํฉ:**
- ๋ชจ๋ ์ผ์ ์ ์ ๋ฒ์
- ์ํ๋: LOW
**์ํฌํ๋ก์ฐ:**
1. ๐ค AI ๋ถ์ โ LOW ์ํ๋
2. โ
์๋ ์น์ธ (์ ๋ฌธ๊ฐ ๊ฐ์
์์ด ๋ฐ๋ก ์คํ)
3. ๐๏ธ ๋ชจ๋ํฐ๋ง ๊ณ์
4. ๐ ์๋ ๊ธฐ๋ก: "์๋ ์น์ธ, ์ ์ ๋ฒ์"
| ์นดํ
๊ณ ๋ฆฌ | ๊ธฐ์ |
|----------|------|
| **์ธ์ด** | Python 3.10+ |
| **ํ๋ ์์ํฌ** | LangGraph |
| **์ํ ๊ด๋ฆฌ** | LangGraph Checkpointer (MemorySaver) |
| **๋น๋๊ธฐ** | asyncio |
| **ํ์
** | typing, typing_extensions |
```txt
langgraph>=0.2.0
langchain-core>=0.3.0
typing-extensions>=4.5.0
```
-
โ HITL ๊ธฐ๋ณธ ํจํด
interrupt()๋ก ์ฌ์ฉ์ ์ ๋ ฅ ๋๊ธฐCommand(resume=value)๋ก ์ฌ๊ฐ
-
โ ์กฐ๊ฑด๋ถ ๋ผ์ฐํ
Command(goto="node_name")๋ก ๋์ ์ด๋- ์ํ์ ๋ฐ๋ฅธ ๋ถ๊ธฐ ์ฒ๋ฆฌ
-
โ ๋น๋๊ธฐ ์คํธ๋ฆฌ๋ฐ
astream()์ ์ด๋ฒคํธ ์ธํจํน__interrupt__๊ฐ์ง ๋ฐ ์ฒ๋ฆฌ
-
โ ์ํ ๊ด๋ฆฌ
- Checkpointer๋ฅผ ํตํ ์ธ์ ์ ์ง
thread_id๋ก ์ธ์ ์๋ณ
๋ฌธ์ : ๋น๋๊ธฐ ์คํธ๋ฆฌ๋ฐ์์ interrupt ๊ฐ์ง ์ ๋จ
# โ ์๋ชป๋ ์ธํจํน
async for mode, *rest in graph.astream(...):
chunk = rest[0] # ๊ตฌ์กฐ ๋ถ์ผ์น
# โ
์ฌ๋ฐ๋ฅธ ์ธํจํน
async for event in graph.astream(...):
if isinstance(event, tuple):
_, content = event
chunk = content[1] if isinstance(content, tuple) else content
else:
chunk = event-
SQLite Checkpointer ์ ํ
from langgraph.checkpoint.sqlite import SqliteSaver checkpointer = SqliteSaver.from_conn_string("qa_checkpoints.db")
- ์ธ์ ์๊ตฌ ์ ์ฅ
- ํ๋ก์ธ์ค ์ฌ์์ ํ ๋ณต๊ตฌ
-
์๋ฌ ์ฒ๋ฆฌ ๊ฐํ
- ์ผ์ ์ฐ๊ฒฐ ์คํจ ์ฒ๋ฆฌ
- ํ์์์ ์ค์
- ์ฌ์๋ ๋ก์ง
-
์๋ฆผ ์์คํ ํตํฉ
# ์ ๋ฌธ๊ฐ์๊ฒ ์ด๋ฉ์ผ/Slack ์๋ฆผ send_notification( expert="safety_officer@company.com", subject=f"๊ธด๊ธ: {facility_id} ์น์ธ ํ์", priority="HIGH" )
-
์น UI ๊ฐ๋ฐ
- FastAPI ๋ฐฑ์๋
- React ํ๋ก ํธ์๋
- ์ค์๊ฐ ์ผ์ ๋์๋ณด๋
-
์ค์ ์ผ์ ์ฐ๋
# SCADA/IoT ์์คํ ์ฐ๊ฒฐ sensor_data = scada_client.read_tags([ "TEMP_SENSOR_01", "PRESSURE_GAUGE_02" ])
-
๋ค์ค ์ ๋ฌธ๊ฐ ์น์ธ
# CRITICAL์ 2๋ช ์น์ธ ํ์ approval_1 = interrupt({"expert": "shift_manager"}) approval_2 = interrupt({"expert": "safety_officer"})
-
ML ๋ชจ๋ธ ํตํฉ
- ์ด์ ์งํ ์์ธก
- ์๊ณ๊ฐ ์๋ ์กฐ์
-
PostgreSQL/MySQL ์ ํ
- ์ํฐํ๋ผ์ด์ฆ๊ธ DB
- ๊ณ ๊ฐ์ฉ์ฑ
-
๊ฐ์ฌ ๋ฆฌํฌํ
- ์น์ธ ์ค์์จ ๋์๋ณด๋
- ์์ฌ๊ฒฐ์ ๋ถ์
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'feat: add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
MIT License - ์์ ๋กญ๊ฒ ์ฌ์ฉ, ์์ , ๋ฐฐํฌ ๊ฐ๋ฅ
์ง๋ฌธ์ด๋ ์ ์์ฌํญ์ด ์์ผ์๋ฉด Issue๋ฅผ ์ด์ด์ฃผ์ธ์!
Happy Learning! ๐