AIArchitectureSpring Boot

에이전트 하네스 설계: 성능을 만드는 건 모델이 아니라 그 주변 구조였다

같은 모델인데 어떤 에이전트는 일을 해내고 어떤 건 계속 헛돕니다. 도구를 몇 개 쥐어줄지, 권한을 어디까지 열지, 실패했을 때 어떻게 되돌릴지를 설계한 과정을 정리했습니다.

Srue2026년 9월 14일
에이전트 하네스 설계: 성능을 만드는 건 모델이 아니라 그 주변 구조였다

TL;DR

  • 모델을 바꿔서 해결되는 문제보다, 하네스를 고쳐서 해결되는 문제가 훨씬 많았습니다.
  • 도구는 많을수록 좋지 않습니다. 개수가 늘면 오선택이 같이 늘어납니다.
  • 권한은 도구 단위가 아니라 작업 단위로 끊어야 관리됩니다.
  • 에이전트는 반드시 실패합니다. 되돌릴 수 없는 작업을 도구로 주지 않는 것이 재시도 로직보다 먼저입니다.

지난 편에서 컨텍스트를 예산으로 보고 도구 정의와 대화 히스토리를 정리했습니다. 자리를 아끼고 나니 답변이 한결 또렷해졌습니다.

그런데 바로 다음 문제가 보였습니다. 컨텍스트가 깔끔해져도 에이전트가 엉뚱한 도구를 고르는 건 여전했습니다. 어떤 요청은 세 번 만에 끝내고, 비슷한 요청은 같은 도구를 다섯 번 호출하다 포기했습니다.

모델 탓인가 싶어 더 좋은 모델로 바꿔봤습니다. 나아지긴 했지만 패턴은 그대로였습니다. 틀리는 지점이 똑같았습니다.

그제서야 문제를 다시 봤습니다. 모델이 아니라 모델에게 쥐어준 환경이 문제였습니다.

하네스라는 말을 쓰게 된 이유

하네스(harness)는 원래 말에 씌우는 마구입니다. 말의 힘을 어디로 쓸지 정하는 장치죠. 말이 아무리 좋아도 마구가 엉망이면 마차는 앞으로 안 갑니다.

에이전트도 같았습니다. 모델 주변에는 이런 것들이 붙어 있습니다.

구성 요소결정하는 것
도구 집합무엇을 할 수 있는가
권한 경계어디까지 해도 되는가
실패 복구틀렸을 때 어떻게 되는가
작업 분리어디서 끊어 다른 주체에게 넘기는가

모델은 이 중 어느 것도 결정하지 않습니다. 전부 우리가 짭니다. 그런데 저는 그동안 모델만 바라보고 있었습니다.

도구 개수와 정확도는 반비례한다

가장 먼저 부딪힌 게 도구 선택이었습니다. Tool Calling을 붙일 때는 도구를 늘리는 게 기능을 늘리는 일이라고만 생각했습니다.

실제로는 이런 일이 벌어졌습니다.

도구 구성관찰된 현상
이름이 비슷한 도구 2개둘을 번갈아 호출하며 같은 결과를 두 번 받음
설명이 모호한 도구전혀 무관한 요청에도 호출됨
파라미터가 많은 도구인자를 잘못 채워 실패 후 재시도 반복
범용 도구 1개 (query, search)무엇을 하든 일단 이것부터 호출

마지막이 특히 고약했습니다. search처럼 아무 데나 쓸 수 있어 보이는 도구를 하나 넣어두니, 에이전트가 판단을 그쪽으로 미뤄버렸습니다.

정리하면서 세운 원칙은 세 가지였습니다.

첫째, 도구 이름과 설명은 경계를 긋는 문장이어야 합니다. "주문을 조회합니다"가 아니라 "주문번호로 단건 주문의 상태와 결제 정보를 조회합니다. 기간 검색에는 쓰지 않습니다"처럼요. 하지 말아야 할 것을 적는 게 효과가 컸습니다.

둘째, 도구는 상시 등록이 아니라 요청별 등록입니다. 의도 분류를 한 번 거쳐 그 요청에 필요한 것만 붙입니다. 자리도 아끼고 오선택도 줄어듭니다.

셋째, 파라미터는 줄입니다. 인자가 많으면 채우다 틀립니다. 기본값으로 처리할 수 있는 건 도구 안에서 처리하고, 에이전트에게는 정말 판단이 필요한 것만 넘깁니다.

권한은 도구 단위가 아니라 작업 단위로

처음엔 도구마다 권한을 붙였습니다. deleteOrder는 관리자만, getOrder는 누구나 — 이런 식으로요.

곧 관리가 안 됐습니다. 도구가 늘어날 때마다 권한 표가 같이 늘어났고, "이 도구 3개를 연달아 쓰면 사실상 삭제와 같다"는 조합을 놓쳤습니다.

그래서 작업 단위로 등급을 나눴습니다.

등급성격처리
읽기상태를 바꾸지 않음자동 실행
되돌릴 수 있는 쓰기실패해도 복구 가능자동 실행 + 감사 로그
되돌릴 수 없는 쓰기외부 전송, 결제, 삭제사람 승인 필수
금지에이전트가 할 이유가 없음도구로 제공하지 않음

마지막 줄이 핵심입니다. 가장 확실한 권한 통제는 도구를 주지 않는 것입니다. 승인 게이트를 아무리 잘 만들어도, 애초에 없는 도구는 호출될 수 없습니다.

승인이 필요한 도구는 이렇게 감쌌습니다.

@Tool(description = """
    주문을 취소하고 결제를 환불합니다.
    되돌릴 수 없으므로 반드시 사전 승인이 필요합니다.
    조회만 필요할 때는 findOrder 를 사용하세요.
    """)
public ToolResult cancelOrder(String orderNo, String reason) {
    ApprovalTicket ticket = approvalService.require(
            ApprovalType.IRREVERSIBLE,
            "주문 취소: %s (사유: %s)".formatted(orderNo, reason));
 
    if (!ticket.isApproved()) {
        // 실패가 아니라 "보류"로 알려야 에이전트가 재시도를 반복하지 않는다
        return ToolResult.pending(
                "승인 대기 중입니다. 승인 후 다시 요청하세요. ticket=" + ticket.id());
    }
 
    orderService.cancel(orderNo, reason);
    return ToolResult.ok("주문 %s 취소 완료".formatted(orderNo));
}

주석에 적은 게 실제로 중요했습니다. 거부를 실패로 돌려주면 에이전트가 같은 호출을 계속 반복합니다. "보류"라는 별도 상태로 알려주고, 다음에 무엇을 해야 하는지까지 문장으로 주면 그 자리에서 멈춥니다.

실패를 전제로 설계한다

에이전트는 반드시 틀립니다. 문제는 틀린 뒤에 무슨 일이 벌어지는가입니다.

겪은 실패를 유형별로 나눠보니 대응이 달라야 했습니다.

실패 유형잘못된 대응맞는 대응
인자를 잘못 채움같은 호출 재시도무엇이 틀렸는지 문장으로 알려주고 1회만 재시도
없는 대상 조회계속 다른 값으로 시도"없음"을 정상 결과로 반환하고 종료
외부 API 일시 오류에이전트가 판단코드 레벨에서 백오프 재시도. 에이전트에게 알리지 않음
권한 부족실패 반환보류 상태 + 다음 행동 안내
같은 도구 연속 호출방치호출 횟수 상한으로 강제 중단

세 번째가 중요합니다. 일시적 오류는 에이전트에게 올리면 안 됩니다. 토큰을 쓰며 "다시 해볼까?"를 판단하게 만들 이유가 없습니다. 재시도는 코드가 하고, 최종 실패만 올립니다.

다섯 번째는 안전장치입니다. 같은 도구를 3회 이상 연속 호출하면 강제로 끊고 사람에게 넘깁니다. 이게 없으면 루프에 빠져 토큰만 태웁니다.

그리고 되돌리기는 에이전트에게 맡기지 않았습니다. 되돌릴 수 없는 작업을 아예 승인 뒤로 보냈기 때문에, 자동 롤백이 필요한 구간 자체가 거의 사라졌습니다. 트랜잭션 경계를 정리할 때와 같은 결론이었습니다 — 롤백을 잘 만드는 것보다 롤백할 일을 안 만드는 게 낫습니다.

언제 서브에이전트로 쪼갤 것인가

하나의 에이전트가 모든 걸 하게 두니 도구가 계속 늘었고, 앞의 문제가 전부 따라왔습니다. 그래서 쪼갰는데, 쪼개는 기준이 없으면 오히려 복잡해집니다.

기준을 이렇게 잡았습니다.

쪼갠다쪼개지 않는다
도구 집합이 거의 겹치지 않음도구를 공유함
권한 등급이 다름 (읽기 전용 vs 승인 필요)같은 등급
실패 대응 방식이 다름같은 방식
중간 결과가 길어 컨텍스트를 많이 먹음결과가 짧음

마지막 항목이 실전에서 가장 유용했습니다. 긴 탐색을 서브에이전트에게 맡기고 요약만 받아오면, 상위 에이전트의 컨텍스트가 오염되지 않습니다. 지난 편에서 다룬 컨텍스트 예산 문제와 바로 이어집니다.

반대로 도구를 공유하는데 굳이 쪼개면, 같은 도구 정의를 두 곳에 실어 나르며 자리만 두 배로 씁니다.

정리 — 하네스 점검표

작업을 마치고 남긴 점검표입니다. 새 에이전트를 만들 때마다 이 순서로 봅니다.

#점검 항목
1도구 설명에 하지 말아야 할 것이 적혀 있는가
2범용 도구(search, query)로 판단을 미루고 있지 않은가
3도구를 상시 등록하고 있지 않은가
4되돌릴 수 없는 작업이 승인 없이 실행되지 않는가
5애초에 주지 말아야 할 도구가 들어가 있지 않은가
6일시적 오류를 에이전트가 판단하고 있지 않은가
7같은 도구 연속 호출에 상한이 있는가
8쪼갠 서브에이전트가 도구를 공유하고 있지 않은가

4번과 5번은 순서가 중요합니다. 5번을 먼저 봐야 합니다. 주지 않은 도구는 승인 게이트도 필요 없습니다.

그리고 이 모든 게 보이려면 로그가 있어야 합니다. 분산 추적을 붙였을 때처럼, 도구별 호출 횟수·실패율·연속 호출 패턴을 남기지 않으면 어디가 망가졌는지 알 수 없습니다.


다음 편

도구를 정리하고 권한을 끊고 나니, 이번엔 연동 비용이 눈에 들어왔습니다. 시스템 하나 붙일 때마다 도구를 새로 만들고 스키마를 새로 쓰는 일이 반복됐거든요.

다음 편에서는 그 반복을 줄이려는 표준, MCP 생태계를 다룹니다.

참고