LangGraph의 Human-in-the-Loop 체크포인트 구현
요약
LangGraph의 interrupt() 기능을 활용하여 Human-in-the-Loop 승인 프로세스를 효율적으로 구현하는 방법을 설명합니다. 기존의 폴링 방식과 달리 프로세스 점유 없이 상태를 영속화하여 리소스를 절약하는 아키텍처를 제안합니다.
핵심 포인트
- interrupt()를 통해 그래프 실행을 일시 중지하고 상태를 영속화할 수 있음
- 기존 폴링 방식 대비 스레드 및 컨테이너 자원 점유를 최소화함
- Impri와 결합하여 비동기적인 승인 및 재개 워크플로우 구축 가능
- 실제 운영 환경에서는 MemorySaver 대신 영구 체크포인터 사용 권장
LangGraph의 interrupt()는 그래프를 실행 도중 일시 중지하고 그 상태를 유지합니다. 이를 Impri와 결합하면 승인 과정이 단순히 대기하는 프로세스를 필요로 하지 않게 됩니다.
폴링(Polling) 도구와의 차이점
대부분의 프레임워크 통합에서 사용되는 일반적인 Human-in-the-loop 패턴은, 하나의 도구가 Impri로 액션을 푸시한 다음, 상태가 변경될 때까지 몇 초마다 GET /v1/actions/:id를 호출하는 루프 안에 머무르는 방식입니다. 이것도 작동하지만, 승인에 걸리는 시간(분 또는 하루) 동안 프로세스(그리고 스레드, 그리고 컨테이너)를 점유하게 됩니다.
LangGraph는 이를 위한 다른 기본 원시 요소(primitive)인 interrupt()를 가지고 있습니다. 노드 내부에서 호출되면, 정확히 그 지점에서 그래프 실행을 일시 중지하고 — 그래프가 체크포인터로 컴파일되었기 때문에 — 나중에 재개하는 데 필요한 모든 것을 영속화합니다. 이 호출하는 프로세스는 반환하거나, 종료하거나, 재배포할 수 있습니다. 아무것도 차단되지 않습니다. 재개는 별도의 호출, 즉 graph.invoke(new Command({ resume: value }), config)를 사용하며, 결정이 실제로 준비될 때마다, 동일한 thread_id로 키가 지정되어 실행됩니다.
이는 Impri에 깔끔하게 매핑됩니다. 액션을 푸시하고, 그 ID와 함께 interrupt()를 호출한 다음, 외부의 무언가(크론 작업, 웹훅 핸들러 등)가 Impri가 결정을 보고하면 그래프를 재개하도록 내버려 두는 것입니다.
그래프 구축하기
환불을 처리할 수 있는 지원 티켓 에이전트에서 환불 노드가 게이팅(gated)되는 예시:
import { Annotation, Command, END, MemorySaver, START, StateGraph, interrupt } from "@langchain/langgraph";
const IMPRI_BASE = "https://api.impri.dev";
...
환불 금액은 의도적으로 수정할 수 없도록 했습니다. editable 속성은 마크다운 텍스트인 preview.body만 재작성하며, 구조화된 필드는 아닙니다. 달러 금액을 편집 가능하게 만들려면 이를 산문에서 다시 파싱해야 하는데, 이는 불안정합니다. 검토자들에게는 사유를 수정하도록 하고, 금액은 에이전트가 이미 계산한 고정 값으로 유지하는 것이 좋습니다.
재개 측면: 결정 폴러
재개 측면: 결정 폴러
Impri의 상태가 변경될 때 그래프 내부로 다시 호출(call back)하는 주체가 여전히 필요합니다. 반드시 실행을 시작한 프로세스일 필요는 없습니다:
async function pollAndResume(threadId: string, actionId: string) {
const res = await fetch(`${IMPRI_BASE}/v1/actions/${actionId}`, {
headers: { Authorization: `Bearer ${IMPRI_API_KEY}` },
...
result.status가 expired인 경우 여기서는 rejected와 동일하게 처리됩니다 — approved는 false이며, issueRefund는 단축(short-circuits)되고 그래프는 결제 제공업체(payment provider)를 호출하지 않은 채로 END에 도달합니다.
내구성(Durability)은 보이는 것보다 중요합니다
MemorySaver는 프로세스 내부에서 작동하며 재시작 시 사라집니다 — 위의 예제에는 괜찮지만, 하룻밤 동안 열려 있을 수 있는 환불 승인 같은 상황에는 그렇지 않습니다. 이 코드가 실제 돈에 닿기 전에는 LangGraph의 영구 체크포인터(Postgres, SQLite) 중 하나로 교체하여, 푸시와 결정 사이에 배포가 발생하더라도 일시 정지된 상태를 유지할 수 있도록 하세요.
Impri가 여기서 하는 것과 하지 않는 것
Impri는 제안된 환불을 저장하고, 사람에게 알리고, 결정을 보류하는 것 외에는 아무것도 하지 않습니다 — 그 이상도 이하도 아닙니다. Impri는 LangGraph가 무엇인지 모르고, 사용자를 위해 resume을 호출하지 않으며, 이 티켓에 $40이 합리적인 환불액인지 결정하지 않습니다. 이 게이트(gate)는 오직 issueRefund가 refundProvider.issue로 가는 유일한 경로일 때만 실질적입니다 — 만약 다른 노드나 다른 에이전트가 그 함수를 직접 호출할 수 있다면, 이 체크포인트는 장식용에 불과합니다.
다음 단계
- Quickstart — API 키를 받고 첫 번째 액션을 푸시하세요
- TypeScript SDK — 원시
fetch호출 대신 타입이 지정된 클라이언트(typed client)를 사용하세요 - Webhooks — 간격으로 폴링하는 대신, 결정이 내려지는 순간 재개를 트리거하세요
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기