1 왜 하네싱인가 — 자율의 비용
C14 에이전트 위임·C12 ReAct·C13 Plan-and-Execute이 만든 자율성은 가치가 크지만 부작용도 따른다.
| 자율성 증가 | 부작용 |
|---|---|
| 에이전트가 도구 선택 | 잘못된 도구 호출이 외부 상태 변경 (DB write·email 발송) |
| 다중 단계 플랜 실행 | 무한 루프·예산 초과 위험 |
| sub-agent 위임 | 권한 cascade — 한 권한이 위임 chain으로 확장 |
| 자기 수정·재시도 | 같은 실패를 반복해 비용·latency 증가 |
| 자유로운 LLM 응답 | hallucination·민감 정보 노출·정책 위반 |
자율 에이전트의 자유도를 안전 경계 안으로 제한하면서 능력은 보존하는 운영 시스템. “말 머리에 굴레(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_id3.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, None4.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 응답으로 대체:
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_stepsLangGraph의 recursion_limit 옵션으로도 같은 효과 — 그러나 명시적 step 카운터로 audit log에 기록 권장.
5.3 Time Budget
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.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 관련 주제
선행 학습 (선수)
- C14 에이전트 위임 — Supervisor 패턴 토대
- C9 Checkpointing·HITL — confirm 결정의 사람 개입
- 02-1 BaseAgent v2 — harness_context 추가 위치
- C18 개인화 Scope — Tool Guard와 자연 결합
18-LangGraph 시리즈 cross-reference
- #30 하네스 설계 차이 — 본 편 3대 컴포넌트 이론적 배경
- #31 Prompt·Context·Harness 구분 — 하네스 정의의 기원
- #33 산업 사례 — 하네싱 도입 운영 사례
- #34 AWS 케이스 — 하네싱 인프라 패턴
후속 (Phase C-6)
- C25 실행 제어 — Timeout·Retry·Circuit Breaker·Kill Switch
- C26 에이전트 생명주기 — 등록·버전·활성화·폐기
Cross-reference
- C19 실험 파이프라인 — 가드 변경의 거버넌스 게이트
- C22 응답 품질 — Output Guard 결정과 quality 신호 연동