> EulerAgent > 튜토리얼 > 그래프 > 그래프 05. Interrupt Hooks — 노드 전...

그래프 05. 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_beforeinterrupt_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를 활용한 그래프를 작성하세요.

  1. 최상위에 checkpointer: memory 선언 (필수)
  2. gather 노드: 데이터 수집 (execute mode)
  3. analyze 노드: 분석 실행 (execute mode, interrupt_after: true — 분석 결과 검토)
  4. recommend 노드: 추천사항 생성 (execute mode)
  5. 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

← 이전 목록으로 다음 →