2. LangGraph 기본기

실행을 멈추고 사람에게 묻기

이 차시를 마치면

사람 승인을 넣는 자리를 정하고, 승인 앞에 두면 안 되는 코드를 가려냅니다.

모델이 실제 행동을 하기 전에 사람이 승인하도록 만드는 방법입니다. 보안 특화 Agent 의 핵심이 여기 있습니다.

목적

Human-in-the-loop 의 목적은 모델이 실제 외부 action 을 수행하기 전에 사람의 승인, 수정, 추가 입력을 받는 것입니다.

기본 패턴 다섯 단계

  1. 그래프를 Checkpointer 와 함께 compile
  2. thread_id 를 설정
  3. Node 내부에서 interrupt(payload)
  4. caller 가 중단 payload 확인
  5. Command(resume=value) 로 같은 Thread 를 재실행

어디에 쓰는가

위험도작업
높음 — 승인 필요이메일 실제 발송, DB 삭제, 외부 게시, 결제, 권한 변경, 채용과 평가 등 민감 판단
낮음 — 승인 불필요초안 생성, 요약, 분류 후보 제안, 검색어 추천

두 목록을 가르는 기준은 되돌릴 수 있는가입니다. 초안은 지우면 되지만 발송한 메일은 회수되지 않습니다. 새 기능을 추가할 때 이 질문 하나로 등급을 정할 수 있습니다. 분석

공식 문서가 특히 주의하라고 안내하는 세 가지

  1. interrupt 호출 순서 변경
  2. 직렬화할 수 없는 payload 반환
  3. interrupt 전에 되돌릴 수 없는 side effect 를 수행

3번이 왜 위험한가

이메일을 먼저 보내고 그다음에 interrupt 를 걸면 어떻게 될까요.

재개 과정에서 Node 가 처음부터 다시 실행될 수 있습니다. 그러면 이메일이 두 번 나갑니다. 승인을 받기 위해 멈춘 것인데, 멈추기 전에 이미 되돌릴 수 없는 일이 벌어진 상태입니다.

비유

결재를 올리기 전에 먼저 돈을 보내고 나서 "이거 보내도 될까요?"라고 묻는 것과 같습니다. 답이 "안 됩니다"여도 돈은 이미 나갔습니다. 게다가 결재 시스템이 재시도를 하면 한 번 더 나갑니다.

안전한 순서

draft 생성  →  interrupt(승인 요청)  →  승인 후에만 side effect

실제 발송, 삭제, 결제 코드는 승인 뒤에 오는 별도 Node 안에 둡니다. 그리고 그 Node 는 같은 작업이 두 번 실행되지 않도록 실행 기록을 남겨 보호합니다.

from langgraph.types import interrupt


def approval(state) -> dict:
    """사람에게 승인을 요청하고 멈춘다.

    payload 에는 직렬화 가능한 값만 넣는다.
    """
    decision = interrupt(
        {
            "question": "이 작업을 실행할까요?",
            "risk_level": state["risk_level"],
            "draft": state["draft"],
            "options": ["approve", "edit", "reject"],
        }
    )
    return {"approved": decision == "approve"}

위 코드는 개념을 보이려고 줄인 것입니다. 다만 interrupt 로 멈추고 Command(resume=...) 로 이어지는 동작 자체는 2026-08-15 에 langgraph 1.2.11 로 확인했습니다. 승인·반려에 따라 경로가 갈리는 것까지 테스트 12개가 덮습니다. 돌아가는 전체 코드는 4번 트랙 5차시에, 직접 돌리는 방법은 8차시에 있습니다.

빠뜨리기 쉬운 것 — Checkpointer

승인 절차에는 Checkpointer 가 반드시 있어야 합니다. 그런데 이걸 빠뜨려도 처음에는 잘 되는 것처럼 보입니다. 여기서 많이 속습니다.

Checkpointer 없이 interrupt 를 부르면 멈추기는 합니다. 결과에 __interrupt__ 도 들어옵니다. 여기까지만 시험해 보고 "다 됐다"고 넘어가기 쉽습니다.

{'v': '', '__interrupt__': [Interrupt(value={'q': '승인?'}, id='351efe...')]}

문제는 사람이 승인한 뒤 이어서 하려는 순간 드러납니다.

RuntimeError: Cannot use Command(resume=...) without checkpointer

멈추는 것과 이어서 하는 것은 다른 일입니다. 멈추는 데는 저장이 필요 없지만, 이어서 하려면 멈춘 지점의 상태가 어딘가에 남아 있어야 합니다. 그 자리가 Checkpointer 입니다.

위 두 출력은 2026-08-15 에 langgraph 1.2.11 로 직접 확인한 것입니다. Checkpointer 없이 interrupt 만 부른 경우와, 그 뒤 Command(resume=...) 까지 부른 경우를 각각 돌렸습니다.

그래서 승인 절차를 시험할 때는 멈추는 것까지가 아니라 이어서 끝나는 것까지 확인해야 합니다. 4번 트랙 5차시의 그래프가 그렇게 되어 있습니다.

이 원칙은 규칙으로 굳어 있습니다

"승인 전에 되돌릴 수 없는 일을 하지 않는다"는 3번 트랙에서 절대 유지해야 하는 안전 불변조건으로 다시 나옵니다. prompt 를 고치기 편하다는 이유로 없애면 안 되는 항목입니다.

전체 구현은 4번 트랙 "보안 특화"에서 코드로 다룹니다.

이해도 확인

문항을 불러오는 중입니다.