본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 09. 11:17

에이전트 시리즈 (17): 하네스 엔지니어링 (Harness Engineering) — 자율 에이전트에 안전 장치(Safety Harness)

요약

자율 에이전트의 위험성을 제어하기 위한 '하네스 엔지니어링(Harness Engineering)' 개념과 구현 방법을 다룹니다. 에이전트의 능력을 유지하면서도 행동 경계를 정의하여 안전한 자율성을 확보하는 다섯 가지 핵심 요소를 설명합니다.

핵심 포인트

  • 하네스 엔지니어링은 에이전트의 행동 경계를 정의하여 리스크를 관리함
  • 액션 스페이스를 통해 화이트리스트 방식으로 도구 호출을 제어함
  • LangGraph의 interrupt를 활용한 인간 체크포인트 구현 방법 제시
  • 실행 경계 설정을 통해 무한 루프 및 토큰 예산 소진 방지

더 자율적일수록, 더 위험하다

에이전트는 파일을 읽고, 코드를 작성하며, API를 호출하고, 이메일을 보낼 수 있습니다. 주어진 작업에 대해 무엇을 할지, 어떻게 할지, 그리고 어디까지 진행할지를 자율적으로 결정합니다.

그것이 바로 에이전트의 가치이자, 동시에 가장 큰 리스크입니다.

"더 자율적이다"라는 것이 "더 낫다"는 것을 의미하지는 않습니다. 제약이 없는 에이전트는 다음과 같은 행동을 할 수 있습니다:

  • 의도하지 않은 도구(Tool) 호출
  • 사용자 모르게 데이터 수정
  • 무한 루프에 빠져 토큰 예산(Token budget)을 모두 소진
  • 추적하거나 되돌리는 것이 불가능한 방식으로 실패

하네스 엔지니어링(Harness Engineering)의 핵심 아이디어는 다음과 같습니다: 에이전트의 능력을 깎아내리지 않으면서 행동의 경계(Behavioral boundaries)를 정의하는 것. 즉, "에이전트가 아무것도 못 하게 하는 것"이 아니라, "제어된 범위(Controlled envelope) 내에서 에이전트가 자율적으로 행동하게 하는 것"입니다.

이 글에서는 세 가지의 직관에 반하는 발견을 포함하여, 실제 벤치마크(Benchmark) 결과와 함께 다섯 가지 요소를 다룹니다.

하네스 엔지니어링의 다섯 가지 요소

요소 1  액션 스페이스 (Action Space)       도구 화이트리스트(Whitelist) — 승인되지 않은 호출 차단
요소 2  휴먼 체크포인트 (Human Checkpoint) 위험한 작업 전 일시 중지 및 승인 대기
요소 3  실행 경계 (Execution Boundary) 최대 단계 제한(Max-step cap)을 통한 폭주 에이전트 방지
...

데모 1: 액션 스페이스 (Action Space) — 레지스트리(Registry)가 곧 경계다

설계 원칙: 허용되는 것을 명시적으로 선언하고, 그 외의 모든 것은 거부한다 (블랙리스트가 아닌 화이트리스트 방식).

ACTION_SPACE: dict[str, dict] = {
    "read_report":  {"risk": "safe",  "needs_approval": False},
    "write_report": {"risk": "risky", "needs_approval": True},
...

세 가지 도구: read_report (읽기 전용), write_report (쓰기), delete_records (위험함, 등록되지 않음).

harness_tools_node는 무언가를 실행하기 전에 레지스트리를 확인합니다:

if name not in ACTION_SPACE:
    audit(name, "blocked", "BLOCKED", "not in action space")
    result_text = (
...

테스트: "사용자 테이블의 모든 레코드를 삭제해줘."

Query: 'Delete all records from the users table.'
Answer: 죄송합니다만, 모든 레코드를 삭제할 수 없습니다...

...

delete_records는 호출된 적이 없습니다. 감사 로그(audit log)에는 BLOCKED(차단됨)라고 기록되었습니다. LLM은 에러 문자열을 읽고 정중한 거절로 응답했습니다.

핵심 요약: 레지스트리(registry)는 LLM의 의도와 무관하게 도구 실행 계층(tool-execution layer)에서 가로챕니다. LLM이 해당 도구를 호출하기를 강력하게 "원하더라도", 하네스(harness)는 도구 노드(tools node)에서 이를 차단합니다.

데모 2–4: 인간 체크포인트(Human Checkpoint) — LangGraph interrupt

이것이 핵심 메커니즘입니다. interrupt()는 LangGraph의 네이티브 일시 중지 프리미티브(pause primitive)입니다. 커스텀 도구 노드 내부에서 interrupt(data)를 호출하면 그래프가 즉시 중단됩니다. Command(resume=value)를 통해 재개할 수 있으며, 이때 interrupt()는 해당 값을 반환합니다.

from langgraph.types import Command, interrupt

def harness_tools_node(state):
...

외부에서 호출자는 일시 중지를 감지하고 재개합니다:

state = harness_app.get_state(config)
if state.next:
    interrupt_data = state.tasks[0].interrupts[0].value
...

세 가지 테스트 결과:

Demo 2 — 안전한 동작 (read_report):
  질의: 'q1_sales 보고서에는 무엇이 들어있나요?'
  [체크포인트 트리거되지 않음]
...

데모 2의 직관에 반하는 결과: LLM은 도구를 전혀 호출하지 않았습니다. 대신 "계속 진행할까요?"라고 물었습니다. 가로챌 도구 호출 자체가 없었기 때문에 interrupt()는 실행되지 않았습니다. 이는 모델 역량(model-capability)의 문제로, 15번 글에서 발견한 MemorySaver 사례와 동일합니다. 즉, 인프라 계층은 올바르게 작동하지만, 모델 계층이 여전히 병목(bottleneck)입니다.

데모 4의 결정적인 발견: 이것이 가장 중요한 결과입니다. 감사 로그(audit log)가 진실을 말해줍니다:

Audit Trail:
  risky   rejected   write_report   human rejected (decision='rejected')
  # "file=override.txt" 항목 없음 — 도구가 호출되지 않음

write_report결코 실행되지 않았습니다. 파일은 결코 작성되지 않았습니다. 하네스는 도구 노드에서 쓰기 작업을 올바르게 차단했습니다.

하지만 LLM의 답변은 "파일이 성공적으로 생성되었습니다"라고 말했습니다 — **모델 환각(model hallucination)**입니다. LLM은 "작업이 거부되었습니다"라는 ToolMessage를 받았음에도 불구하고, 사실과 모순되는 응답을 생성했습니다.

하네스(Harness)는 모델의 거짓말이 아니라 행동을 차단합니다. 실제 파일 시스템은 안전하지만, 사용자에게 전달되는 답변은 틀린 상태입니다. 이를 해결하려면 하네스 상단에 출력 검증 계층 (output-validation layer)을 두거나, 더 강력한 지시 이행 능력 (instruction-following capability)을 가진 모델을 사용해야 합니다.

데모 5: 실행 경계 (Execution Boundary) — 그래프 레벨이 올바른 수준입니다

저의 초기 구현 방식은 agent.invoke()while 루프 내에 감싸고, 각 호출 이후에 도구 호출 (tool-call) 단계를 계산하는 방식이었습니다:

# 이 구현은 잘못되었습니다
def run_bounded(query, max_steps):
    while True:
...

벤치마크를 통해 결함이 드러났습니다:

[multi-step, max_steps=1]
  Status : completed  |  Steps used: 3
  Answer : The combined report has been saved to combined.txt.

max_steps=1임에도 불구하고 3단계가 실행되었습니다. 이유는 다음과 같습니다: create_react_agent는 내부적으로 전체 ReAct 루프를 실행합니다. invoke()가 반환될 때쯤에는 이미 모든 작업이 완료된 상태입니다. 외부 카운터는 사후 기록 (post-hoc bookkeeping)일 뿐이며, 실행 도중에 중단할 수 없습니다.

올바른 접근 방식: LangGraph의 그래프 레벨 재귀 제한 (graph-level recursion limit)을 사용하세요:

result = harness_app.invoke(
    {"messages": [HumanMessage(query)]},
    config={
...

recursion_limit은 LangGraph의 스케줄러 (scheduler)에 의해 강제됩니다. 이 제한을 초과하면 LangGraph는 GraphRecursionError를 발생시키며 실행을 실제로 중단합니다. 이는 사후에 숫자를 세는 것과는 다릅니다.

데모 6: 롤백 (Rollback) — 쓰기 작업 주변의 컨텍스트 매니저 (Context Manager)

핵심 패턴은 다음과 같습니다: 쓰기 전 스냅샷 (snapshot) 생성, 실패 시 복구. contextmanager를 이용한 구현이 가장 간단합니다:

@contextmanager
def rollback_on_failure(state: dict, op_name: str):
    snapshot = copy.deepcopy(state)
...

사용법 — 모든 쓰기 작업을 with 문으로 감쌉니다:

with rollback_on_failure(SYSTEM_CONFIG, "bad_version_bump"):
    SYSTEM_CONFIG["version"] = "2.0"
    raise ValueError("Version incompatible")   # 롤백을 트리거함
...

벤치마크 결과:

Test B — failed update:
  Snapshot: {'version': '1.0', 'timeout': 60, 'max_retries': 3}
  'bad_version_bump' FAILED (Version 2.0 incompatible)
...

동일한 패턴이 데이터베이스 (Database) 작업에도 적용됩니다. DB 트랜잭션 (Transaction)을 rollback_on_failure 내부에 감싸고, 예외 처리기 (Exception handler)에서 ROLLBACK을 실행하십시오.

완전한 감사 추적 (Complete Audit Trail)

여섯 가지 데모를 모두 마친 후의 감사 로그 (Audit log)는 다음과 같습니다:

시간(Time)   위험도(Risk)   결과(Result)   작업(Action)  (비고(note))
----------------------------------------------------------------------
16:36:26     차단됨(blocked)  차단됨(BLOCKED)  delete_records  작업 공간(action space)에 없음
...

모든 항목에는 타임스탬프 (Timestamp), 위험 수준 (Risk level), 결과 (Result), 작업 이름 (Operation name), 그리고 비고 (Notes)가 포함됩니다. 추가 전용 쓰기 의미론 (Append-only write semantics, 기존 레코드 수정 불가)과 결합된 이 로그는 컴플라이언스 감사 (Compliance auditing)에 즉시 사용할 수 있습니다.

설계 체크리스트 (Design Checklist)

작업 공간 (Action Space)

  • 화이트리스트 (Whitelist) 원칙: 허용된 도구 (Tools)를 명시적으로 선언하고, 그 외의 모든 것은 거부할 것
  • 위험 계층 (Risk tiers): safe (자동 실행) / risky (승인 필요) / 부재 (영구 차단)
  • 세분성 (Granularity): 도구당 하나의 등록 항목을 가질 것; 고위험 작업과 저위험 작업을 병합하지 말 것

인간 체크포인트 (Human Checkpoint)

  • 일시 중지 및 재개를 위해 LangGraph의 interrupt() + Command(resume=...)를 사용할 것
  • 확인 로직을 에이전트 노드 (Agent node)가 아닌 도구 노드 (Tools node)에 구현할 것
  • 체크포인트 데이터는 인간의 의사결정을 위해 충분한 컨텍스트 (Context, 도구 이름, 인자, 위험 수준)를 포함해야 함
  • 환각된 승인 (Hallucinated confirmations)을 줄이기 위해 더 강력한 모델 (GPT-4/Claude) + 출력 검증 (Output validation)을 사용할 것

실행 경계 (Execution Boundary)

  • 외부 루프 카운터가 아닌 LangGraph의 그래프 수준 recursion_limit을 사용할 것
  • 운영 환경 권장 사항: 간헐적인 무한 루프를 처리하기 위해 recursion_limit을 10~20으로 설정할 것

감사 로그 (Audit Log)

  • 추가 전용 쓰기 (Append-only writes); 기존 레코드는 불변 (Immutable)이어야 함
  • 각 항목: 타임스탬프 / 작업 / 위험 수준 / 결과 / 주요 인자 (Key args)
  • 성공한 작업뿐만 아니라 차단 및 거부된 로그도 기록할 것

롤백 (Rollback)

  • 쓰기(write) 작업 전 copy.deepcopy()를 사용하여 스냅샷을 생성하거나, git stash 또는 DB 트랜잭션(DB transactions)을 사용합니다.
  • 쓰기 블록을 컨텍스트 매니저(context manager)로 감쌉니다. 예외(exception) 발생 시 자동으로 복구가 트리거됩니다.
  • 되돌릴 수 없는 작업(파일 삭제 등)은 추가적인 인간 체크포인트(human checkpoint)를 거칩니다. 롤백(rollback)은 최후의 수단입니다.

요약 (Summary)

다섯 가지 핵심 요점:

  1. 레지스트리(Registry)가 가장 신뢰할 수 있는 방어 수단입니다: 등록되지 않은 도구(tool)는 LLM의 의도와 상관없이 절대 실행되지 않으며, 별도의 프롬프트 조작(Prompt wrangling)도 필요하지 않습니다.
  2. interrupt()는 인간 체크포인트를 위한 올바른 도구입니다: 이는 LLM이 "자발적으로 따르기"를 기대하는 것이 아니라, 스케줄러(scheduler) 수준에서 실행을 일시 중지합니다.
  3. 하네스(Harness)는 모델의 거짓말이 아닌 행동을 차단합니다: 데모 4가 이를 명확히 보여줍니다. 파일은 실제로 작성되지 않았지만, LLM은 성공했다고 보고했습니다. 출력의 신뢰성은 모델의 능력에 달려 있습니다.
  4. 실행 경계(Execution boundary)는 반드시 그래프 수준이어야 합니다: recursion_limit는 실제 차단 지점입니다. 외부 루프 카운터(outer-loop counter)는 단지 사후 기록(post-hoc bookkeeping)에 불과합니다.
  5. 다섯 가지 요소는 상호 보완적입니다: 레지스트리는 승인되지 않은 작업을 차단하고, 체크포인트는 위험한 작업을 처리하며, 경계는 폭주하는 루프를 방지하고, 감사(audit)는 추적을 가능하게 하며, 롤백은 복구를 가능하게 합니다. 각 요소는 다른 요소가 남기는 사각지대를 메워줍니다.

다음 편: 비용 및 성능 최적화 (Cost and Performance Optimization) — 프롬프트 캐싱(Prompt Caching)이 비용을 어떻게 절감하는지, 모델 라우팅(model routing)이 속도와 품질의 균형을 어떻게 맞추는지, 그리고 도구 호출(tool calls)의 병렬화가 어떻게 단계(step) 수를 줄이는지에 대해 다룹니다.

참고 문헌 (References)

실제 엔터프라이즈급 워크플로에서 검증된 AI 에이전트와 기술의 큐레이션 마켓플레이스인 PrimeSkills를 확인해 보세요. 불필요한 내용은 빼고, 실제로 작동하는 것들만 제공합니다.

홈페이지에서 더 유용한 지식과 흥미로운 제품들을 찾아보세요.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0