본문으로 건너뛰기
페이지 목차
이 페이지에서 검색

멀티 에이전트

이 페이지에 묻기

멀티 에이전트 · 현재 차시와 목차 본문에서만 찾아 답합니다. 답을 지어내지 않고 해당하는 대목을 그대로 보여 주며, 여기에 없으면 없다고 알려 드립니다.

생성형 AI가 아니라 이 페이지 본문을 찾아 그대로 보여 주는 방식입니다. 외부로 나가는 요청이 없습니다.

MULTI · AGENT

멀티 에이전트

생성 수정

도구 실행·검색·기억·그래프 흐름·전문 에이전트의 협업을 구성하고 권한·평가·사람의 확인으로 통제합니다.

26개 문서 · 3일차 · 24 / 26

23차시 · Human-in-the-Loop

지도 위치 : 3일차 · README 4장의 ⑥ 서비스 층(사람의 승인) | 이전: 22차시 오케스트레이션 | 다음: 24차시 서비스 통합과 실행 키워드 : HITL, 인터럽트, 승인 게이트, 에스컬레이션

이 차시를 다 읽으면

  1. HITL을 유와 종차로 정의하고, "사람이 손을 대는가" 축에서 자동 실행과 구별할 수 있다.
  2. 흐름의 어디에 사람을 둘지 네 가지 기준(쓰기 · 되돌릴 수 없음 · 낮은 확신 · 민감정보)으로 고를 수 있다.
  3. 승인 게이트를 LangGraph의 interrupt + 체크포인터 + Command(resume) 로 만드는 원리를 설명할 수 있다.
  4. 승인 화면에 보여 줄 것과, 거절 · 수정 후 재개하는 흐름을 설계할 수 있다.
  5. 에스컬레이션 기준을 정하고, 재개 때 쓰기 작업이 두 번 실행되지 않게 막을 수 있다.

0. 이 차시의 판별 축

#축왼쪽 (0)오른쪽 (1)이 축으로 갈리는 것큰 축 (README 5장)
1사람이 손을 대는가 ⭐사람 승인 후 실행자동 실행실행 전에 사람이 보는가⑦ 무상태 ↔ 상태 (파생)
2읽기 ↔ 쓰기 ⭐조회 — 상태를 안 바꿈, 재시도 안전변경 — 상태를 바꿈, 재시도 위험사람을 둘 자리⑦의 파생
3무상태 ↔ 상태멈추면 처음부터 다시멈춘 State를 저장해 그 자리에서 재개승인을 기다릴 수 있는가⑦ 무상태 ↔ 상태
4디스크 ↔ 메모리체크포인터 저장소 (DB)실행 중인 프로세스의 메모리서버가 재시작돼도 승인 대기가 남는가① 디스크 ↔ 메모리
5부탁 ↔ 강제"신청 전에 사용자에게 확인해"라고 프롬프트에 씀그래프 구조에 승인 노드를 둠승인 없이 실행될 수 있는가②의 파생

이 차시가 답하는 질문은 "에이전트가 되돌릴 수 없는 일을 하기 전에, 어떻게 멈추고 사람에게 묻고 다시 이어 가나?" 입니다. 앞에서 배운 보안(8차시)에서 쓰기 도구는 사람의 확인이 필요하다고 했고, 18차시에서 체크포인터를 예고했습니다. 앞 차시(22차시)에서는 담당이 needs_approval을 돌려주면 "승인 대기"로 답만 했습니다. 이번에는 실제로 멈추고, 저장하고, 승인이 오면 재개 합니다. README 5.2 중심 축에서 보면, 모델이 흐름을 정하는 자리에 사람이 정하는 칸 을 끼워 넣는 일입니다.


1. HITL — 정의와 경계

1.1 정의

HITL (Human-in-the-Loop) — [에이전트 흐름 설계 방식]인데, [ 정해진 지점에서 실행을 멈추고 사람의 판단(승인 · 수정 · 거절)을 받아 그 결과로 흐름을 이어 간다 ]는 점에서 처음부터 끝까지 자동으로 도는 흐름과 구별된다.

자동 실행HITL
쓰기 작업모델이 요청하면 바로 실행사람이 보고 승인해야 실행
실행 시간몇 초사람이 볼 때까지 (몇 분 ~ 며칠)
필요한 것—멈춘 상태를 저장하는 장치 (3절)
틀렸을 때이미 실행됨실행 전에 걸러짐

1.2 반례 — 이건 HITL이 아닙니다

대상왜 아닌가
모델이 "신청할까요?"라고 묻는 답변프롬프트로 부탁한 것. 사용자가 "네"라고 하기 전에 모델이 도구를 부르면 막을 장치가 없음
실행 후 사람에게 알림 메일사람이 사후에 봄. 되돌릴 수 없는 일은 이미 일어남
사람이 대화로 질문하는 것 자체사용자 입력일 뿐, 흐름 중간의 판단 지점이 아님

⭐ 첫 반례가 핵심입니다. "확인하고 신청해"는 부탁 이고, 그래프에 승인 노드를 두어 승인 없이는 신청 노드에 갈 수 없게 만드는 것이 강제 입니다(문장 2 — 모델은 판단하고, 코드는 지킨다).

💡 비유 (기억 보조용) — 결재 서류. 담당자가 기안을 올리면 결재자가 도장을 찍을 때까지 서류가 서랍에 보관 되고, 도장이 찍히면 그 서류 그대로 다음 단계로 갑니다. 서랍이 체크포인터입니다.


2. 어디에 사람을 두나

기준사람을 두는 이유오피스봇 예
쓰기 작업상태를 바꿈. 재시도도 위험장비 신청, 휴가 신청
되돌릴 수 없는 작업실수하면 복구 비용이 큼신청 후 결재선으로 넘어간 건
낮은 확신모델이 스스로 확신하지 못함분류 confidence="low", 계획이 모호함 (19 · 20차시)
민감정보외부로 나가거나 다른 사람에게 보임건강 사유가 든 휴가 신청 사유 (17차시)

⚠️ 모든 곳에 사람을 두면 사람이 읽지 않고 승인 버튼만 누르게 됩니다. 승인 요청이 많을수록 승인의 무게가 가벼워집니다. 읽기 작업(휴가 잔여 조회, 규정 검색)에는 두지 않고, 위 네 기준에 걸리는 곳에만 둡니다.


3. 승인 게이트와 인터럽트

3.1 정의

승인 게이트 — [그래프 노드]인데, [ 사람의 승인 결과가 들어오기 전에는 다음 노드로 가지 못하게 막는다 ]는 점에서 State만 보고 바로 길을 고르는 라우터와 구별된다.

인터럽트 — [실행 제어 방식]인데, [ 그래프 실행을 노드 안에서 멈추고 State를 저장한 뒤, 외부 입력이 오면 그 자리에서 재개한다 ]는 점에서 오류로 실행을 끝내는 중단과 구별된다.

3.2 축 ⑦ — 멈춘 상태를 저장해야 재개할 수 있다

앞에서 배운 대로 LLM API는 무상태입니다(16차시). 그래프 실행도 프로세스 메모리에만 있으면, 승인을 기다리는 몇 시간 사이에 서버가 재시작되면 사라집니다. 그래서 인터럽트에는 체크포인터가 필수 입니다. 18차시에서 예고한 그 장치입니다.

from langgraph.types import interrupt, Command
from langgraph.checkpoint.memory import MemorySaver

def approval_gate(state: State) -> dict:
    decision = interrupt({                               # 여기서 멈추고 State 저장
        "작업": "장비 신청",
        "대상": state["requester_id"],
        "내용": state["draft"],                          # 예: 27인치 모니터 1대
        "되돌릴 수 있는가": "제출 후 취소는 IT팀 문의 필요",
    })
    if decision["action"] == "approve":
        return {"approved": True}
    if decision["action"] == "edit":
        return {"draft": decision["draft"], "approved": True}
    return {"approved": False, "reject_reason": decision.get("reason", "")}

app = g.compile(checkpointer=MemorySaver())              # 체크포인터 없으면 재개 불가
config = {"configurable": {"thread_id": "equip-req-0001"}}

app.invoke({"question": "모니터 하나 더 받고 싶어요", ...}, config)   # → 승인 대기에서 멈춤
# ... 몇 시간 뒤, 승인 화면에서 버튼을 누르면
app.invoke(Command(resume={"action": "approve"}), config)              # → 그 자리에서 재개

🔍 읽는 포인트

  • interrupt({...}) — 그래프가 여기서 멈춥니다. 넘긴 dict가 승인 화면에 보여 줄 내용입니다(4절). 재개되면 resume에 넣은 값이 decision으로 돌아옵니다.
  • checkpointer=MemorySaver() — 멈춘 State를 저장합니다. 단, MemorySaver는 메모리 에 저장하므로 서버가 꺼지면 사라집니다(축 ①의 메모리 쪽). 실습용이고, 운영에서는 SQLite · Postgres 같은 DB 기반 체크포인터 를 씁니다.
  • thread_id — 어느 실행을 재개할지 가리키는 열쇠 입니다. 앞에서 배운 세션 ID(16차시)와 같은 역할이며, 다른 사용자의 thread_id로 재개되지 않도록 코드가 소유자를 확인합니다.
  • Command(resume=...) — 같은 config(같은 thread_id)로 넘겨야 그 자리에서 이어집니다.
  • 멈춘 상태와 대기 중인 인터럽트 내용은 app.get_state(config)나 실행 결과로 확인합니다. 반환 형식은 버전에 따라 다르니 공식 문서를 확인 하세요.

4. 승인 화면에 보여 줄 것

사람이 읽고 판단할 수 있는 화면이어야 승인이 의미를 가집니다(2절 경고).

보여 줄 것예왜
무엇을장비 신청: 27인치 모니터 1대모델이 만든 초안 그대로
누구 대상신청자: 개발팀 김OO다른 사람 이름으로 신청되지 않았는지
되돌릴 수 있는가제출 후 취소는 IT팀 문의승인의 무게를 알게 함
왜 이렇게 만들었나근거: 사용자 발화 원문, 장비 규정 조항모델의 해석이 맞는지
선택지승인 / 수정 / 거절3가지가 다 있어야 함

⚠️ 누가 승인하나 도 코드가 정합니다. 본인 확인형(신청자가 최종 확인)인지, 결재형(팀장이 승인)인지 정하고, 승인 버튼을 누른 사람의 신원은 로그인 세션에서 가져와 권한을 검사합니다(8차시). 모델이 "팀장님이 승인하셨습니다"라고 쓴 글은 승인이 아닙니다.


5. 거절 · 수정 후 재개

사람의 결정resume에 넣는 값그래프의 다음 길
승인{"action": "approve"}submit 노드 → 실행
수정{"action": "edit", "draft": {...}}수정된 초안 으로 submit
거절{"action": "reject", "reason": "이미 지급됨"}사용자에게 거절 사유 안내 → END
응답 없음(없음)보관 기한 뒤 자동 만료, 사용자에게 알림

💡 수정 후에 다시 승인을 받을지 는 정책입니다. 사람이 직접 고친 값이면 바로 실행하고, 수정 내용을 모델이 다시 해석해야 하면 한 번 더 승인 게이트를 거치게 합니다. 이것은 승인 노드 뒤에 조건부 엣지(19차시)로 그립니다.


6. 에스컬레이션

에스컬레이션 — [사람 개입 방식]인데, [ 봇이 처리하지 않고 사람 담당자에게 일 자체를 넘긴다 ]는 점에서 봇이 만든 초안에 사람이 도장만 찍는 승인 게이트와 구별된다.

넘기는 기준예앞에서 나온 자리
범위 밖 · 민감 주제징계, 신고, 연봉 협상19차시 분기 기준
반복 실패되묻기 2회 실패, 재검색 2회 실패, 재계획 실패19 · 20차시 상한
전체 상한 도달supervisor 단계 · 토큰 상한22차시
사용자가 원함"사람이랑 얘기할래요"—

넘길 때는 지금까지의 맥락을 요약해 담당자에게 함께 넘깁니다. 사용자가 같은 말을 처음부터 다시 하게 만들지 않는 것이 에스컬레이션의 품질입니다. 요약에는 17차시의 마스킹을 그대로 적용합니다.


7. ⚠️ 쓰기 도구의 멱등성 — 두 번 실행 막기

LangGraph에서 인터럽트로 멈췄던 노드는 재개될 때 그 노드의 처음부터 다시 실행 됩니다(세부 동작은 공식 문서 확인). 그래서 interrupt 앞에 쓰기 작업을 두면 재개 때 한 번 더 실행됩니다.

위험대응
승인 노드 안, interrupt 앞에서 신청 API 호출쓰기는 승인 노드 뒤의 별도 노드 (submit)에서만
네트워크 오류로 submit을 재시도멱등성 키 — 같은 요청 ID로 두 번 와도 한 번만 처리 (8차시)
사용자가 승인 버튼을 두 번 누름같은 thread_id의 두 번째 resume은 무시 (이미 처리된 상태 확인)
def submit(state: State) -> dict:
    if not state["approved"]:
        return {"answer": f"신청하지 않았습니다. 사유: {state['reject_reason']}"}
    ticket = request_equipment(
        requester_id=state["requester_id"],          # 로그인 세션 값
        item=state["draft"],
        idempotency_key=state["request_id"],         # 같은 키면 시스템이 한 번만 처리
    )
    return {"answer": f"신청했습니다. 접수번호 {ticket.id}"}

🔍 읽는 포인트

  • if not state["approved"] — 거절 · 만료면 실행하지 않는 길 이 먼저입니다.
  • idempotency_key=state["request_id"] — 신청 시스템이 이 키를 기억해 같은 키의 두 번째 요청을 무시합니다. request_id는 흐름이 시작될 때 한 번 만들어 State에 넣어 둡니다. 신청 시스템이 이런 키를 받지 못한다면, 우리 쪽 DB에 "처리한 요청 ID" 기록을 두고 확인합니다.

8. 오피스봇 장비 신청 승인 흐름

"모니터 하나 더 받고 싶어요"
      ↓
 [classify] → 장비신청 (19차시)
      ↓
 [it 에이전트] → 초안 작성, status="needs_approval" (21차시)
      ↓
 [approval_gate] ── interrupt ──→ (State를 DB에 저장, 대기)
      ↓                               ↓
      ↓                     승인 화면: 무엇을 · 누구 · 되돌릴 수 있나
      ↓                               ↓
      ↓ ←── Command(resume=...) ── 신청자 / 결재자 버튼
      ↓
   승인 · 수정 → [submit] (멱등성 키) → "접수번호 ..."
   거절       → [안내] → "신청하지 않았습니다. 사유: ..."
   만료       → [안내] → 알림

앞으로 다시 만납니다 — 24차시에서 이 흐름을 API로 엽니다. 대화 전송 엔드포인트가 승인 대기에서 멈추면 화면에 승인 요청을 띄우고, 승인 처리 엔드포인트 가 Command(resume=...)를 보내 재개합니다.


9. 실습 — 맡기고 검수하기

지시

오피스봇 장비 신청 흐름에 승인 게이트를 붙여줘.
참고 공식 문서: LangGraph의 human-in-the-loop(interrupt, Command), 체크포인터(persistence) 문서.
  설치된 langgraph 버전과, 재개 시 노드가 어디서부터 다시 실행되는지에 대한 문서 문장을 인용해 보고할 것.
구조: it 에이전트(초안) → approval_gate(interrupt) → submit 또는 안내 → END. 8절 그림대로.
제약:
- 체크포인터는 SQLite 기반(설치 버전 문서 기준)으로. MemorySaver는 쓰지 말 것.
- 쓰기(request_equipment)는 submit 노드에서만, 멱등성 키 필수. 테스트용 가짜 신청 시스템 사용.
- resume을 보낸 사람의 신원은 로그인 세션에서. thread_id 소유자가 아니면 거부.
테스트: ① 승인 ② 수정(모니터 → 노트북 거치대) 후 승인 ③ 거절 ④ 대기 중 서버 프로세스 재시작 후 승인
  ⑤ 같은 resume 두 번 ⑥ 다른 직원이 남의 thread_id로 승인 시도
보고 형식: [시나리오 | interrupt 내용 | resume 값 | 신청 시스템에 기록된 건수 | 최종 답] 표.
추측하지 말고 실행 출력 원문과 가짜 신청 시스템의 기록 원문을 붙여.

받을 증거

  • 시나리오 6개의 표와 가짜 신청 시스템의 기록 원문 (건수 확인용)
  • ④에서 재시작 전후의 프로세스 로그 와 재개 성공 기록
  • 재개 동작에 대한 공식 문서 인용

내가 직접 확인할 것

볼 것이걸로 아는 것
③ 거절에서 신청 기록이 0건 인가승인 없이 실행되지 않는가 (1.2 강제)
④ 재시작 뒤에도 승인 대기가 남아 재개되는가상태가 디스크에 저장되는가 (3.2, 축 ①)
⑤에서 신청 기록이 1건 인가멱등성 (7)
⑥에서 거부 되는가승인자 신원을 코드가 검사하는가 (4)
승인 화면(또는 interrupt 내용)에 무엇을 · 누구 · 되돌릴 수 있나 가 다 있는가사람이 판단할 재료가 있는가 (4)

부록 A. 스스로 확인하기

#질문답이 있는 곳
1HITL을 유와 종차로 정의하면?1.1
2모델이 "신청할까요?"라고 묻는 것이 HITL이 아닌 이유는?1.2
3사람을 둘 자리의 네 가지 기준은? 모든 곳에 두면 안 되는 이유는?2
4인터럽트에 체크포인터가 필수인 이유는?3.2
5MemorySaver를 운영에 쓰면 안 되는 이유는?3.2
6승인 화면에 보여 줄 다섯 가지는?4
7승인 게이트와 에스컬레이션의 차이는?6
8쓰기를 interrupt 앞에 두면 생기는 일은?7

부록 B. 자주 하는 실수

실수바로잡기
프롬프트에 "신청 전에 확인받아"만 씀❌ 부탁입니다. 승인 노드를 그래프에 두세요
체크포인터 없이 interrupt 사용❌ 재개할 상태가 없습니다
운영에서 MemorySaver 사용⚠️ 서버가 재시작되면 승인 대기가 사라집니다. DB 기반으로
승인 노드 안 interrupt 앞에서 쓰기 실행❌ 재개 때 두 번 실행됩니다. 쓰기는 뒤 노드에서
승인 화면에 "승인하시겠습니까?"만 표시⚠️ 무엇을 · 누구 · 되돌릴 수 있나를 보여 주세요
읽기 작업에도 승인을 붙임⚠️ 승인 피로로 쓰기 승인까지 대충 누르게 됩니다
에스컬레이션 때 맥락 없이 넘김⭕ 마스킹한 요약을 함께 넘기세요

부록 C. NotebookLM 복습 프롬프트

이 23차시 노트만 근거로 비전공자용 5문제를 내줘.
"HITL이 아닌 것(부탁과 강제)", "어디에 사람을 두나", "인터럽트와 체크포인터(축 ⑦)", "재개 시 두 번 실행 막기"는 반드시 포함해줘.
내가 답한 뒤에 채점하고, 틀린 이유를 노트의 절 번호를 근거로 설명해줘.