실무 Multi-Agent 오케스트레이션 23장 · Human-in-the-Loop 3 / 7 ← 이전목차다음 → TechLead Cro

23장. Human-in-the-Loop

interrupt — 그래프를 멈추고 이어 가기

한 줄 요약

LangGraph의 interrupt 는 그래프를 그 자리에서 멈추고 바깥에 질문을 내보냅니다. 멈춘 상태는 체크포인터(checkpointer) 가 저장하고, 같은 thread_id 로 Command(resume=값)을 보내면 멈춘 곳부터 이어서 실행됩니다.


1. 세 가지 부품

부품 하는 일 코드
interrupt(내보낼 값) 노드 안에서 부르면 그래프가 멈춘다. 넘긴 값은 그래프 밖으로 나간다 decision = interrupt({...})
체크포인터 멈춘 순간의 상태를 저장한다. 없으면 멈출 수 없다 builder.compile(checkpointer=InMemorySaver())
Command(resume=값) 저장된 상태를 불러와 이어서 실행한다 graph.invoke(Command(resume="approve"), config=config)

여기에 실행을 구분하는 이름표 하나가 더 필요합니다.

config = {"configurable": {"thread_id": "refund-C003-001"}}

thread_id 는 "어느 실행을 이어 갈 것인가"를 가리키는 이름입니다. 환불 요청이 동시에 여러 건 멈춰 있어도, 이름이 다르면 서로 섞이지 않습니다. 멈출 때와 이어 갈 때 같은 thread_id를 써야 합니다.


2. 멈추는 쪽 — 노드 안

def approval_gate(state: RefundState) -> dict:
    decision = interrupt({
        "type": "refund_approval",
        "order_id": state["order_id"],
        "action_id": state["action_id"],
        "review": state["review"],
        "ask": "이 환불을 실행할까요? (approve / reject)",
    })
    return {"approved": decision == "approve"}

이 함수는 두 번에 걸쳐 실행됩니다.

때 일어나는 일
처음 실행 interrupt(...)에서 멈춘다. 괄호 안의 값이 밖으로 나간다. return까지 가지 못한다
이어 갈 때 interrupt(...)가 이번에는 멈추지 않고 resume으로 받은 값을 돌려준다. 그 값이 decision에 담기고 return까지 간다

밖에서 사람이 넣어 준 결정이, 노드 안에서는 interrupt()가 돌려준 값으로 나타나는 것입니다.

내보내는 값에 action_id 가 들어 있는 것을 봐 두세요. 검토 노드에서 request_refund가 등록한 승인 요청의 처리번호(A로 시작)입니다. 상담원이 보는 요청서와 처리 기록의 한 줄이 이 번호로 이어집니다.


3. 이어 가는 쪽 — 그래프 밖

# 첫 실행: 승인 게이트에서 멈춘다
result = graph.invoke({...처음 상태...}, config=config)

if "__interrupt__" in result:                       # 멈췄다는 표시
    payload = result["__interrupt__"][0].value      # interrupt 에 넘겼던 값
    # ... 상담원 화면에 payload 를 보여 준다 ...

    # 상담원의 결정을 넣어 이어서 실행한다
    result = graph.invoke(Command(resume="approve"), config=config)
줄 뜻
"__interrupt__" in result 그래프가 끝까지 가지 못하고 멈췄다는 표시
result["__interrupt__"][0].value interrupt(...)에 넘겼던 값. 상담원에게 보여 줄 내용
Command(resume="approve") 처음 상태 대신 "이어서 실행하라"는 명령을 넣는다
config=config 같은 thread_id 여야 한다

수업에서는 한 파일 안에서 곧바로 이어 가지만, 두 invoke 사이에 얼마든지 시간이 흘러도 됩니다. 그 사이를 체크포인터가 메웁니다.


4. 알아 둘 규칙 네 가지

LangGraph 공식 문서(https://docs.langchain.com/oss/python/langgraph/interrupts)가 밝히는 규칙입니다.

규칙 내용 우리 코드에서
체크포인터가 있어야 한다 상태를 저장할 곳이 없으면 멈췄다 이어 갈 수 없다 compile(checkpointer=InMemorySaver())
같은 thread_id로 이어 간다 다른 이름을 주면 새 실행으로 본다 config를 그대로 다시 쓴다
이어 갈 때 노드는 처음부터 다시 실행된다 interrupt가 있던 줄에서 이어지는 것이 아니다. 그 노드의 첫 줄부터 다시 돈다 approval_gate에는 interrupt 앞에 다른 일이 없다
내보내는 값은 단순한 자료여야 한다 문자열, 숫자, 딕셔너리, 리스트처럼 JSON으로 바꿀 수 있는 값 딕셔너리 하나를 넘긴다

세 번째 규칙이 설계에 영향을 줍니다. 만약 검토와 interrupt를 한 노드에 넣었다면, 상담원이 승인하는 순간 검토가 한 번 더 실행됩니다. 승인 요청을 등록하는 코드가 다시 돌고, LLM을 다시 부르니 비용이 들고, 상담원이 본 것과 다른 요청서가 만들어집니다.

그래서 우리 그래프는 검토 노드와 승인 노드를 나눴습니다. 검토 결과(요청서와 action_id)는 상태에 저장되어 있으므로, 이어 갈 때는 승인 노드만 다시 돕니다.


5. 체크포인터 — 저장은 어디에 되는가

수업에서 쓰는 InMemorySaver 는 이름 그대로 상태를 메모리에 저장합니다.

저장소 저장 위치 프로그램을 끄면
InMemorySaver 실행 중인 프로그램의 메모리 사라진다
데이터베이스 기반 체크포인터 디스크의 데이터베이스 남는다

따라서 지금 만드는 것은 "프로그램이 실행되는 동안"의 멈춤과 이어 가기입니다. 상담원이 다음 날 승인해도 이어지게 하려면 체크포인터를 데이터베이스에 저장하는 방식으로 바꿔야 합니다. 그래프의 나머지 코드는 그대로 둡니다.

체크포인터와 처리 기록은 서로 다른 것을 저장합니다. 헷갈리지 않게 나눠 둡니다.

무엇을 저장하나 어디에 프로그램을 끄면
체크포인터 (InMemorySaver) 그래프의 실행 상태 (어느 노드에서 멈췄나, 상태 값) 메모리 사라진다
처리 기록 (memory_store/actions.json) 승인 요청과 그 결과 (승인대기 → 승인완료 / 반려) 파일 남는다

16장과 나란히 놓으면 이렇습니다.

무엇을 저장하나 그래서 가능해지는 것
16장 대화 이력 대화를 이어 간다
23장 그래프의 실행 상태 실행을 멈췄다 이어 간다

6. 그래프 전체 모양

시작 → [검토] ──(요건 충족)──→ [승인 게이트] → [결과 기록] → 끝
          │                     여기서 멈춤
          └──(요건 미달)──→ [이유 안내] → 끝
노드 누구의 일 배운 곳
검토 review 코드(기간 판정, 승인 요청 등록) + AI(사유 분류·요청서) 18장 (노드), 21장 (request_refund)
갈림길 route_after_review 코드 19장 (조건 분기)
승인 게이트 approval_gate 사람 이번 장 (interrupt)
결과 기록 execute 코드 21장 (decide)
이유 안내 reject 코드

새로 배우는 것은 interrupt 하나입니다. 나머지는 18장과 19장의 부품, 그리고 21장에서 만든 처리 도구입니다.


핵심 정리

  • interrupt(값) 은 그래프를 멈추고 값을 밖으로 내보냅니다.
  • 체크포인터가 멈춘 상태를 저장합니다. 없으면 interrupt를 쓸 수 없습니다.
  • 같은 thread_id 로 Command(resume=값)을 보내면 이어서 실행되고, 그 값이 interrupt()의 반환값이 됩니다.
  • 이어 갈 때 노드는 처음부터 다시 실행됩니다. 그래서 검토와 승인을 다른 노드로 나눕니다.
  • InMemorySaver는 프로그램을 끄면 사라집니다. 오래 기다리려면 데이터베이스 기반으로 바꿉니다.
← 이전 절AI의 일과 사람의 일 — 준비는 코드와 AI가, 결정은 사람이다음 절 →요청서 — 사람이 바로 판단하게 만든다
오명운 · macro@prag-ai.com