'AI 에이전트 엔지니어링(AI Agent Engineering)' 시리즈의 4편입니다. 수퍼바이저, 서브에이전트 핸드오프, HITL 승인 게이트, 상태 그래프를 다룹니다.
1. 단일 에이전트의 한계와 그래프 오케스트레이션의 필요성
초기 PoC에서는 모든 도구와 지시를 한 에이전트에 넣기도 합니다. 도구와 작업 범위가 늘어나면 컨텍스트 크기와 도구 선택을 관리하기 어려워질 수 있습니다.
단일 에이전트의 문제 연결은 **모든 전문 지식 프롬프트 비대화(System Instruction Overhead) → 30개 이상의 파편화된 도구 스키마(Tool Schema Bloat) → 거대한 단일 컨텍스트 루프(Context Contamination) → 환각 및 지시 이탈(Instruction Drift & Compounding Errors)**입니다.
그래프 오케스트레이션은 다음 연결을 분리합니다.
| 출발 | 도착 | 전달·조건 |
|---|---|---|
| Supervisor Router: 중앙 상태 제어 노드 | Research Agent: 독립 검색 컨텍스트 | 상태 축소 (State Reduction) |
| Research Agent | Supervisor Router | 리포트 반환 |
| Supervisor Router | Developer Agent: 코드 전용 격리 도구 | 격리 프롬프트 전달 |
| Developer Agent | HITL Gate: 비동기 인터럽트 | 패치 검증 요청 |
| HITL Gate | Deploy Agent: 격리된 실행 환경 | 승인 완료 (Approved) |
1.1 단일 에이전트의 병목 현상
모델의 성능 외에도 컨텍스트에 섞인 정보와 선택할 도구의 수가 실행에 영향을 줍니다.
| 병목 유형 | 발생 원인 (Root Cause) | 프로덕션 임팩트 | 그래프 오케스트레이션 해결 방안 |
|---|---|---|---|
| 컨텍스트 오염 (Context Contamination) | 무분별하게 누적되는 대화 히스토리 및 도구의 Large Payload | LLM 주의력 분산으로 인한 지시 이탈(Instruction Drift) 발생 | 서브에이전트별 독립 스코프 컨텍스트 이관 및 결과 요약 합성 |
| 도구 비대화 (Tool Schema Overhead) | 단일 프롬프트에 30개 이상의 OpenAPI 도구 스키마 직접 주입 | 토큰 비용 폭증 및 유사 도구 간 오호출(Mis-selection) 확률 폭증 | 전문 노드별 도구 스키마 격리(Max –개) 배치 |
| 오류 복구 불능 (Error Compounding) | 초기 추론 실수가 루프 컨텍스트에 포함되어 계속 재인용 | 잘못된 전제를 바탕으로 환각을 반복하는 컴파운딩 오류에 매몰 | 체크포인트 롤백(Rollback) 및 조건부 에지 기반 Fallback |
단일 에이전트 루프에서 발생한 단 한 번의 잘못된 도구 입력이나 파싱 실수가 전체 컨텍스트 히스토리에 전파되면, 후속 추론 단계 전체가 오염되는 컴파운딩 오류(Compounding Errors)가 발생합니다.
1.2 절차적 워크플로우 vs 그래프 기반 상태 머신
이러한 한계를 극복하기 위해 에이전트 아키텍처는 단순 선형 워크플로우(DAG)에서 상태 그래프(Dynamic State Graph)로 진화하고 있습니다.
| 구분 | 결정론적 워크플로우 (Deterministic Workflow) | 그래프 기반 상태 머신 (Dynamic State Graph) |
|---|---|---|
| 제어 흐름 | 하드코딩된 Python 순차 실행 (A -> B -> C) | 조건부 에지(Conditional Edge) 기반 자율 라우팅 |
| 순환 및 복구 | 루프 및 동적 재시도(Retry/Fallback) 구현 복잡 | 순환 그래프(Cyclic Graph)를 통한 자율 상태 재진입 |
| 상태 불변성 | 함수 파라미터 간 단순 변수 전달 | Pydantic 스키마 기반 전역 상태 저장 및 스냅샷 관리 |
| 인터럽트 지원 | 동기식 대기로 서버 스레드 블로킹 발생 | 비동기 인터럽트(Pause) 및 체크포인트 복원(Resume) |
| 오류 복구력 | 예외 발생 시 전체 프로세스 강제 종료 | 노드 단위 격리 및 롤백 스냅샷 복구 지원 |
에이전틱 애플리케이션에서는 분기(Branching)뿐만 아니라 실패 시 이전 상태로 되돌아가는 순환(Looping)과 외부 승인을 기다리는 인터럽트(Interrupt)가 빈번히 발생하므로, 무순환 그래프(DAG) 대신 순환 상태 머신(State Graph)을 기본 아키텍처로 선택해야 합니다.
2. 수퍼바이저(Supervisor) 패턴과 동적 핸드오프(Dynamic Handoff)
수퍼바이저(Supervisor)는 전체 목표에 따라 서브에이전트에게 작업을 맡깁니다. 결과를 모은 뒤 다음 작업을 정합니다.
| 순서 | 출발 → 도착 | 전달·동작 |
|---|---|---|
| 1 | User → Sup | 과업 요청: “결제 API 리팩토링 및 배포” |
| 2 | Sup → Res | 핸드오프: 의존성 검색 컨텍스트 스코프 |
| 3 | Res → Sup | 코드 분석 리포트 전달: 상태 축소 |
| 4 | Sup → Dev | 핸드오프: 리포트 + Dev 전용 도구 |
| 5 | Dev → Sup | 패치 파일 생성 완료 |
| 6 | Sup → Gate | 배포 승인 인터럽트 발행: State Checkpointed |
2.1 중앙 수퍼바이저 vs P2P Swarm 네트워크 패턴
서브에이전트 간 제어권을 넘기는 패턴은 크게 중앙 라우터 방식과 자율 핸드오프 방식으로 나뉩니다.
| 아키텍처 패턴 | 제어 흐름 구조 | 핵심 장점 | 위험 요소 & 단점 | 추천 적용 도메인 |
|---|---|---|---|---|
| 중앙 수퍼바이저 (Centralized Router) | 중앙 라우터 노드가 상태를 검토하고 서브에이전트로 분기 | • 명확한 가시성 및 추적성 • 중앙 집중식 보안/권한 제어 | • 수퍼바이저 프롬프트 성능 병목 • 라우팅 효율에 따른 홉(Hop) 증가 | 엔터프라이즈 업무 자동화, HITL 승인이 필수적인 시스템 |
| P2P Swarm (Dynamic Handoff) | 서브에이전트가 직접 handoff_to_agent() 도구 호출 | • 빠른 실행 속도 (라우터 홉 감소) • 유연한 동적 자율 협업 | • 무한 순환 호출(Infinite Loop) 위험 • 전체 상태 흐름 추적의 어려움 | 탐색적 연구, 데이터 분석, 자율적 핑퐁이 필요한 창의적 작업 |
P2P Swarm 아키텍처는 제어권 위임이 자유로운 반면 서브에이전트끼리 서로를 무한 호출하는 핑퐁 루프(Infinite Handoff Loop)가 발생하기 쉽습니다. 반드시 전역 카운터나 루프 브레이커(Loop Breaker)를 탑재해야 합니다.
2.2 상태 전달 전략: Full Transcript vs State Reduction
서브에이전트로 제어권을 이전할 때, 대화 전체 히스토리를 넘기면 단일 에이전트의 컨텍스트 오염 문제가 그대로 재발합니다. 프로덕션 에이전트 엔진은 상태 요약 및 축소(State Reduction) 방식을 채택합니다.
| 전달 전략 | 전달 데이터 스코프 | 토큰 효율성 | 모델 주의력 | 오염 방지 능력 |
|---|---|---|---|---|
| Full Transcript | 전체 대화 히스토리 및 이전 도구 실행 결과 일체 | 토큰 소모 극대화 (비용 폭증) | 불필요 패치에 주의력 분산 | 초기 오류가 계속 전파됨 |
| State Reduction | 정제된 Pydantic State (필수 목표 + 요약 결과만 전달) | 최고 수준의 토큰 절감 | 작업 필수 맥락에 100% 집중 | 서브에이전트 간 완벽 격리 |
# 상태 축소(State Reduction) 메커니즘 예시class GlobalState(BaseModel): task_id: str original_goal: str plan_steps: list[str] research_summary: Optional[str] = None generated_code: Optional[str] = None approval_status: str = "PENDING" messages: list[BaseMessage] # 전역 대화 목록
수퍼바이저가 DeveloperAgent를 호출할 때는 GlobalState.messages 전체가 아니라, original_goal과 research_summary만을 정제하여 프롬프트로 주입함으로써 서브에이전트의 집중도를 최대로 끌어올립니다.
서브에이전트에 격리된 컨텍스트(Scoped Context)를 넘겨주고 작업 결과는 구조화된 상태 갱신값(State Mutation)으로 리턴받아 병합하는 방식이 토큰 비용 절감과 정밀도 향상의 핵심입니다.
3. Human-in-the-Loop (HITL) 인터럽트와 승인 게이트
에이전트가 외부 API 호출, DB 레코드 삭제, 금융 결제, 또는 프로덕션 서버 배포와 같이 가역적이지 않은(Irreversible) 고위험 액션을 수행할 때는 반드시 HITL(Human-in-the-Loop) 승인 게이트가 개입해야 합니다.
3.1 HITL Approval Gate 상태 전환 다이어그램:
| 범위 | 출발 → 도착 | 조건·동작 |
|---|---|---|
| 전체 | 진입점 → Idle | 그래프 세션 생성 |
| SupervisorRouting 내부 | Idle → Analyzing | 과업 분해 및 라우팅 |
| SupervisorRouting 내부 | Analyzing → SubAgentExecution | 서브에이전트 제어권 이관 |
| SupervisorRouting 내부 | SubAgentExecution → EvaluatingRisk | 결과 수집 및 위험도 평가 |
| 바깥 연결 | EvaluatingRisk → ExecutingAction | 저위험 액션 (Safe Action) |
| 바깥 연결 | EvaluatingRisk → InterruptedPending | 고위험 액션 감지 (High-Risk) |
| InterruptedPending 내부 | 진입점 → CheckpointSaved | State 스냅샷 DB 영속화 |
| InterruptedPending 내부 | CheckpointSaved → WaitingHumanInput | 웹 서버 스레드 비차단 (Pause) |
| HumanDecision 내부 | WaitingHumanInput → Approved | 관리자 승인 (Approve) |
| HumanDecision 내부 | WaitingHumanInput → Rejected | 관리자 거절 + 수정 피드백 (Reject) |
| HumanDecision 내부 | WaitingHumanInput → TimedOut | 타임아웃 / 세션 취소 (Timeout) |
| 바깥 연결 | Approved → ExecutingAction | Checkpoint 복원 후 노드 실행 |
| 바깥 연결 | Rejected → RePlanning | 피드백 주입 후 Developer 재작업 |
| 바깥 연결 | RePlanning → Analyzing | 수퍼바이저 재라우팅 |
| 바깥 연결 | TimedOut → RollbackState | 이전 안전 스냅샷으로 롤백 |
| 바깥 연결 | RollbackState → 종료점 | 세션 안전 종료 |
| 바깥 연결 | ExecutingAction → Completed | 과업 최종 완료 |
| 바깥 연결 | Completed → 종료점 | 완료 종료 |
원본에서 WaitingHumanInput은 InterruptedPending과 HumanDecision 양쪽 범위에 나타납니다. 표에서도 이 중첩 표기를 유지합니다.
3.2 비동기 인터럽트 및 4단계 실행 라이프사이클
HITL을 성공적으로 구현하려면 그래프 실행 엔진이 동기식 대기가 아닌 비동기 중단(Pause)과 재개(Resume) 라이프사이클을 지원해야 합니다.
| 실행 단계 | 상태 전환 | Checkpoint 저장 내용 | 시스템 스레드 동작 |
|---|---|---|---|
| 1. 위험 감지 | NONE → PENDING | 현재 AgentState snapshot을 DB에 저장 | 위험 노드 실행 직전 비동기 중단 |
| 2. 비동기 대기 | PENDING 유지 | DB 스냅샷 저장 상태 유지 (is_interrupted=True) | HTTP 서버 응답 반환 후 스레드 해제 |
| 3. 인간 결정 주입 | PENDING → APPROVED / REJECTED | 승인 상태 및 feedback 속성 업데이트 | Webhook / API 요청 수신 시 트리거 |
| 4. 그래프 재개 | APPROVED → NONE | 복원된 스냅샷 기반 다음 노드 실행 | 비동기 스트림 재연결 및 실행 완수 |
HITL 인터럽트 구현에서 핵심은 웹 서버 스레드가 비동기로 동작하여 리소스를 블로킹하지 않아야 하며, 무기한 대기 상태에서도 DB에 저장된 체크포인트를 통해 서버 재시작 후 안전하게 복구할 수 있어야 합니다.
HITL Resume 엔드포인트는 악의적인 사용자의 상태 조작 공격 대상이 될 수 있습니다. 반드시 RBAC(Role-Based Access Control) 인가 검증과 수정 불가능한 감사 로그(Audit Log)를 남겨야 합니다.
4. 파이썬 트랜잭션 상태 관리와 실시간 스트리밍 인터페이스
멀티 에이전트 상태 머신은 복수의 비동기 노드가 동시 다발적으로 상태를 읽고 쓰는 환경이므로, 트랜잭션 격리와 실시간 스트리밍(Streaming) 구조가 필수적입니다.
4.1 Async Checkpoint 상태 영속화 시퀀스
체크포인터는 그래프의 각 노드 실행 직전/직후의 상태를 영속화하여 비동기 중단 및 장애 발생 시의 복구를 보장합니다.
| 순서 | 출발 → 도착 | 메시지·동작 |
|---|---|---|
| 1 | Engine → Checkpointer | save(AgentState) 호출 |
| 2 | Checkpointer → DB | HSET thread_id:tx_9901 state_json |
| 3 | DB → Checkpointer | OK (Save Confirmed) |
| 4 | Engine → Client | Stream Event [INTERRUPT] |
| 5 | Admin → Client | Approve & Deploy |
| 6 | Client → Engine | POST /api/v1/resume |
| 7 | Engine → Checkpointer | load('tx_9901') |
| 8 | Checkpointer → DB | HGET tx_9901 |
| 9 | DB → Checkpointer | Raw AgentState JSON |
| 10 | Checkpointer → Engine | AgentState 복원 |
| 11 | Engine → Engine | Mutate State |
| 12 | Engine → Node | Deployer Node 실행 |
| 13 | Node → Engine | Execution Success |
| 14 | Engine → Checkpointer | save(FinalState) |
| 15 | Engine → Client | Stream Event [NODE_END] |
4.2 이벤트 기반 스트리밍 (Event-driven Streaming)
사용자 경험(UX) 측면에서 멀티 에이전트의 길고 복잡한 연산 과정을 투명하게 보여주기 위해 4가지 레벨의 스트리밍 이벤트를 제공해야 합니다.
| 스트림 이벤트 유형 | 발생 시점 | 주요 페이로드 구성 | Front-end UX 활용 |
|---|---|---|---|
NODE_START | 새 상태 노드 진입 시 | node_name, history_len, timestamp | 진행 상황 프로그레스 바 및 에이전트 변경 알림 |
NODE_END | 상태 노드 연산 완료 시 | node_name, next_node, state_diff | 단계 완수 체크마크 표시 및 다음 단계 안내 |
INTERRUPT | HITL Gate 감지 시 | patch_to_review, approval_status | 사용자 승인 Modal 팝업 및 피드백 입력창 노출 |
STREAM_CHUNK | LLM 토큰 생성 시 | delta_text, agent_id | 실시간 타이핑 텍스트 모나코 에디터 렌더링 |
상태 변경의 불변성(Immutability)을 보장하기 위해 노드 간 이동 시에는 기존 State를 직접 수정하기보다 복사본을 만들어 갱신(State Mutation) 후 저장하는 트랜잭션 패턴을 사용해야 합니다.
5. 프로덕션 레벨 파이썬 구현: 수퍼바이저 그래프 & HITL 승인 엔진
다음 Python asyncio 예시는 Pydantic v2를 사용해 상태 그래프, 수퍼바이저 라우팅, HITL 게이트, 메모리 체크포인트, 이벤트 스트리밍을 연결합니다.
본 코드는 서드파티 프레임워크 없이 Python 순수 코드로 작성되어 LangGraph, AutoGen, CrewAI 등 하부 엔진의 인터럽트 및 스냅샷 복원 동작 원리를 직관적으로 이해할 수 있도록 설계되었습니다.
"""Production-grade Multi-Agent State Graph Orchestrator with HITL GateFilename: agent_orchestrator.py""" import asynciofrom enum import Enumfrom typing import Any, AsyncGenerator, Awaitable, Callable, Dict, List, Optionalfrom pydantic import BaseModel, Field # -----------------------------------------------------------------------# 1. State Models & Types# ----------------------------------------------------------------------- class ApprovalStatus(str, Enum): NONE = "NONE" PENDING = "PENDING" APPROVED = "APPROVED" REJECTED = "REJECTED" class AgentState(BaseModel): thread_id: str user_query: str current_node: str = "supervisor" next_node: Optional[str] = None # Domain specific data research_notes: List[str] = Field(default_factory=list) generated_patch: Optional[str] = None # Control & HITL Flags is_interrupted: bool = False approval_status: ApprovalStatus = ApprovalStatus.NONE hitl_feedback: Optional[str] = None execution_history: List[str] = Field(default_factory=list) def log(self, step: str) -> None: self.execution_history.append(step) class StreamEvent(BaseModel): event_type: str # "NODE_START", "NODE_END", "INTERRUPT", "STREAM_CHUNK" node_name: str payload: Dict[str, Any] # -----------------------------------------------------------------------# 2. State Checkpointer & Persistence Store# ----------------------------------------------------------------------- class AsyncCheckpointer: def __init__(self) -> None: self._store: Dict[str, AgentState] = {} async def save(self, state: AgentState) -> None: # Deep copy simulated via model_copy self._store[state.thread_id] = state.model_copy(deep=True) async def load(self, thread_id: str) -> Optional[AgentState]: state = self._store.get(thread_id) if state: return state.model_copy(deep=True) return None # -----------------------------------------------------------------------# 3. Subagent Nodes & Logic# ----------------------------------------------------------------------- async def supervisor_node(state: AgentState) -> AgentState: state.log("Supervisor: Assessing task status...") if not state.research_notes: state.next_node = "researcher" elif state.generated_patch is None: state.next_node = "developer" elif state.approval_status == ApprovalStatus.NONE: state.next_node = "hitl_gate" elif state.approval_status == ApprovalStatus.APPROVED: state.next_node = "deployer" elif state.approval_status == ApprovalStatus.REJECTED: state.next_node = "developer" # Re-plan/Re-code on rejection else: state.next_node = "end" return state async def researcher_node(state: AgentState) -> AgentState: state.log("Researcher: Gathering system requirements and codebase context...") await asyncio.sleep(0.3) # Async API work simulation state.research_notes.append("Found legacy payment gateway route requiring OAuth2 update.") state.research_notes.append("Dependency check: Auth SDK v2.4 compatibility confirmed.") state.next_node = "supervisor" return state async def developer_node(state: AgentState) -> AgentState: state.log("Developer: Drafting code patch...") await asyncio.sleep(0.4) if state.approval_status == ApprovalStatus.REJECTED: # Refine code based on HITL feedback state.generated_patch = f"PATCH v2 (Refactored based on feedback: {state.hitl_feedback})" state.approval_status = ApprovalStatus.NONE # Reset for re-approval else: state.generated_patch = "PATCH v1: Fix OAuth2 Authorization Header formatting in payment_client.py" state.next_node = "supervisor" return state async def hitl_approval_gate_node(state: AgentState) -> AgentState: state.log("HITL Gate: Checking safety boundary for code deployment...") if state.approval_status == ApprovalStatus.NONE: state.log("HITL Gate: High-risk action detected (Deploy)! Interrupting graph execution.") state.is_interrupted = True state.approval_status = ApprovalStatus.PENDING state.next_node = "supervisor" elif state.approval_status in (ApprovalStatus.APPROVED, ApprovalStatus.REJECTED): state.is_interrupted = False state.next_node = "supervisor" return state async def deployer_node(state: AgentState) -> AgentState: state.log("Deployer: Applying patch to production environment...") await asyncio.sleep(0.5) state.log(f"Deployer: Successfully deployed -> {state.generated_patch}") state.next_node = "end" return state # -----------------------------------------------------------------------# 4. State Graph Orchestrator Engine# ----------------------------------------------------------------------- class StateGraphEngine: def __init__(self, checkpointer: AsyncCheckpointer) -> None: self.nodes: Dict[str, Callable[[AgentState], Awaitable[AgentState]]] = {} self.checkpointer = checkpointer def add_node(self, name: str, func: Callable[[AgentState], Awaitable[AgentState]]) -> None: self.nodes[name] = func async def run_stream( self, thread_id: str, initial_query: Optional[str] = None ) -> AsyncGenerator[StreamEvent, None]: # Load or initialize state state = await self.checkpointer.load(thread_id) if state is None: if not initial_query: raise ValueError("Initial query required for new thread.") state = AgentState(thread_id=thread_id, user_query=initial_query) while state.current_node != "end": current_name = state.current_node node_func = self.nodes.get(current_name) if not node_func: raise KeyError(f"Node '{current_name}' is not registered in state graph.") yield StreamEvent( event_type="NODE_START", node_name=current_name, payload={"history_len": len(state.execution_history)} ) # Execute Node state = await node_func(state) # Check for HITL Interrupt if state.is_interrupted: await self.checkpointer.save(state) yield StreamEvent( event_type="INTERRUPT", node_name=current_name, payload={ "message": "Graph paused for human approval.", "patch_to_review": state.generated_patch, "approval_status": state.approval_status.value } ) return # Exit stream, waiting for external resume trigger yield StreamEvent( event_type="NODE_END", node_name=current_name, payload={"next_node": state.next_node} ) state.current_node = state.next_node await self.checkpointer.save(state) async def resume_with_approval( self, thread_id: str, approved: bool, feedback: Optional[str] = None ) -> AsyncGenerator[StreamEvent, None]: state = await self.checkpointer.load(thread_id) if not state or not state.is_interrupted: raise RuntimeError(f"Thread '{thread_id}' is not in an interrupted state.") # Inject Human Decision state.approval_status = ApprovalStatus.APPROVED if approved else ApprovalStatus.REJECTED state.hitl_feedback = feedback state.is_interrupted = False state.log(f"Human Decision Injected: Approved={approved}, Feedback={feedback}") await self.checkpointer.save(state) # Resume execution stream async for event in self.run_stream(thread_id): yield event # -----------------------------------------------------------------------# 5. Verification & Execution Walkthrough# ----------------------------------------------------------------------- async def main() -> None: checkpointer = AsyncCheckpointer() engine = StateGraphEngine(checkpointer) # Register Graph Nodes engine.add_node("supervisor", supervisor_node) engine.add_node("researcher", researcher_node) engine.add_node("developer", developer_node) engine.add_node("hitl_gate", hitl_approval_gate_node) engine.add_node("deployer", deployer_node) thread_id = "tx_session_9901" print("=== [1 Phase: Execution until HITL Interrupt] ===") async for event in engine.run_stream(thread_id, initial_query="Update payment API route"): print(f"[{event.event_type}] Node: {event.node_name} -> {event.payload}") # Inspect state at interrupt point paused_state = await checkpointer.load(thread_id) print(f"\n[System Status] Graph Interrupted? {paused_state.is_interrupted}") print(f"[Review Target] Proposed Code: {paused_state.generated_patch}") print("\n=== [2 Phase: Simulated Human Rejection & Feedback Loop] ===") async for event in engine.resume_with_approval( thread_id, approved=False, feedback="Use Bearer prefix for Auth header" ): print(f"[{event.event_type}] Node: {event.node_name} -> {event.payload}") print("\n=== [3 Phase: Final Human Approval & Deployment] ===") async for event in engine.resume_with_approval(thread_id, approved=True): print(f"[{event.event_type}] Node: {event.node_name} -> {event.payload}") final_state = await checkpointer.load(thread_id) print("\n=== [Final Execution Log] ===") for idx, log_entry in enumerate(final_state.execution_history, 1): print(f"{idx}. {log_entry}") if __name__ == "__main__": asyncio.run(main())
6. Mermaid 아키텍처 & 시퀀스 다이어그램
6.1 수퍼바이저 & 서브에이전트 그래프 오케스트레이션
시스템 전체의 상태 노드 전환과 수퍼바이저 제어 구조를 시각화한 상태 그래프 다이어그램입니다.
Start Task → Node: Supervisor Router에서 시작합니다.
| 출발 노드 | 조건·전달 | 도착 노드 |
|---|---|---|
| Supervisor Router | Research Needed | Researcher Sub-agent |
| Researcher Sub-agent | Return Notes | Supervisor Router |
| Supervisor Router | Code Needed | Developer Sub-agent |
| Developer Sub-agent | Return Patch | Supervisor Router |
| Supervisor Router | Approval Required | HITL Approval Gate |
| HITL Approval Gate | Pending / Interrupt | Checkpointer Store |
| HITL Approval Gate | Approved | Deployer Sub-agent |
| HITL Approval Gate | Rejected w/ Feedback | Developer Sub-agent |
| Deployer Sub-agent | 작업 종료 | Task Finished |
대기 중 Checkpointer Store에서 실행으로 향하는 연결은 원본에 없습니다. 재개 시퀀스는 다음 절에서 별도로 읽습니다.
6.2 HITL 인터럽트 및 비동기 재개(Resume) 시퀀스
외부 웹/애플리케이션 인터페이스와 에이전트 그래프 엔진 간의 비동기 인터럽트 및 상태 복원(Resume) 과정을 나타낸 시퀀스 다이어그램입니다.
| 순서 | 출발 → 도착 | 메시지·동작 |
|---|---|---|
| 1 | Engine → Engine | HITL Gate 노드 진입: 위험 액션 감지 |
| 2 | Engine → DB | AgentState 저장: is_interrupted=True, thread_id='tx_123' |
| 3 | Engine → API | Stream Event: INTERRUPT, 응답 종료 및 연결 해제 |
| 4 | API → Inspector | UI 알림: “배포 승인 대기 중 (Patch v1)” |
| 5 | Inspector → API | POST /api/v1/agent/resume (Approved=True) |
| 6 | API → Engine | engine.resume_with_approval('tx_123', approved=True) |
| 7 | Engine → DB | Load AgentState('tx_123') |
| 8 | DB → Engine | State 복원 완료 |
| 9 | Engine → Engine | approval_status = APPROVED 갱신 |
| 10 | Engine → Node | Deployer Node 실행 |
| 11 | Node → Engine | 배포 성공 완료 |
| 12 | Engine → API | Stream Event: NODE_END, 최종 완료 |
7. 멀티 에이전트 오케스트레이션 운영 체크리스트
프로덕션 환경에 수퍼바이저 및 멀티 에이전트 오케스트레이션을 배치하기 전에 반드시 검증해야 할 체크리스트입니다.
| 영역 | 항목 | 검증 내용 | 심각도 |
|---|---|---|---|
| 아키텍처 | 서브에이전트 단일 책임 | 각 서브에이전트의 역할과 보유 도구가 명확히 격리되어 있는가? (도구 10개 이하 권장) | High |
| 상태 관리 | 상태 축소 (State Reduction) | 수퍼바이저 및 서브에이전트 이동 시 불필요한 전체 대화 히스토리를 요약/제거하는가? | Critical |
| 상태 관리 | 체크포인트 영속성 | 프로세스 재부팅 시에도 대기 중인 인터럽트 상태를 DB에서 안전하게 복원할 수 있는가? | Critical |
| HITL 보안 | 인터럽트 롤백 보장 | 승인 거부(Rejected) 시 이전 안전 노드로 복귀하여 재작업(Re-plan)을 수행하는 흐름이 존재하는가? | Critical |
| HITL 보안 | 인가(Authorization) 게이트 | 승인 신호(Resume Trigger)를 보내는 주체의 권한이 RBAC 시스템과 연동되어 검증되는가? | Critical |
| 운영 / Observability | 무한 루프 감지 (Loop Breaker) | 서브에이전트 간 핸드오프 횟수가 최대 이터레이션(e.g., Max 15 steps)을 초과할 때 강제 종료되는가? | High |
| 운영 / Observability | 이벤트 스트리밍 추적 | Node Transition, Tool Call, LLM Streaming이 OpenTelemetry/LangSmith 트레이스와 동기화되는가? | Medium |
프로덕션 멀티 에이전트 오케스트레이션 구축 시 체크포인트 영속성과 무한 루프 방지(Loop Breaker)가 구비되지 않은 상태로 실서버 배포를 진행할 경우, 과도한 LLM API 호출 비용 발생 및 제어 불능 상태에 직면할 위험이 높습니다.
마무리하며
에이전트 사이에 작업을 나누려면 전역 상태, 라우팅 규칙, 전달할 컨텍스트를 먼저 정해야 합니다. 승인 대기 상태를 저장하고 재개하는 과정도 같은 실행 흐름 안에서 관리합니다.

댓글
GitHub 계정으로 로그인하면 댓글을 남길 수 있습니다. 댓글은 GitHub Discussions를 통해 운영됩니다.