오케스트레이션과 멀티 에이전트: 수퍼바이저와 HITL 승인 게이트
메뉴

AI Agent Engineering

오케스트레이션과 멀티 에이전트: 수퍼바이저와 HITL 승인 게이트

단일 에이전트의 한계를 극복하는 그래프 기반 오케스트레이션, 수퍼바이저 라우팅, 전문 서브에이전트 간 동적 핸드오프, 그리고 HITL 승인 게이트 및 트랜잭션 상태 관리

오케스트레이션과 멀티 에이전트: 수퍼바이저와 HITL 승인 게이트 hero image
Markdown약 5375 tokens

'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 AgentSupervisor Router리포트 반환
Supervisor RouterDeveloper Agent: 코드 전용 격리 도구격리 프롬프트 전달
Developer AgentHITL Gate: 비동기 인터럽트패치 검증 요청
HITL GateDeploy Agent: 격리된 실행 환경승인 완료 (Approved)

1.1 단일 에이전트의 병목 현상

모델의 성능 외에도 컨텍스트에 섞인 정보와 선택할 도구의 수가 실행에 영향을 줍니다.

병목 유형발생 원인 (Root Cause)프로덕션 임팩트그래프 오케스트레이션 해결 방안
컨텍스트 오염
(Context Contamination)
무분별하게 누적되는 대화 히스토리 및 도구의 Large PayloadLLM 주의력 분산으로 인한 지시 이탈(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)는 전체 목표에 따라 서브에이전트에게 작업을 맡깁니다. 결과를 모은 뒤 다음 작업을 정합니다.

순서출발 → 도착전달·동작
1User → Sup과업 요청: “결제 API 리팩토링 및 배포”
2Sup → Res핸드오프: 의존성 검색 컨텍스트 스코프
3Res → Sup코드 분석 리포트 전달: 상태 축소
4Sup → Dev핸드오프: 리포트 + Dev 전용 도구
5Dev → Sup패치 파일 생성 완료
6Sup → 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 내부진입점 → CheckpointSavedState 스냅샷 DB 영속화
InterruptedPending 내부CheckpointSaved → WaitingHumanInput웹 서버 스레드 비차단 (Pause)
HumanDecision 내부WaitingHumanInput → Approved관리자 승인 (Approve)
HumanDecision 내부WaitingHumanInput → Rejected관리자 거절 + 수정 피드백 (Reject)
HumanDecision 내부WaitingHumanInput → TimedOut타임아웃 / 세션 취소 (Timeout)
바깥 연결Approved → ExecutingActionCheckpoint 복원 후 노드 실행
바깥 연결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 상태 영속화 시퀀스

체크포인터는 그래프의 각 노드 실행 직전/직후의 상태를 영속화하여 비동기 중단 및 장애 발생 시의 복구를 보장합니다.

순서출발 → 도착메시지·동작
1Engine → Checkpointersave(AgentState) 호출
2Checkpointer → DBHSET thread_id:tx_9901 state_json
3DB → CheckpointerOK (Save Confirmed)
4Engine → ClientStream Event [INTERRUPT]
5Admin → ClientApprove & Deploy
6Client → EnginePOST /api/v1/resume
7Engine → Checkpointerload('tx_9901')
8Checkpointer → DBHGET tx_9901
9DB → CheckpointerRaw AgentState JSON
10Checkpointer → EngineAgentState 복원
11Engine → EngineMutate State
12Engine → NodeDeployer Node 실행
13Node → EngineExecution Success
14Engine → Checkpointersave(FinalState)
15Engine → ClientStream 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단계 완수 체크마크 표시 및 다음 단계 안내
INTERRUPTHITL Gate 감지 시patch_to_review, approval_status사용자 승인 Modal 팝업 및 피드백 입력창 노출
STREAM_CHUNKLLM 토큰 생성 시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 RouterResearch NeededResearcher Sub-agent
Researcher Sub-agentReturn NotesSupervisor Router
Supervisor RouterCode NeededDeveloper Sub-agent
Developer Sub-agentReturn PatchSupervisor Router
Supervisor RouterApproval RequiredHITL Approval Gate
HITL Approval GatePending / InterruptCheckpointer Store
HITL Approval GateApprovedDeployer Sub-agent
HITL Approval GateRejected w/ FeedbackDeveloper Sub-agent
Deployer Sub-agent작업 종료Task Finished

대기 중 Checkpointer Store에서 실행으로 향하는 연결은 원본에 없습니다. 재개 시퀀스는 다음 절에서 별도로 읽습니다.

6.2 HITL 인터럽트 및 비동기 재개(Resume) 시퀀스

외부 웹/애플리케이션 인터페이스와 에이전트 그래프 엔진 간의 비동기 인터럽트 및 상태 복원(Resume) 과정을 나타낸 시퀀스 다이어그램입니다.

순서출발 → 도착메시지·동작
1Engine → EngineHITL Gate 노드 진입: 위험 액션 감지
2Engine → DBAgentState 저장: is_interrupted=True, thread_id='tx_123'
3Engine → APIStream Event: INTERRUPT, 응답 종료 및 연결 해제
4API → InspectorUI 알림: “배포 승인 대기 중 (Patch v1)”
5Inspector → APIPOST /api/v1/agent/resume (Approved=True)
6API → Engineengine.resume_with_approval('tx_123', approved=True)
7Engine → DBLoad AgentState('tx_123')
8DB → EngineState 복원 완료
9Engine → Engineapproval_status = APPROVED 갱신
10Engine → NodeDeployer Node 실행
11Node → Engine배포 성공 완료
12Engine → APIStream 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를 통해 운영됩니다.

TOP