인간 개입

인간 개입 (Human-in-the-Loop)

인간 개입(Human-in-the-Loop, HITL) 패턴은 에이전트가 특정 지점에서 멈추고 사람의 승인을 받은 뒤 다시 실행을 이어가는 방식이에요. 이메일 발송, 레코드 삭제 같은 되돌리기 어려운 작업이 있을 때 에이전트가 중요한 결정을 내리기 전에 사람이 개입할 지점을 만들어줍니다.

HITL은 LangGraph의 인터럽트(interrupt)와 체크포인트(checkpoint) 위에 구축되기 때문에 이 멈춤은 영속적(durable) 이에요. 사용자가 페이지를 새로고침해도, 리뷰어가 다른 컴포넌트에서 응답해도, 에이전트는 전체 런을 다시 재생하지 않고 중단된 정확한 지점에서 재개됩니다.

출처: LangChain 공식 문서 — human-in-the-loop

인터럽트가 동작하는 방식 (How interrupts work)

LangGraph 에이전트는 인터럽트(interrupt) 를 지원해요. 에이전트가 제어를 클라이언트로 양보하는 명시적 일시정지 지점이죠. 에이전트가 인터럽트를 만나면:

  1. 에이전트가 실행을 멈추고 인터럽트 페이로드를 내보내요.
  2. useStream 훅이 stream.interrupt로 인터럽트를 노출합니다.
  3. UI가 승인(approve)/거부(reject)/편집(edit) 옵션을 갖춘 리뷰 카드를 렌더링해요.
  4. 사용자가 결정을 내립니다.
  5. 여러분의 코드가 resume 명령과 함께 stream.submit()을 호출해요.
  6. 에이전트가 멈췄던 지점부터 이어받아 진행합니다.

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)

사용자가 결정을 내린 뒤 전체 주기는 다음과 같아요.

  1. stream.submit(null, { command: { resume: hitlResponse } })를 호출해요.
  2. useStream 훅이 resume 명령을 LangGraph 백엔드로 보냅니다.
  3. 에이전트가 HITLResponse를 받고 실행을 계속해요. 각 항목은 ...

재개할 수 있는 결정의 종류로는 이런 것들이 있습니다.

  • { type: "approve" } — 행동이 그대로 실행됩니다.
  • { type: "reject" } — 행동이 실행되지 않아요.
  • { type: "edit", args } — 수정된 인자로 행동을 다시 실행합니다.
  • { type: "respond", message } — 도구를 실행하지 않고 사람의 메시지를 바로 도구 결과로 돌려줍니다.
  1. 에이전트가 스트리밍을 재개하면 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)에 담아 화면에 유지할 수도 있어요. useStreamrespond를 사용하죠.

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이 스레드의 체크포인트로 이를 처리합니다.
  • 모든 결정 로깅 — 감사 추적을 위해 모든 승인/거부/편집 결정을 타임스탬프와 결정한 사용자와 함께 로그로 남기세요.
  • 타임아웃 신중히 설정 — 오래 실행되는 에이전트가 인간 리뷰를 무한정 기다리면 안 돼요. 에이전트가 얼마나 기다렸는지 표시하는 것도 고려하세요.

더 알아보기 (Learn more)