인간 개입
인간 개입 (Human-in-the-Loop)
인간 개입(Human-in-the-Loop, HITL) 패턴은 에이전트가 특정 지점에서 멈추고 사람의 승인을 받은 뒤 다시 실행을 이어가는 방식이에요. 이메일 발송, 레코드 삭제 같은 되돌리기 어려운 작업이 있을 때 에이전트가 중요한 결정을 내리기 전에 사람이 개입할 지점을 만들어줍니다.
HITL은 LangGraph의 인터럽트(interrupt)와 체크포인트(checkpoint) 위에 구축되기 때문에 이 멈춤은 영속적(durable) 이에요. 사용자가 페이지를 새로고침해도, 리뷰어가 다른 컴포넌트에서 응답해도, 에이전트는 전체 런을 다시 재생하지 않고 중단된 정확한 지점에서 재개됩니다.
인터럽트가 동작하는 방식 (How interrupts work)
LangGraph 에이전트는 인터럽트(interrupt) 를 지원해요. 에이전트가 제어를 클라이언트로 양보하는 명시적 일시정지 지점이죠. 에이전트가 인터럽트를 만나면:
- 에이전트가 실행을 멈추고 인터럽트 페이로드를 내보내요.
useStream훅이stream.interrupt로 인터럽트를 노출합니다.- UI가 승인(approve)/거부(reject)/편집(edit) 옵션을 갖춘 리뷰 카드를 렌더링해요.
- 사용자가 결정을 내립니다.
- 여러분의 코드가 resume 명령과 함께
stream.submit()을 호출해요. - 에이전트가 멈췄던 지점부터 이어받아 진행합니다.
useStream 설정하기 (Setting up useStream)
useStream을 HITL 에이전트에 연결하세요. 그래프가 인터럽트에 걸리면 훅이 대기 중인 페이로드를 stream.interrupt에 노출합니다. 그 값이 설정되어 있는 동안 승인 카드를 렌더링하고, resume 명령으로 런을 재개하면 돼요.
참고: 코드 예제는 타입 안전한 스트림 상태를 위해
useStream<typeof myAgent>를 사용합니다. 타입 추론은 Python 또는 JavaScript 백엔드 문서를 참고하세요.
인터럽트 페이로드 (The interrupt payload)
에이전트가 일시정지하면 stream.interrupt에는 HITLRequest가 담겨요. 구조는 다음과 같습니다.
allowedDecisions: ("approve" | "reject" | "edit" | "respond")[];
}
| 속성 | 설명 |
|---|---|
actionRequests |
에이전트가 수행하려는 대기 중인 행동들의 배열 |
actionRequests[].name |
행동 이름 (예: "send_email", "delete_record") |
actionRequests[].description |
행동이 무엇을 하는지 설명하는 선택적 사람이 읽을 수 있는 설명 |
reviewConfigs |
어떤 결정이 허용되는지 제어하는 행동별 설정 |
reviewConfigs[].allowedDecisions |
표시할 버튼들: "approve", "reject", "edit", "respond" |
재개 흐름 (The resume flow)
사용자가 결정을 내린 뒤 전체 주기는 다음과 같아요.
stream.submit(null, { command: { resume: hitlResponse } })를 호출해요.useStream훅이 resume 명령을 LangGraph 백엔드로 보냅니다.- 에이전트가
HITLResponse를 받고 실행을 계속해요. 각 항목은 ...
재개할 수 있는 결정의 종류로는 이런 것들이 있습니다.
{ type: "approve" }— 행동이 그대로 실행됩니다.{ type: "reject" }— 행동이 실행되지 않아요.{ type: "edit", args }— 수정된 인자로 행동을 다시 실행합니다.{ type: "respond", message }— 도구를 실행하지 않고 사람의 메시지를 바로 도구 결과로 돌려줍니다.
- 에이전트가 스트리밍을 재개하면
interrupt속성은null로 리셋됩니다.
여러 개의 대기 행동 처리하기 (Handling multiple pending actions)
에이전트가 여러 행동을 한 번에 수행하려 할 때 인터럽트에는 복수의 actionRequests가 담길 수 있어요. 각각에 대해 카드를 렌더링하고, 재개하기 전에 모든 결정을 모아야 합니다.
function MultiActionReview({
interrupt,
onRespond,
}: {
interrupt: { value: HITLRequest };
onRespond: (response: HITLResponse) => void;
}) {
// interrupt.value.actionRequests의 각 항목마다 카드를 렌더링하고,
// onRespond로 모든 결정을 한 번에 제출한다.
}
커스텀 인터럽트 폼 (Custom interrupt forms)
단순한 승인/거부를 넘어, 도구마다 서로 다른 폼을 렌더링하고 싶을 수 있어요. 항공권 예약, 환불 승인, 콘텐츠 리뷰 같은 다양한 형식을 인터럽트 페이로드에 설명하고, 프론트엔드가 그에 맞는 폼을 렌더링하는 방식이죠.
인터럽트 페이로드에 폼 설명하기
각 도구에 고유한 formType(예: "refund-approval", "content-review")을 부여하면 프론트엔드가 그것을 기준으로 스위칭해 일치하는 폼을 렌더링할 수 있어요.
import { createAgent, tool } from "langchain";
import { interrupt } from "@langchain/langgraph";
import { z } from "zod";
export interface FormField {
name: string;
label: string;
}
formType: "flight-booking" | "refund-approval" | "content-review";
tool: string;
title: string;
context: Record<string, unknown>;
fields: FormField[];
도구별로 다른 폼 렌더링하기
프론트엔드에서 card.formType을 스위칭해서 각 도구마다 적절한 InterruptForm을 렌더링합니다.
))}
{card && <InterruptForm card={card} onResolve={handleResolve} />}
</div>
);
}
// `InterruptForm`은 card.formType에 따라 항공편 / 환불 / 콘텐츠 카드를 렌더링한다.
respond(decision, { update })로 카드를 화면에 유지하기
결정을 처리하면서 카드를 그 상태와 함께 메시지 형태로 같은 슈퍼스텝(superstep)에 담아 화면에 유지할 수도 있어요. useStream의 respond를 사용하죠.
import { AIMessage } from "langchain";
function handleResolve(decision: ReviewDecision) {
// 결정이 반영된 카드를 스냅샷해서 읽기 전용으로 렌더링한다.
const resolvedCard = { ...card, resolved: true, decision };
const cardMessage = new AIMessage({
content: `Review ${decision.approved ? "approved" : "declined"}.`,
});
// respond(decision, { update })으로 카드를 같은 슈퍼스텝에서 상태에 담아 보낸다.
}
모범 사례 (Best practices)
- 명확한 맥락 표시 — 에이전트가 무엇을, 왜 하려는지 항상 보여주세요. 행동 설명과 전체 인자를 포함하죠.
- 승인을 가장 쉬운 경로로 — 행동이 올바르면 승인은 한 번의 클릭이어야 해요. 여러 단계 흐름은 거부/편집에 아껴두세요.
- 편집된 인자 검증 — 사용자가 행동 인자를 편집하면 보내기 전에 JSON 구조를 검증하고, 잘못된 입력엔 인라인 에러를 보여주세요.
- 인터럽트 상태 영속화 — 사용자가 페이지를 새로고침해도 인터럽트가 여전히 보여야 해요.
useStream이 스레드의 체크포인트로 이를 처리합니다. - 모든 결정 로깅 — 감사 추적을 위해 모든 승인/거부/편집 결정을 타임스탬프와 결정한 사용자와 함께 로그로 남기세요.
- 타임아웃 신중히 설정 — 오래 실행되는 에이전트가 인간 리뷰를 무한정 기다리면 안 돼요. 에이전트가 얼마나 기다렸는지 표시하는 것도 고려하세요.