MINERVA Phase C-6 — 하네싱 아키텍처 (Supervisor·Guard Rail·Resource Quota)

에이전트가 자율 실행할수록 잘못된 일을 빠르게 할 수 있다 — 3대 컴포넌트로 안전한 자유의 경계를 정한다

Phase C-3까지의 에이전트는 점점 자율적으로 도구를 선택하고 다중 단계 플랜을 실행한다. 자유도가 늘어날수록 잘못된 결정의 영향도 커진다. 본 편은 하네싱(harnessing)의 정의, 3대 컴포넌트(Supervisor·Guard Rail·Resource Quota), 입력·출력·도구 가드의 차이, 토큰·시간·비용 예산 관리, audit log·거버넌스 결합, MINERVA에서 LangGraph 위에 어떻게 하네싱을 얹는지 정리한다.

Agent
저자

Kwangmin Kim

공개

2026년 05월 06일

1 왜 하네싱인가 — 자율의 비용

C14 에이전트 위임·C12 ReAct·C13 Plan-and-Execute이 만든 자율성은 가치가 크지만 부작용도 따른다.

자율성 증가 부작용
에이전트가 도구 선택 잘못된 도구 호출이 외부 상태 변경 (DB write·email 발송)
다중 단계 플랜 실행 무한 루프·예산 초과 위험
sub-agent 위임 권한 cascade — 한 권한이 위임 chain으로 확장
자기 수정·재시도 같은 실패를 반복해 비용·latency 증가
자유로운 LLM 응답 hallucination·민감 정보 노출·정책 위반
정의: 하네싱 (Harnessing)

자율 에이전트의 자유도를 안전 경계 안으로 제한하면서 능력은 보존하는 운영 시스템. “말 머리에 굴레(harness)를 씌워 방향을 잡는다”의 비유다. 마차가 멈추는 것이 아니라 가이드라인 안에서 달린다.

2 3대 컴포넌트

[Supervisor]   "누가 무엇을 할지" — 라우팅·위임 정책
   ↓
[Guard Rail]   "각 단계에서 무엇이 허용되는지" — 입력·출력·도구 검증
   ↓
[Resource Quota]  "얼마나 많이 할 수 있는지" — 토큰·시간·비용·동시성 한계
   ↓
[Audit Log]    "무엇이 일어났는지" — 모든 결정의 기록

세 컴포넌트가 직교다 — 같은 에이전트 동작에 셋이 동시 작용. 한 컴포넌트만으로는 부족.

3 Supervisor — 라우팅과 권한

3.1 단순 Supervisor — Router

# app/harness/supervisor.py
from typing import Literal


class SimpleRouter:
    """의도 기반 라우팅 + 권한 체크."""

    def route(self, query: Query) -> str:
        intent = query.intent
        segment = query.segment

        # 1. 의도-에이전트 매핑 (단순 lookup)
        agent_id = INTENT_AGENT_MAP.get(intent, "qna_chatbot")

        # 2. 권한 체크 (segment·agent 매트릭스)
        if not has_access(segment, agent_id):
            raise PermissionError(f"{segment} cannot access {agent_id}")

        return agent_id

3.2 다중 에이전트 Supervisor

여러 sub-agent를 조정하는 상위 에이전트. LangGraph로 자연스럽게 구현 가능 — C14 에이전트 위임 참조.

# LangGraph supervisor pattern
from langgraph.graph import StateGraph, END

def supervisor_node(state: State) -> dict:
    """LLM이 다음에 어느 sub-agent를 호출할지 결정."""
    decision = llm_call(SUPERVISOR_PROMPT, state)
    return {"next_agent": decision.agent_id, "task": decision.task}


def route_to_sub_agent(state: State) -> Literal["qna", "code", "search", "end"]:
    return state["next_agent"]


graph = StateGraph(State)
graph.add_node("supervisor", supervisor_node)
graph.add_node("qna", qna_agent)
graph.add_node("code", code_agent)
graph.add_node("search", search_agent)
graph.add_conditional_edges("supervisor", route_to_sub_agent, {
    "qna": "qna", "code": "code", "search": "search", "end": END,
})

이 supervisor가 하네싱의 첫 번째 게이트: 잘못된 sub-agent 호출 자체를 차단.

3.3 권한 매트릭스

# config/access.yaml
access:
  qna_chatbot:
    departments: [all]
    intents: [knowledge_lookup, decision_support]
    tools: [search, summarize]
    max_steps: 5

  code_agent:
    departments: [rnd, support]
    intents: [task_assist, troubleshoot]
    tools: [search, code_exec, github_read]
    max_steps: 10

  finance_analyst:
    departments: [finance]
    intents: [decision_support]
    tools: [search, db_query, calc]
    max_steps: 8
    requires_hitl: true                 # 모든 응답에 HITL 검토 필수

Supervisor는 권한 위반 시 즉시 거부 — sub-agent에 위임된 후 안에서 차단되는 것보다 빠르고 audit이 명확.

4 Guard Rail — 단계별 검증

4.1 Input Guard

# app/harness/guard_input.py
def input_guard(query: Query) -> tuple[bool, str | None]:
    # PII 탐지
    if has_credit_card(query.text):
        return False, "신용카드 번호가 포함된 질의는 차단"
    if has_external_email(query.text):
        return False, "외부 이메일 주소 포함"

    # 길이 제한
    if len(query.text) > 4000:
        return False, "질의 길이 4000자 초과"

    # 의도가 out_of_scope
    if classify_intent(query.text) == "out_of_scope":
        return False, "도메인 외 질의"

    # 비속어·prompt injection 시도
    if detect_prompt_injection(query.text):
        return False, "프롬프트 주입 시도 의심"

    return True, None

4.2 Output Guard

# app/harness/guard_output.py
def output_guard(response: str, citations: list, query: Query) -> tuple[bool, str | None]:
    # PII 누출 (모델이 학습 데이터의 PII 출력 가능)
    if has_pii(response):
        return False, "응답에 PII 포함"

    # 회사 외부 정보 누출
    if has_internal_secrets(response):
        return False, "내부 비밀 정보 포함"

    # citation 0 + 사실 단언 (hallucination 위험)
    if has_facts(response) and len(citations) == 0:
        return False, "근거 없는 사실 단언"

    # 정책 위반 (가격 조작·차별·법적 자문 등)
    if violates_policy(response):
        return False, "정책 위반"

    return True, None

응답이 차단되면 fallback 응답으로 대체:

FALLBACK = "이 질의에 대해서는 도움드리기 어렵습니다. 다른 표현으로 시도해 주세요."

4.3 Tool Guard

도구 호출이 가장 위험. 외부 상태 변경·돈 지출·데이터 노출.

# app/harness/guard_tool.py
class ToolGuard:
    def __init__(self, agent_id: str, segment: dict):
        self.allowed = load_allowed_tools(agent_id)
        self.denied = load_denied_tools(agent_id)
        self.confirmation_required = load_confirmation_tools(agent_id)
        self.segment = segment

    def check(self, tool_name: str, args: dict) -> ToolDecision:
        if tool_name in self.denied:
            return ToolDecision.deny("denied tool")
        if tool_name not in self.allowed:
            return ToolDecision.deny("not in allowed list")

        # 도구별 인자 검증
        if tool_name == "send_email":
            if not args["to"].endswith("@company.com"):
                return ToolDecision.deny("외부 이메일 주소 차단")
        if tool_name == "db_write":
            if args["table"] not in WRITABLE_TABLES:
                return ToolDecision.deny("쓰기 금지 테이블")
            return ToolDecision.confirm("DB 변경은 사람 확인 필수")

        if tool_name in self.confirmation_required:
            return ToolDecision.confirm("사용자 확인 필요")

        return ToolDecision.allow()

도구 가드는 3가지 결과를 낸다 — allow·deny·confirm. confirm은 사람에게 prompt → 사용자 확인 후 실행 (C9 HITL 패턴 활용).

4.4 LLM-based Guard

규칙 기반이 못 잡는 모호한 위반은 LLM judge로 보강.

GUARD_PROMPT = """
다음 응답이 사내 정책에 위반되는가?

[정책]
- 외부 공유 금지 정보 누출 X
- 차별·혐오 표현 X
- 법적·의료 자문은 면책 안내

[응답]
{response}

위반 여부 (yes/no)와 이유를 한 줄로 답변.
"""


def llm_output_guard(response: str) -> tuple[bool, str]:
    out = llm_call(GUARD_PROMPT.format(response=response))
    if out.startswith("yes"):
        return False, out
    return True, None

비용 — 모든 응답에 LLM guard는 비현실적. 다음 케이스에만: - 규칙 가드가 borderline 판정 (PII 의심) - 새 의도·새 도메인 (학습 부족) - HITL 임계 미만이지만 위험 신호

5 Resource Quota — 예산 관리

5.1 Token Budget

# app/harness/quota.py
class TokenBudget:
    def __init__(self, max_input: int, max_output: int, max_total: int):
        self.max_input = max_input
        self.max_output = max_output
        self.max_total = max_total

    def check(self, used: int, requested: int) -> bool:
        return used + requested <= self.max_total


# 사용
def run_step(state: State, budget: TokenBudget):
    if not budget.check(state["tokens_used"], 1000):
        raise QuotaExceeded(f"token budget exhausted")
    ...

5.2 Step Limit

다중 단계 에이전트가 무한 루프에 빠지지 않도록.

class StepLimit:
    def __init__(self, max_steps: int = 20):
        self.max_steps = max_steps

    def check(self, current: int) -> bool:
        return current < self.max_steps

LangGraph의 recursion_limit 옵션으로도 같은 효과 — 그러나 명시적 step 카운터로 audit log에 기록 권장.

5.3 Time Budget

class TimeBudget:
    def __init__(self, deadline: datetime):
        self.deadline = deadline

    def check(self) -> bool:
        return datetime.utcnow() < self.deadline


# 사용
deadline = datetime.utcnow() + timedelta(seconds=30)
budget = TimeBudget(deadline)

while not done and budget.check():
    step()

5.4 Cost Budget

class CostBudget:
    def __init__(self, max_usd: float):
        self.max_usd = max_usd
        self.used_usd = 0.0

    def charge(self, model: str, tokens: dict) -> bool:
        cost = compute_cost(model, tokens)
        if self.used_usd + cost > self.max_usd:
            return False
        self.used_usd += cost
        return True

각 사용자·세그먼트·실험에 일별·월별 cost 한도. 초과 시 자동 fallback (저비용 모델 또는 거부).

5.5 Concurrency Quota

# 부하 보호 — 한 사용자가 동시에 N개 이상 query 못 보냄
import asyncio

class UserConcurrency:
    def __init__(self, max_per_user: int = 3):
        self.semaphores: dict[str, asyncio.Semaphore] = {}
        self.max_per_user = max_per_user

    async def acquire(self, user_id: str):
        if user_id not in self.semaphores:
            self.semaphores[user_id] = asyncio.Semaphore(self.max_per_user)
        await self.semaphores[user_id].acquire()

    def release(self, user_id: str):
        self.semaphores[user_id].release()

6 Audit Log

모든 하네싱 결정을 영구 기록. 디버깅·거버넌스·법적 증거 대비.

# app/harness/audit.py
class HarnessAudit(BaseModel):
    run_id: str
    timestamp: datetime
    step: int
    component: str            # "supervisor" | "guard_input" | "guard_output" | "guard_tool" | "quota"
    decision: str             # "allow" | "deny" | "confirm" | "exceeded"
    reason: str
    metadata: dict


# 매 결정마다 기록
def record_audit(...):
    audit_log.append(HarnessAudit(...))
    write_to_storage(audit)   # 비동기 — 운영 latency 영향 없게

audit log는 C20 대화 로깅 raw 계층의 일부 — 같은 retention·접근 통제.

7 LangGraph로 하네싱 구현

LangGraph의 Pregel-like 구조에 하네싱을 자연스럽게 얹을 수 있다.

from langgraph.graph import StateGraph, END
from typing import TypedDict, Annotated


class HarnessedState(TypedDict):
    query: Query
    response: str | None
    tokens_used: Annotated[int, lambda a, b: a + b]
    cost_usd: Annotated[float, lambda a, b: a + b]
    audit_log: Annotated[list, lambda a, b: a + b]
    step: Annotated[int, lambda a, b: a + b]


def with_input_guard(node_fn):
    def wrapped(state):
        ok, reason = input_guard(state["query"])
        if not ok:
            return {"response": FALLBACK, "audit_log": [{"deny": reason}]}
        return node_fn(state)
    return wrapped


def with_quota_check(node_fn, budget):
    def wrapped(state):
        if not budget.check(state["tokens_used"], 1000):
            return {"response": "예산 초과", "audit_log": [{"deny": "quota"}]}
        return node_fn(state)
    return wrapped


# 적용
graph = StateGraph(HarnessedState)
graph.add_node("agent", with_quota_check(with_input_guard(agent_fn), token_budget))

각 데코레이터가 한 가드 — composable, 재사용. 새 가드 추가는 데코레이터 추가만.

8 거버넌스와 결합

C19 실험 파이프라인 governance와 일관된 분류:

변경 게이트
가드 룰 미세 조정 (정규식·임계) auto_ship
새 도구 등록·새 sub-agent 추가 pending_review
권한 매트릭스 변경·scope 확장 governance_review
LLM guard prompt 수정 pending_review
Quota 한도 변경 pending_review

가드 변경도 PR + 리뷰 + 회귀 테스트(golden set·A/A) — 자동 ship 카테고리도 dry-run 통과 후.

9 MINERVA 적용

app/harness/
├── supervisor.py            # router·permission matrix
├── guard_input.py            # PII·길이·intent·injection
├── guard_output.py           # PII·secrets·hallucination·정책
├── guard_tool.py             # allow/deny/confirm by tool
├── llm_guard.py              # borderline LLM judge
├── quota.py                  # token·step·time·cost·concurrency
├── audit.py                  # HarnessAudit + storage
└── decorators.py             # LangGraph 데코레이터

config/
├── access.yaml               # 권한 매트릭스
├── tool_policy.yaml          # 도구별 allow/deny/confirm
└── quota.yaml                # 사용자·세그먼트·실험별 한도

기존 02-1 BaseAgent v2의 contract에 harness_context 필드 추가 — 모든 단계에 가드·예산·audit이 전달됨.

10 자주 발생하는 함정

10.1 Over-restriction

가드를 보수적으로 설정 → 정상 query도 거부됨. 사용자 만족 ↓.

해법: - 가드별 거짓 양성률 모니터링 - borderline은 LLM guard로 escalate 후 차단보다 confirm - C22 응답 품질에 “가드 차단으로 답변 못 받음” 별도 카테고리

10.2 Hidden Bypass

엔지니어가 디버깅 편의로 admin 모드·hidden flag로 가드 우회. 시간 흘러 이 우회가 잊혀 production에 적용.

해법: - 우회 모드는 시간 제한 (1시간 후 자동 비활성) - 우회 사용은 별도 audit으로 영구 기록 - 분기마다 우회 사용 audit 리뷰

10.3 Monitor Blindness

가드는 동작하는데 그 결정이 audit log에만 남고 누구도 보지 않음 → 잘못된 차단 누적.

해법: - 가드 차단율 dashboard - 차단율 임계 초과 시 자동 알림 - 사용자 피드백(“이게 왜 차단됐어요?”) 채널 명시

10.4 Tool Permission Creep

처음엔 좁게 시작했다가 사용자 요청에 따라 한 도구씩 추가 → 1년 후 거의 모든 도구가 허용. 권한 매트릭스 의미 무력.

해법: - 도구 추가는 명시 정당화 + sunset 날짜 - 분기마다 권한 매트릭스 audit — 사용 빈도 0인 권한 회수

10.5 LLM Guard Cost Explosion

LLM guard를 모든 응답에 적용 → 비용·latency 폭증.

해법: - 규칙 가드가 우선, LLM guard는 borderline에만 - 캐시 — 같은 응답에 같은 판단 재사용 - 비동기 — 응답은 즉시 보내고 가드는 사후 검증·필요시 회수

10.6 Race condition in Quota

여러 동시 요청이 같은 budget을 동시에 차감 → 한도 초과 검출 실패.

해법: Redis atomic counter 또는 DB 트랜잭션. in-memory dict는 단일 프로세스에서만 안전.

11 정리

영역 핵심
Supervisor 의도-에이전트 매핑·권한 매트릭스·다중 에이전트 라우팅
Guard Rail input·output·tool 3종, 규칙 + LLM borderline
Resource Quota token·step·time·cost·concurrency 5축
Audit Log 모든 결정 기록, raw 계층의 일부
LangGraph 통합 데코레이터로 composable, State에 token·cost·audit 전파
거버넌스 C19와 동일 분류 (auto·pending·governance)
함정 over-restriction·hidden bypass·monitor blindness·permission creep·LLM cost·race condition

12 응용 분야

시나리오 활용 컴포넌트
외부 도구 호출 차단 Tool Guard (allow/deny list)
LLM 응답 PII 누출 방지 Output Guard (PII 정규식 + LLM borderline)
사용자별 일일 비용 한도 Cost Quota (Redis atomic)
다중 에이전트 위임 권한 Supervisor (매트릭스 + cascade 차단)
HITL 임계 자동 분기 Tool Guard confirm + Output Guard escalate
보안 감사 대비 Audit Log (영구 보관 + access log)

13 관련 주제

선행 학습 (선수)

18-LangGraph 시리즈 cross-reference

  • #30 하네스 설계 차이 — 본 편 3대 컴포넌트 이론적 배경
  • #31 Prompt·Context·Harness 구분 — 하네스 정의의 기원
  • #33 산업 사례 — 하네싱 도입 운영 사례
  • #34 AWS 케이스 — 하네싱 인프라 패턴

후속 (Phase C-6)

Cross-reference

Subscribe

Enjoy this blog? Get notified of new posts by email: