페이지 목차
이 페이지에서 검색
멀티 에이전트
이 페이지에 묻기
멀티 에이전트 · 현재 차시와 목차 본문에서만 찾아 답합니다. 답을 지어내지 않고 해당하는 대목을 그대로 보여 주며, 여기에 없으면 없다고 알려 드립니다.
생성형 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