그래프 05. Interrupt Hooks — 노드 전후 실행 일시 정지
학습 목표
이 튜토리얼을 마치면 다음을 할 수 있습니다.
interrupt_before: true를 사용해 특정 노드 실행 전에 그래프를 일시 정지할 수 있다.interrupt_after: true를 사용해 특정 노드 실행 후에 그래프를 일시 정지할 수 있다.- HITL
force_tool방식과 interrupt 방식의 차이를 정확히 설명할 수 있다. - 두 interrupt를 동시에 사용하는 패턴을 작성할 수 있다.
graph compile로 IR에서 interrupt_before/after 설정을 확인할 수 있다.- interrupt hooks의 적절한 사용 시나리오를 판단할 수 있다.
사전 준비
mkdir -p examples/graphs/hooks
# LangGraph 체크포인트 기능 확인
python -c "from langgraph.graph import StateGraph; print('LangGraph StateGraph OK')"
예상 출력:
LangGraph StateGraph OK
Interrupt Hooks란?
Interrupt hooks는 LangGraph의 체크포인트(checkpoint) 메커니즘을 활용하여 특정 노드의 실행 전 또는 후에 그래프를 일시 정지합니다.
두 가지 interrupt 타입
| 타입 | 선언 | 일시 정지 시점 | 상태 스냅샷 |
|---|---|---|---|
interrupt_before |
interrupt_before: true |
해당 노드 실행 직전 | 이전 노드까지의 상태 |
interrupt_after |
interrupt_after: true |
해당 노드 실행 직후 | 해당 노드 완료 후 상태 |
노드 실행 타임라인:
interrupt_before: true 선언 시:
이전_노드 → [일시 정지] → 해당_노드 → 다음_노드
interrupt_after: true 선언 시:
이전_노드 → 해당_노드 → [일시 정지] → 다음_노드
HITL force_tool vs interrupt_before/after
두 방식 모두 사람이 개입할 기회를 만들지만 메커니즘과 목적이 다릅니다.
| 항목 | HITL force_tool | interrupt_before/after |
|---|---|---|
| 메커니즘 | euleragent 파일 기반 JSONL 승인 큐 | LangGraph 체크포인트 |
| 목적 | 특정 도구 사용 전 승인 | 노드 전/후 상태 검사 및 재개 |
| 지속성 | 승인 큐 파일에 저장 | LangGraph 체크포인터(메모리/DB) |
| 재개 방법 | euleragent approve |
LangGraph graph.invoke(state) |
| 사용 시나리오 | 웹 검색, 파일 쓰기, 셸 실행 승인 | 중요 노드 상태 검사, 디버깅 |
| Pattern 지원 | O | X (Graph 전용) |
언제 interrupt를 쓸까?
interrupt_before 권장 시나리오:
- publish 노드 전: "정말 배포하시겠습니까?"
- payment 노드 전: "결제를 진행하시겠습니까?"
- email_send 노드 전: "이 이메일을 발송하시겠습니까?"
- 고위험 API 호출 전: 상태 검사
interrupt_after 권장 시나리오:
- research 노드 후: 수집된 데이터 검토
- draft 노드 후: 초안 검토 후 수정 여부 결정
- 디버깅: 중간 상태를 로그로 확인
단계별 실습
단계 1: interrupt_before — human_review 노드
중요한 publish 단계 전에 사람이 검토할 수 있도록 interrupt를 설정합니다.
# examples/graphs/hooks/interrupt_before.yaml
id: graph.interrupt_before_demo
version: 1
category: demo
description: publish 노드 전 interrupt_before로 사람 검토
checkpointer: memory # ← interrupt 사용 시 필수. 미선언 시 INTERRUPT_REQUIRES_CHECKPOINTER 오류
defaults:
max_iterations: 2
max_total_tool_calls: 20
nodes:
- id: research
kind: llm
runner:
mode: plan
force_tool: web.search
guardrails:
tool_call_budget:
web.search: 3
- id: draft
kind: llm
runner:
mode: execute
artifacts:
primary: article.md
- id: human_review
kind: llm
interrupt_before: true # ← 이 노드 실행 전에 그래프 일시 정지
runner:
mode: execute
artifacts:
primary: review_notes.md
- id: publish
kind: llm
runner:
mode: execute
artifacts:
primary: published.md
edges:
- from: research
to: draft
when: "approvals_resolved"
- from: draft
to: human_review
when: "true"
- from: human_review
to: publish
when: "true"
- from: publish
to: finalize
when: "true"
finalize:
artifact: published.md
euleragent graph validate examples/graphs/hooks/interrupt_before.yaml
예상 출력:
단계 3/3: Graph 추가 검증...
[✓] interrupt_before: human_review (1개 노드)
[✓] interrupt_after: 없음
완료
결과: 유효 ✓
참고: interrupt_before가 설정된 노드: [human_review]
실행 시 LangGraph 체크포인터가 필요합니다.
단계 2: graph compile로 IR에서 interrupt_before 확인
euleragent graph compile examples/graphs/hooks/interrupt_before.yaml \
--out /tmp/interrupt_before_ir.json
# IR에서 interrupt 관련 필드 확인
python -m json.tool /tmp/interrupt_before_ir.json | \
python -c "
import sys, json
d = json.load(sys.stdin)
# 각 노드의 interrupt 설정 확인
for node in d['nodes']:
if node.get('interrupt_before') or node.get('interrupt_after'):
print(f\"노드 '{node['id']}': interrupt_before={node['interrupt_before']}, interrupt_after={node['interrupt_after']}\")
# langgraph_builder의 interrupt 설정 확인
print('langgraph_builder.interrupt_before:', d['langgraph_builder']['interrupt_before'])
print('langgraph_builder.interrupt_after:', d['langgraph_builder']['interrupt_after'])
"
예상 출력:
노드 'human_review': interrupt_before=True, interrupt_after=False
langgraph_builder.interrupt_before: ['human_review']
langgraph_builder.interrupt_after: []
IR의 langgraph_builder 섹션에서 interrupt_before와 interrupt_after 리스트를 확인합니다.
이 리스트가 LangGraph graph.compile(interrupt_before=[...], interrupt_after=[...]) 호출에
사용됩니다.
{
"langgraph_builder": {
"add_nodes": ["research", "draft", "human_review", "publish", "finalize"],
"add_edges": [...],
"interrupt_before": ["human_review"],
"interrupt_after": []
}
}
이것이 실행 시 다음 LangGraph 코드로 구체화됩니다.
# euleragent 내부 생성 코드 (개념적)
compiled = graph.compile(
checkpointer=checkpointer, # 상태 저장용 체크포인터
interrupt_before=["human_review"] # human_review 전에 일시 정지
)
단계 3: interrupt_after — draft 노드 후 검토
draft 노드가 완료된 후 생성된 초안을 검토하고 싶을 때 interrupt_after를 사용합니다.
# examples/graphs/hooks/interrupt_after.yaml
id: graph.interrupt_after_demo
version: 1
category: demo
description: draft 노드 후 interrupt_after로 초안 검토
checkpointer: memory # ← interrupt 사용 시 필수
defaults:
max_iterations: 3
max_total_tool_calls: 25
nodes:
- id: research
kind: llm
runner:
mode: execute
- id: draft
kind: llm
interrupt_after: true # ← 이 노드 실행 후 그래프 일시 정지
runner:
mode: execute
artifacts:
primary: draft.md
- id: evaluate
kind: judge
judge:
schema: evaluator_v1
route_values: [finalize, revise]
- id: revise
kind: llm
runner:
mode: execute
artifacts:
primary: draft.md
edges:
- from: research
to: draft
when: "true"
- from: draft
to: evaluate
when: "true"
- from: evaluate
to: finalize
when: "judge.route == finalize"
- from: evaluate
to: revise
when: "judge.route == revise"
- from: revise
to: evaluate
when: "true"
finalize:
artifact: draft.md
euleragent graph validate examples/graphs/hooks/interrupt_after.yaml
예상 출력:
결과: 유효 ✓
참고: interrupt_after가 설정된 노드: [draft]
interrupt_after: true가 있는 draft 노드의 IR 표현입니다.
{
"id": "draft",
"kind": "llm",
"interrupt_before": false,
"interrupt_after": true,
"runner": { "mode": "execute" },
"artifacts": { "primary": "draft.md" }
}
단계 4: 두 interrupt 동시 사용
interrupt_after: true(draft 후)와 interrupt_before: true(publish 전)를 함께 사용합니다.
이렇게 하면 초안 완성 후 검토와, 배포 전 최종 확인 두 번의 개입 지점이 생깁니다.
# examples/graphs/hooks/interrupt_both.yaml
id: graph.interrupt_both_demo
version: 1
category: demo
description: draft 후 + publish 전 두 번의 interrupt
checkpointer: memory # ← interrupt 사용 시 필수
defaults:
max_iterations: 2
max_total_tool_calls: 30
nodes:
- id: research
kind: llm
runner:
mode: execute
- id: draft
kind: llm
interrupt_after: true # ← 초안 완성 후 검토 기회
runner:
mode: execute
artifacts:
primary: content.md
- id: evaluate
kind: judge
judge:
schema: evaluator_v1
route_values: [finalize, revise]
- id: revise
kind: llm
runner:
mode: execute
artifacts:
primary: content.md
- id: prepare_publish
kind: llm
runner:
mode: execute
artifacts:
primary: publish_ready.md
- id: publish
kind: llm
interrupt_before: true # ← 배포 직전 최종 확인
runner:
mode: execute
artifacts:
primary: published.md
edges:
- from: research
to: draft
when: "true"
- from: draft
to: evaluate
when: "true"
- from: evaluate
to: prepare_publish
when: "judge.route == finalize"
- from: evaluate
to: revise
when: "judge.route == revise"
- from: revise
to: evaluate
when: "true"
- from: prepare_publish
to: publish
when: "true"
- from: publish
to: finalize
when: "true"
finalize:
artifact: published.md
euleragent graph validate examples/graphs/hooks/interrupt_both.yaml
예상 출력:
단계 3/3: Graph 추가 검증...
[✓] interrupt_before: publish (1개 노드)
[✓] interrupt_after: draft (1개 노드)
[✓] 두 interrupt 호환성 검사 통과
(같은 노드에 interrupt_before와 interrupt_after 동시 선언 없음)
완료
결과: 유효 ✓
interrupt_after: [draft]
interrupt_before: [publish]
euleragent graph compile examples/graphs/hooks/interrupt_both.yaml \
--out /tmp/interrupt_both_ir.json
python -m json.tool /tmp/interrupt_both_ir.json | \
python -c "
import sys, json
d = json.load(sys.stdin)
lb = d['langgraph_builder']
print('interrupt_before:', lb['interrupt_before'])
print('interrupt_after:', lb['interrupt_after'])
"
예상 출력:
interrupt_before: ['publish']
interrupt_after: ['draft']
이 두 값이 LangGraph graph.compile() 호출에 모두 전달됩니다.
# LangGraph 내부 코드 (개념적)
compiled = graph.compile(
checkpointer=checkpointer,
interrupt_before=["publish"],
interrupt_after=["draft"]
)
단계 5: interrupt와 HITL force_tool 비교 실습
같은 목적(사람 개입)을 달성하는 두 방식을 나란히 비교합니다.
# HITL force_tool 방식 (Pattern 및 Graph 모두 지원)
nodes:
- id: research
kind: llm
runner:
mode: plan # plan 모드에서 tool 사용 계획 제안
force_tool: web.search # 반드시 web.search 도구 사용
# 실행 흐름:
# 1. LLM이 web.search 계획 제안
# 2. euleragent가 approvals.jsonl에 승인 요청 기록
# 3. 사람이 euleragent approve 명령으로 승인
# 4. 승인 후 web.search 실행
# 5. approvals_resolved 조건이 충족됨
# interrupt_before 방식 (Graph 전용)
nodes:
- id: publish
kind: llm
interrupt_before: true # LangGraph 체크포인트로 일시 정지
runner:
mode: execute
# 실행 흐름:
# 1. LangGraph가 publish 노드 직전에 실행 중단
# 2. LangGraph 상태가 체크포인터에 저장
# 3. 사람이 상태 확인 후 graph.invoke()로 재개
# 4. publish 노드 실행 계속
예상 출력 요약
| 명령 | 예상 결과 |
|---|---|
graph validate interrupt_before.yaml |
유효 ✓, interrupt_before: [human_review] |
graph compile interrupt_before.yaml |
IR interrupt_before: ["human_review"] |
graph validate interrupt_after.yaml |
유효 ✓, interrupt_after: [draft] |
graph validate interrupt_both.yaml |
유효 ✓, 두 interrupt 모두 확인 |
graph compile interrupt_both.yaml |
IR에 interrupt_before: ["publish"], interrupt_after: ["draft"] |
핵심 개념 요약
| 개념 | 설명 |
|---|---|
interrupt_before: true |
해당 노드 실행 전 LangGraph 체크포인트 일시 정지 |
interrupt_after: true |
해당 노드 실행 후 LangGraph 체크포인트 일시 정지 |
| 체크포인터 | 일시 정지된 그래프 상태를 저장하는 LangGraph 컴포넌트 |
| HITL force_tool | euleragent 파일 기반 승인 큐 (Pattern + Graph) |
| interrupt | LangGraph 체크포인트 기반 일시 정지 (Graph 전용) |
graph.compile(interrupt_before=[...]) |
IR의 interrupt 설정이 매핑되는 LangGraph API |
흔한 오류
오류 1: 같은 노드에 interrupt_before + interrupt_after 동시 선언
- id: critical_node
kind: llm
interrupt_before: true # ← 전
interrupt_after: true # ← 후 동시에
runner:
mode: execute
경고: INTERRUPT_DOUBLE_DECLARED
노드 'critical_node'에 interrupt_before와 interrupt_after가 모두 true입니다.
이 노드는 실행 전과 후 두 번 일시 정지됩니다.
의도한 동작인지 확인하세요.
이것은 에러가 아닌 경고입니다. 의도한 경우(전과 후 모두 검토)라면 무시해도 됩니다.
오류 2: interrupt를 Pattern에서 사용하려는 시도
# Pattern validate로 interrupt가 있는 Graph YAML 검증
euleragent pattern validate examples/graphs/hooks/interrupt_before.yaml
경고: UNKNOWN_NODE_FIELD
노드 'human_review'에 알 수 없는 필드: interrupt_before
Pattern은 interrupt_before/after를 지원하지 않습니다.
Graph 모듈에서 사용하세요: euleragent graph validate
오류 3: 체크포인터 선언 없이 interrupt 사용
interrupt_before/interrupt_after를 사용하는 그래프는 최상위에 checkpointer: 필드가
반드시 필요합니다. 선언하지 않으면 graph validate 단계에서 즉시 오류가 납니다.
# BAD — checkpointer 없이 interrupt 사용
id: my_graph
version: 1
description: ...
# checkpointer: MISSING ← 이 줄이 없으면 아래 오류 발생
nodes:
- id: review
kind: llm
interrupt_before: true # ← 검증 시 INTERRUPT_REQUIRES_CHECKPOINTER 발생
runner: {mode: execute}
오류: INTERRUPT_REQUIRES_CHECKPOINTER
노드 'review'가 interrupt_before/interrupt_after를 선언했지만
최상위에 checkpointer가 구성되지 않았습니다.
LangGraph는 브레이크포인트에서 일시 정지 후 재개하기 위해
체크포인터가 필요합니다.
최상위에 'checkpointer: true' (또는 'checkpointer: memory') 를 추가하세요.
수정:
# GOOD — checkpointer 선언
id: my_graph
version: 1
description: ...
checkpointer: memory # ← 추가
nodes:
- id: review
kind: llm
interrupt_before: true
runner: {mode: execute}
사용 가능한 값: true(기본 메모리 체크포인터), "memory", "sqlite".
영구 저장이 필요하면 "sqlite"를 사용하고 euleragent 실행 환경에서 경로를 설정하세요.
오류 4: finalize 노드에 interrupt 선언
finalize:
artifact: output.md
interrupt_before: true # ← finalize는 최종 노드 — interrupt 불가
오류: INTERRUPT_ON_FINALIZE
finalize 노드에는 interrupt_before/after를 선언할 수 없습니다.
interrupt는 그래프 실행 중간 노드에만 유효합니다.
finalize 전에 interrupt를 원한다면 finalize 직전 노드에 선언하세요.
실습 과제
과제: 3단계 검토 워크플로우
다음 스펙으로 interrupt hooks를 활용한 그래프를 작성하세요.
- 최상위에
checkpointer: memory선언 (필수) - gather 노드: 데이터 수집 (execute mode)
- analyze 노드: 분석 실행 (execute mode,
interrupt_after: true— 분석 결과 검토) - recommend 노드: 추천사항 생성 (execute mode)
- send_report 노드: 보고서 발송 (execute mode,
interrupt_before: true— 발송 전 최종 확인)
euleragent graph validate examples/graphs/hooks/three_stage_review.yaml
euleragent graph compile examples/graphs/hooks/three_stage_review.yaml
IR에서 다음을 확인하세요.
- interrupt_after: ["analyze"]
- interrupt_before: ["send_report"]
이전: 04_bounded_loop.md | 다음: 06_state_schema.md