페이지 목차
이 페이지에서 검색
멀티 에이전트
이 페이지에 묻기
멀티 에이전트 · 현재 차시와 목차 본문에서만 찾아 답합니다. 답을 지어내지 않고 해당하는 대목을 그대로 보여 주며, 여기에 없으면 없다고 알려 드립니다.
생성형 AI가 아니라 이 페이지 본문을 찾아 그대로 보여 주는 방식입니다. 외부로 나가는 요청이 없습니다.
MULTI · AGENT
멀티 에이전트
생성 수정
도구 실행·검색·기억·그래프 흐름·전문 에이전트의 협업을 구성하고 권한·평가·사람의 확인으로 통제합니다.
26개 문서 · 3일차 · 24 / 26
23차시 · Human-in-the-Loop
지도 위치 : 3일차 · README 4장의 ⑥ 서비스 층(사람의 승인) | 이전: 22차시 오케스트레이션 | 다음: 24차시 서비스 통합과 실행 키워드 : HITL, 인터럽트, 승인 게이트, 에스컬레이션
이 차시를 다 읽으면
- HITL을 유와 종차로 정의하고, "사람이 손을 대는가" 축에서 자동 실행과 구별할 수 있다.
- 흐름의 어디에 사람을 둘지 네 가지 기준(쓰기 · 되돌릴 수 없음 · 낮은 확신 · 민감정보)으로 고를 수 있다.
- 승인 게이트를 LangGraph의 interrupt + 체크포인터 + Command(resume) 로 만드는 원리를 설명할 수 있다.
- 승인 화면에 보여 줄 것과, 거절 · 수정 후 재개하는 흐름을 설계할 수 있다.
- 에스컬레이션 기준을 정하고, 재개 때 쓰기 작업이 두 번 실행되지 않게 막을 수 있다.
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. 스스로 확인하기
| # | 질문 | 답이 있는 곳 |
|---|---|---|
| 1 | HITL을 유와 종차로 정의하면? | 1.1 |
| 2 | 모델이 "신청할까요?"라고 묻는 것이 HITL이 아닌 이유는? | 1.2 |
| 3 | 사람을 둘 자리의 네 가지 기준은? 모든 곳에 두면 안 되는 이유는? | 2 |
| 4 | 인터럽트에 체크포인터가 필수인 이유는? | 3.2 |
| 5 | MemorySaver를 운영에 쓰면 안 되는 이유는? | 3.2 |
| 6 | 승인 화면에 보여 줄 다섯 가지는? | 4 |
| 7 | 승인 게이트와 에스컬레이션의 차이는? | 6 |
| 8 | 쓰기를 interrupt 앞에 두면 생기는 일은? | 7 |
부록 B. 자주 하는 실수
| 실수 | 바로잡기 |
|---|---|
| 프롬프트에 "신청 전에 확인받아"만 씀 | ❌ 부탁입니다. 승인 노드를 그래프에 두세요 |
| 체크포인터 없이 interrupt 사용 | ❌ 재개할 상태가 없습니다 |
| 운영에서 MemorySaver 사용 | ⚠️ 서버가 재시작되면 승인 대기가 사라집니다. DB 기반으로 |
| 승인 노드 안 interrupt 앞에서 쓰기 실행 | ❌ 재개 때 두 번 실행됩니다. 쓰기는 뒤 노드에서 |
| 승인 화면에 "승인하시겠습니까?"만 표시 | ⚠️ 무엇을 · 누구 · 되돌릴 수 있나를 보여 주세요 |
| 읽기 작업에도 승인을 붙임 | ⚠️ 승인 피로로 쓰기 승인까지 대충 누르게 됩니다 |
| 에스컬레이션 때 맥락 없이 넘김 | ⭕ 마스킹한 요약을 함께 넘기세요 |
부록 C. NotebookLM 복습 프롬프트
이 23차시 노트만 근거로 비전공자용 5문제를 내줘.
"HITL이 아닌 것(부탁과 강제)", "어디에 사람을 두나", "인터럽트와 체크포인터(축 ⑦)", "재개 시 두 번 실행 막기"는 반드시 포함해줘.
내가 답한 뒤에 채점하고, 틀린 이유를 노트의 절 번호를 근거로 설명해줘.
LangTailor