Agent Harness라는 개념을 처음 접한 날
결제·정산 자동화를 직접 운영하는 팀이 Agent Harness를 처음 접하고, 실사고 경험과 코드 구현을 통해 이 구조가 왜 결제 현장에서 필수 설계 원칙인지 정리했다.
같은 루프를 세 번째 짜다가 손이 멈췄다
작년 말, 팀에서 LLM 기반 내부 도구를 거의 동시에 세 개 만들고 있었다. 정산 파일에서 이상 건을 탐지하는 스크립트, 은행 명세와 내부 원장을 비교해 불일치를 보고하는 도구, 특정 조건에서 환불을 자동 트리거하는 자동화였다. 각각 기획 목적은 달랐지만, 코드를 짜다 보면 묘하게 같은 구조가 반복됐다. 모델을 호출하고, 응답을 파싱하고, 필요하면 도구를 실행하고, 결과를 컨텍스트에 붙여 다시 모델에 넘기는 루프. 세 번째 도구의 코드를 열었을 때, 첫 두 줄을 보자마자 "이거 또 이 패턴이네"라는 생각과 함께 손이 멈췄다.
그때 코드 리뷰 중 agent harness라는 표현을 처음 접했다. 처음엔 또 다른 마케팅 용어겠거니 넘겼다. 그런데 읽을수록 달랐다. 이건 LLM 호출을 감싸는 편의 래퍼 클래스 이야기가 아니었다. 에이전트가 실행되는 환경 자체를 규정하는 구조—어떤 도구를 허용할지, 오류가 나면 어떻게 처리할지, 몇 번까지 재시도할지, 루프를 끊는 조건은 무엇인지를 하나의 레이어에서 일관되게 다루는 것이 harness였다. 결제·정산 현장에서 이게 왜 중요한지는 코드를 보는 순간 바로 납득됐다.
Harness가 없으면 결제 현장에서 벌어지는 일
일반적인 챗봇이나 정보 검색 에이전트라면, 루프가 한 번 더 돌거나 재시도가 한 번 더 일어나도 큰 문제가 없다. 최악의 경우 응답이 조금 느려지거나 내용이 약간 달라진다. 하지만 결제와 정산은 다르다. 잘못된 재시도 한 번이 이중 청구가 되고, 컨텍스트 관리 실패 하나가 원장 불일치로 이어진다. 에러 처리 정책이 도구마다 제각각이면, 같은 오류 상황에서 어떤 도구는 3번 재시도하고 어떤 도구는 그냥 넘어간다—두 행동 모두 정답이 아닐 수 있는데도.
우리 팀에서 직접 겪은 일이다. 정산 자동화용 LLM 에이전트에 외부 PG사 API를 조회하는 도구를 붙였는데, 초기 구현에서는 도구 레벨 재시도 로직도, 에이전트 루프 최대 반복 횟수 제한도 없었다. PG사 API가 타임아웃을 반환하자 모델은 동일한 도구를 계속 호출했다. 결과적으로 5분 안에 해소됐어야 할 조회가 API rate limit에 걸렸고, 해당 정산 배치 전체가 지연됐다. 원인 파악에만 한 시간 가까이 걸렸다. harness가 있었다면 "최대 반복 3회, 도구 실패 시 즉시 중단"이라는 조건 두 줄로 막을 수 있는 일이었다.
Harness가 실제로 규정하는 네 가지
Harness를 이해하는 가장 빠른 방법은 테스트 harness와 비교하는 것이다. 테스트를 짤 때 우리는 개별 테스트 케이스에만 집중한다. 테스트 러너가 실행 환경을 어떻게 세팅하고, 결과를 어떻게 수집하고, 실패 시 어떻게 리포팅하는지는 러너가 알아서 한다. Agent harness도 동일한 관심사 분리다. 에이전트 로직(무엇을 판단하고 무엇을 실행할지)과 실행 인프라(어떻게 실행하고 어떻게 통제할지)를 분리한다.
우리 기준으로 harness가 담당하는 핵심 항목은 네 가지다. (1) 루프 종료 조건 — 모델이 완료라고 판단하거나, 최대 반복 횟수(우리는 보통 58회)에 도달하거나, 전체 타임아웃(60초)이 지나면 루프를 강제 종료한다. (2) 도구 권한 관리 — 어떤 에이전트가 어떤 도구에 접근할 수 있는지를 harness 레이어에서 선언한다. 정산 조회 에이전트가 환불 실행 API를 호출하는 일은 모델 프롬프트로 막는 게 아니라 registry에서 막는다. (3) 컨텍스트 크기 제어 — 128k 토큰 컨텍스트는 도구 실행 결과가 쌓이면 생각보다 빨리 찬다. 정산 조회 한 건에 수천 건 거래 내역이 붙으면 34회 루프만으로도 한계에 근접한다. Harness는 오래된 도구 실행 결과를 요약·압축하거나 잘라낸다. (4) 실행 감사 로그 — 규제 환경에서는 에이전트가 어떤 판단으로 어떤 도구를 실행했는지가 감사 대상이다. 도구 결과만 기록하면 부족하다. Harness가 "몇 번째 턴에, 어떤 도구를, 어떤 인자로"를 일관되게 기록해야 한다.
직접 구현하면서 달라진 시각
개념을 이해한 뒤 간단한 harness를 직접 구현해봤다. 핵심 루프는 30줄 남짓이었다.
MAX_TURNS = 5
TIMEOUT_SEC = 60
while turn < MAX_TURNS and elapsed < TIMEOUT_SEC:
response = model.call(context)
if response.is_tool_call:
tool = registry.get(response.tool)
if tool is None:
raise HarnessError("unauthorized_tool", response.tool)
result = tool.execute_idempotent(response.args)
context.append(result)
audit_log(turn, response.tool, response.args, result)
else:
return response.text
turn += 1
raise HarnessError("max_turns_exceeded")
단순해 보이지만 이 코드에서 결제 현장 관점의 핵심은 두 군데다. 첫째, registry.get(response.tool) — 모델이 요청하는 도구가 이 에이전트에게 허용된 도구인지를 harness가 런타임에서 검증한다. 모델이 아무리 "환불 API를 호출해야 한다"고 판단하더라도 registry에 없으면 실행되지 않는다. 프롬프트 레벨의 지시는 모델이 무시하거나 해석을 달리할 수 있지만, registry 검증은 코드가 막는다. 둘째, execute_idempotent() — 동일한 tool + args 조합이 같은 세션에서 두 번 호출되면 두 번째는 첫 번째 결과를 캐시에서 반환한다. 결제 환경에서 이중 실행은 이중 청구로 직결된다. 이 두 가지만 있어도 없는 것에 비해 사고 발생 경로가 현저히 줄었다.
다중 에이전트 구조에서 Harness의 경계 문제
하나를 만들고 나면 다음 질문이 자연스럽게 온다. 에이전트를 여러 개 엮으면 어떻게 되나? 정산 조회 에이전트, 불일치 탐지 에이전트, 보고서 생성 에이전트가 서로를 도구처럼 호출하는 구조에서는 harness가 어디서 어디까지를 담당해야 하는가. 단일 에이전트에서는 명확했던 경계가 다중 에이전트 구조에서는 흐릿해진다. 상위 에이전트가 하위 에이전트를 도구로 부를 때, 하위 에이전트의 실패를 상위 harness가 어떻게 처리할지가 즉시 모호해진다.
우리가 현재 택한 방식은 이렇다. 각 에이전트는 자신만의 harness를 가진다. 상위 에이전트가 하위 에이전트를 "도구"로 호출할 때, 그 도구의 내부 실행은 하위 에이전트의 harness가 책임지고, 상위 harness는 성공/실패 여부만 본다. 이렇게 하면 각 레이어의 책임이 명확해지고, 하위 에이전트 내부의 재시도나 컨텍스트 관리가 상위 로직에 영향을 주지 않는다. 단, 아직 해결하지 못한 문제가 하나 있다. 하위 에이전트가 외부 API에 쓰기(write) 작업을 실행한 뒤 상위 harness가 전체를 롤백하려 할 때, 이미 실행된 외부 작업을 어떻게 처리할 것인가. 현재는 쓰기 작업이 포함된 에이전트에는 반드시 사람 확인 스텝을 두는 방식으로 우회하고 있다. 완전한 답은 아직 없다.
지금 당장 써먹을 수 있는 체크리스트
LLM 에이전트를 처음 만들거나 기존 코드에 harness 개념을 도입하려는 팀이라면, 복잡한 프레임워크보다 아래 네 가지를 먼저 코드에 박아두길 권한다. 이 순서대로 작업하면 된다.
① 종료 조건을 루프보다 먼저 정의한다. 루프 로직을 짜기 전에 "이 에이전트는 언제 멈춰야 하는가"를 코드로 명시하라. 최대 반복 횟수, 전체 타임아웃, 도구 실패 시 즉시 중단 여부. 이 세 가지가 없는 채로 배포된 에이전트는 예상치 못한 루프에 반드시 빠진다—운이 좋으면 rate limit에 걸려 알아채고, 운이 나쁘면 이중 실행까지 간다.
② 도구 허용 목록을 프롬프트가 아닌 코드로 강제한다. "이 도구만 쓰세요"를 프롬프트에 적는 건 권고일 뿐이다. Registry를 만들고 런타임에서 검증하라. 허용되지 않은 도구 호출은 에러로 처리하고 audit log에 남겨라. 이게 나중에 감사 대응과 이상 탐지에 직접 쓰인다.
③ 쓰기(write) 도구는 조회(read) 도구와 레지스트리를 분리한다. 조회와 실행을 같은 도구 목록에 섞지 마라. 조회 전용 에이전트와 실행 에이전트를 별도로 설계하고, 실행 에이전트는 명시적 트리거(사람 확인 또는 별도 승인 플로우) 없이는 작동하지 않게 한다. 이 원칙 하나가 자동화 관련 사고 가능성을 유의미하게 낮춘다.
④ 도구 결과가 아니라 harness 결정을 로그에 남긴다. "어떤 도구가 무슨 결과를 반환했나"만 기록하면 부족하다. "몇 번째 턴에, 어떤 이유로, 어떤 종료 조건이 트리거됐나"를 함께 기록하라. 문제가 생겼을 때 재현과 원인 파악에 드는 시간이 극적으로 줄어든다. 우리 기준으로 로그에 turn, tool_name, args_hash, exit_reason 네 필드만 추가해도 사고 대응 속도가 체감될 만큼 달라졌다.
개념에 이름을 붙일 수 있다는 건, 다음에 같은 문제를 설계할 때 처음부터 다시 시작하지 않아도 된다는 뜻이다. Harness라는 단어를 팀이 공유하고 나서, 에이전트 설계 대화가 눈에 띄게 빨라졌다. "이 도구 retry는 harness 레벨에서 처리할까, 도구 레벨에서 처리할까"—이 질문 하나만 해도 논의가 구체적인 지점에서 시작된다.
— by slecs
* 위 링크는 인프런 affiliate 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.
* 위 추천 링크는 쿠팡파트너스 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.