Published on

AutoGPT 도구 루프 폭주, 예산과 가드레일로 차단

Authors

서론

AutoGPT 계열 에이전트(도구 호출 기반 에이전트)는 한 번 “생각-행동-관찰” 루프가 돌기 시작하면, 종료 조건이 약하거나 도구 결과가 애매할 때 같은 검색을 반복하거나 무의미한 리트라이를 계속하며 비용을 폭주시킬 수 있습니다. 특히 searchbrowser 같은 외부 도구는 네트워크 지연과 요금이 붙고, code 실행은 실패 후 재시도를 유도하기 쉽습니다. 문제는 단순히 “프롬프트를 잘 쓰자”로 끝나지 않습니다. 시스템 레벨에서 예산(budget)과 가드레일(guardrails)을 설계해야 합니다.

이 글은 “도구 루프 폭주”를 다음 4가지 관점으로 차단하는 방법을 정리합니다.

  • 예산: 토큰, 스텝, 시간, 비용, 도구별 쿼터를 강제
  • 종료: 명시적 종료 조건과 실패 한도, 동일 행동 반복 감지
  • 안전: 도구 입력 검증, 샌드박스, 허용 목록
  • 관측: 루프의 원인과 패턴을 로그/메트릭으로 재현 가능하게

메모리/컨텍스트가 비대해져 루프가 더 심해지는 경우도 많습니다. 메모리 압축과 벡터DB 튜닝은 별도 글인 AutoGPT 메모리 폭주? 벡터DB+요약압축 튜닝도 함께 참고하면 좋습니다.

1) 도구 루프 폭주가 생기는 대표 패턴

1-1. “애매한 관찰”로 인한 재검색 루프

검색 결과가 목표를 충족하지 못하거나, 결과 요약이 부정확하면 에이전트는 같은 쿼리를 조금씩 바꿔 반복합니다. 예를 들어 “A 라이브러리 최신 버전”을 찾는데 페이지 접근이 막혀 있거나, 결과가 스팸이면 search 를 계속 호출합니다.

1-2. “실패 후 즉시 재시도” 루프

도구 호출이 timeout 이나 429 로 실패하면, 에이전트가 동일 요청을 그대로 재시도합니다. 사람이라면 백오프나 대체 전략을 쓰겠지만, 가드레일이 없으면 무한 재시도로 이어집니다.

1-3. “목표가 모호”해서 종료를 못 하는 루프

목표가 “조사해줘”처럼 끝이 없으면, 에이전트는 충분 조건을 만족하지 못해 계속 탐색합니다. 결과물이 어느 수준이면 종료할지, “완료”의 정의가 빠져 있는 경우가 많습니다.

1-4. “도구 체인”이 자기증식하는 루프

search 로 찾은 링크를 browser 로 열고, 거기서 또 링크를 따라가고, 다시 search 로 돌아오는 식의 체인이 길어집니다. 이때 스텝 제한이 없으면 비용이 급격히 늘어납니다.

2) 예산 설계: 토큰·스텝·시간·비용을 동시에 걸어라

예산은 한 가지로는 부족합니다. 토큰이 남아도 시간이 오래 걸릴 수 있고, 스텝이 남아도 외부 도구가 비싸질 수 있습니다. 실무에서는 최소한 아래 4종을 같이 둡니다.

  • 스텝 예산: 루프 반복 횟수 상한
  • 토큰 예산: 입력/출력 토큰 상한
  • 시간 예산: 벽시계 시간 상한
  • 비용 예산: 모델/도구별 금액 상한

2-1. 스텝 예산: “생각-행동-관찰” 루프 상한

스텝 예산은 가장 직관적인 안전장치입니다. 예를 들어 20스텝이 넘어가면 강제 종료하고 “현재까지 결과 + 다음에 할 일”을 반환하게 만듭니다.

from dataclasses import dataclass
import time

@dataclass
class Budget:
    max_steps: int = 20
    max_seconds: int = 60

class BudgetExceeded(Exception):
    pass

class BudgetGuard:
    def __init__(self, budget: Budget):
        self.budget = budget
        self.start = time.time()
        self.steps = 0

    def tick(self):
        self.steps += 1
        if self.steps > self.budget.max_steps:
            raise BudgetExceeded(f"step budget exceeded: {self.steps}")
        if time.time() - self.start > self.budget.max_seconds:
            raise BudgetExceeded("time budget exceeded")

핵심은 “예산 초과 시 무엇을 반환할지”입니다. 실패로 끝내면 사용자 경험이 나빠지고, 재시도가 또 폭주를 유발합니다. 예산 초과 시에는 다음을 반환하는 패턴이 좋습니다.

  • 현재까지의 근거와 결과 요약
  • 남은 불확실성
  • 다음 스텝 제안(사람 승인 필요)

2-2. 도구별 쿼터: search 는 5회, browser 는 3회 같은 상한

루프 폭주는 특정 도구에서 발생하는 경우가 많습니다. 따라서 도구별 호출 횟수와 비용을 따로 제한합니다.

from collections import defaultdict

class ToolQuotaExceeded(Exception):
    pass

class ToolQuota:
    def __init__(self, limits: dict[str, int]):
        self.limits = limits
        self.counts = defaultdict(int)

    def consume(self, tool_name: str):
        self.counts[tool_name] += 1
        limit = self.limits.get(tool_name)
        if limit is not None and self.counts[tool_name] > limit:
            raise ToolQuotaExceeded(
                f"tool quota exceeded: {tool_name} {self.counts[tool_name]}/{limit}"
            )

quota = ToolQuota({
    "search": 5,
    "browser": 3,
    "python": 10,
})

도구별 쿼터는 “비싼 도구를 먼저 잠그는” 전략이 중요합니다. 예를 들어 사내에서는 browser 가 외부 망을 타고, 보안 프록시를 거쳐 지연과 비용이 커질 수 있습니다.

2-3. 토큰 예산: 모델 레벨에서 강제하고, 누적도 추적

모델 호출 시 max_output_tokens 같은 하드 리밋을 걸고, 세션 누적 토큰도 별도로 계산합니다. 모델 SDK가 제공하는 사용량 필드를 로그로 남겨야 사후 분석이 가능합니다.

class TokenBudgetExceeded(Exception):
    pass

class TokenBudget:
    def __init__(self, max_total_tokens: int):
        self.max_total = max_total_tokens
        self.used = 0

    def add_usage(self, prompt_tokens: int, completion_tokens: int):
        self.used += prompt_tokens + completion_tokens
        if self.used > self.max_total:
            raise TokenBudgetExceeded(
                f"token budget exceeded: {self.used}/{self.max_total}"
            )

2-4. 비용 예산: “도구 비용”까지 합산해야 진짜 예산이다

에이전트는 LLM 토큰 비용뿐 아니라 외부 API 과금, 크롤링 비용, 실행 환경 비용까지 발생시킵니다. 따라서 비용 예산은 “추정치라도” 합산해야 합니다.

  • LLM: 토큰 기반
  • 검색 API: 요청당 과금
  • 브라우징: 프록시/스크래핑 인프라 비용
  • 코드 실행: 워커/컨테이너 비용

비용을 정확히 계산하기 어렵다면, 최소한 도구별 “가중치 점수”라도 두고 점수 예산을 넘으면 중단하도록 설계합니다.

3) 가드레일 설계: 종료 조건, 반복 감지, 실패 한도

예산은 최후의 안전장치입니다. 폭주를 “빨리 멈추는” 것도 중요하지만, 더 중요한 건 “폭주가 시작되기 전에 방향을 바꾸는” 것입니다.

3-1. 종료 조건을 시스템 프롬프트가 아니라 런타임 규칙으로 강제

프롬프트에 “충분하면 종료해”라고 써도, 모델이 과도하게 탐색할 수 있습니다. 종료 조건은 코드로 강제하는 편이 안정적입니다.

  • 목표 달성 체크리스트를 구조화
  • 체크리스트 충족 시 즉시 종료
  • 충족 불가 시 “사람 승인 요청” 상태로 전환

예시: “최신 버전 확인” 작업이라면

  • 2개 이상의 독립 출처 확인
  • 버전 문자열이 일치
  • 날짜가 최근 90일 이내

이 중 하나라도 안 되면 종료하지 말고 “불확실성”을 리포트하게 만듭니다.

3-2. 동일 행동 반복 감지: 액션 시그니처로 루프를 끊기

도구 폭주의 핵심은 “거의 같은 행동”을 반복하는 것입니다. 도구 이름 + 정규화된 입력을 해시로 만들어, 동일 시그니처가 N회 반복되면 중단하거나 다른 전략을 강제합니다.

import json
import hashlib

def action_signature(tool: str, tool_input: dict) -> str:
    normalized = json.dumps(tool_input, sort_keys=True, ensure_ascii=False)
    raw = f"{tool}:{normalized}".encode("utf-8")
    return hashlib.sha256(raw).hexdigest()

class RepeatGuard:
    def __init__(self, max_repeats: int = 2):
        self.max_repeats = max_repeats
        self.count = {}

    def check(self, sig: str):
        self.count[sig] = self.count.get(sig, 0) + 1
        if self.count[sig] > self.max_repeats:
            raise RuntimeError("repeat loop detected")

실무 팁은 “정규화”입니다. 예를 들어 검색 쿼리에서 공백/대소문자/불필요한 파라미터를 제거해야 진짜 반복을 잡아냅니다.

3-3. 실패 한도와 백오프: 429timeout 은 정책으로 처리

도구 실패는 모델이 해결하기 어렵습니다. 실패는 런타임 정책으로 분기하세요.

  • timeout : 동일 요청 1회만 재시도, 이후 다른 도구로 대체
  • 429 : 지수 백오프 후 1회 재시도, 이후 중단
  • 5xx : 백오프 후 재시도, 그래도 실패면 회로 차단
import random
import time

def backoff_sleep(attempt: int, base: float = 0.5, cap: float = 8.0):
    delay = min(cap, base * (2 ** attempt))
    jitter = random.random() * 0.1 * delay
    time.sleep(delay + jitter)

3-4. “사람 승인” 게이트: 위험 도구에만 선택적으로 적용

모든 행동을 승인받게 하면 에이전트의 장점이 사라집니다. 대신 위험도가 높은 도구에만 게이트를 둡니다.

  • 외부 네트워크 접근 browser
  • 결제/구매/삭제 계열 API
  • 대량 데이터 조회 쿼리
  • 파일 시스템 쓰기

승인 게이트는 다음 정보를 함께 보여줘야 합니다.

  • 실행하려는 도구와 입력
  • 예상 비용/시간
  • 기대 결과
  • 실패 시 영향

4) 도구 입력 검증과 허용 목록: “할 수 있는 것”을 줄이면 폭주도 줄어든다

에이전트가 폭주하는 이유 중 하나는 선택지가 너무 많기 때문입니다. 도구를 많이 줄수록 안정성은 올라갑니다.

4-1. 도메인 허용 목록: 브라우저는 특정 도메인만

예를 들어 문서 조사 에이전트라면 docs 도메인만 열게 하거나, 사내 위키만 허용합니다.

from urllib.parse import urlparse

ALLOW_HOSTS = {"docs.python.org", "developer.mozilla.org"}

def validate_url(url: str):
    host = urlparse(url).netloc
    if host not in ALLOW_HOSTS:
        raise ValueError("host not allowed")

4-2. JSON 스키마로 도구 입력을 강제

도구 입력은 자연어가 아니라 구조화된 JSON이어야 합니다. 스키마 검증을 통과하지 못하면 도구를 실행하지 말고, 모델에게 “입력 형식 오류”를 반환해 수정하게 하세요.

  • 필수 필드 누락 방지
  • 문자열 길이 제한
  • 페이지 수, 결과 수 상한

5) 관측성: 폭주를 “재현 가능”하게 만들어야 고친다

도구 루프는 재현이 어려운 편입니다. 따라서 실행 트레이스를 남겨야 합니다.

필수로 남길 것

  • run_idstep 번호
  • 모델 호출: 프롬프트 토큰, 출력 토큰, 지연 시간
  • 도구 호출: 도구명, 입력, 결과 요약, 지연 시간, 에러 코드
  • 종료 이유: 정상 완료, 예산 초과, 반복 감지, 승인 대기

관측성은 “에이전트版 CrashLoop”를 잡는 과정과 유사합니다. 반복 실패의 원인을 빠르게 좁히는 접근은 K8s CrashLoopBackOff 원인 10분 진단법에서 소개한 것처럼, 원인 분류와 지표 기반 진단이 핵심입니다.

6) 실전 아키텍처: 예산+가드레일을 런타임 미들웨어로 묶기

가장 유지보수하기 좋은 방식은 “에이전트 코어”와 “가드레일 미들웨어”를 분리하는 것입니다.

  • 에이전트 코어: 계획 수립, 도구 선택, 결과 종합
  • 미들웨어: 예산 체크, 반복 감지, 입력 검증, 로깅
class AgentRuntime:
    def __init__(self, agent, budget_guard, quota, repeat_guard):
        self.agent = agent
        self.budget_guard = budget_guard
        self.quota = quota
        self.repeat_guard = repeat_guard

    def call_tool(self, tool_name: str, tool_input: dict):
        self.budget_guard.tick()
        self.quota.consume(tool_name)
        sig = action_signature(tool_name, tool_input)
        self.repeat_guard.check(sig)
        # 여기서 validate_url, schema 검증 등을 추가
        return self.agent.tools[tool_name](**tool_input)

이 구조의 장점은 다음과 같습니다.

  • 에이전트 프레임워크를 바꿔도 정책은 그대로 재사용
  • 테스트가 쉬움(도구 호출만 모킹하면 됨)
  • 정책 위반 시 종료 이유가 명확

7) 프롬프트 레벨 가드레일: “생각”을 줄이기보다 “출력 계약”을 강화

루프 폭주는 종종 모델이 과도한 내부 추론을 하면서 도구 호출을 정당화하려고 할 때 심해집니다. 하지만 프롬프트로 “생각을 줄여라”만 요구하면 품질이 떨어질 수 있습니다. 대신 출력 계약을 강화하세요.

  • 다음 액션은 반드시 하나만 선택
  • 액션 전 “왜 이 도구가 필요한지”를 한 문장으로 제한
  • 종료 조건 충족 여부를 체크리스트로 보고

추론 노출을 통제해야 하는 환경이라면, 사고 과정 노출을 줄이는 패턴도 필요합니다. 관련해서 Chain-of-Thought 누출 막는 프롬프트 패턴 7가지도 같이 읽어보면 도움이 됩니다.

8) 체크리스트: 운영에서 바로 쓰는 폭주 방지 설정

아래는 “도구 루프 폭주”를 실제로 크게 줄여주는 최소 체크리스트입니다.

  • 스텝 예산: 기본 15~30, 작업 성격별 프로파일 분리
  • 시간 예산: 기본 30~120초, 도구 지연 고려
  • 토큰 예산: 세션 누적 기준 상한
  • 도구별 쿼터: search , browser , python 별도 제한
  • 반복 감지: 동일 액션 시그니처 2회 초과 시 중단
  • 실패 정책: 429 , timeout , 5xx 별도 처리 + 백오프
  • 종료 규칙: 체크리스트 충족 시 즉시 종료, 불확실하면 승인 대기
  • 도구 입력 검증: JSON 스키마 + 문자열 길이/결과 수 제한
  • 허용 목록: URL 도메인, 파일 경로, SQL 테이블 등
  • 로그/메트릭: 종료 이유와 비용을 반드시 기록

결론

AutoGPT 도구 루프 폭주는 “모델이 멍청해서”가 아니라, 시스템이 종료 조건과 비용 경계를 명시하지 않았기 때문에 발생합니다. 해결책은 프롬프트 튜닝만이 아니라, 런타임에서 예산과 가드레일을 강제하고, 반복과 실패를 정책으로 다루며, 관측 가능하게 만드는 것입니다.

정리하면, 예산은 최후의 브레이크이고, 가드레일은 차선을 유지하는 장치입니다. 둘을 함께 설계하면 에이전트는 더 싸고, 더 예측 가능하며, 운영 가능한 소프트웨어가 됩니다.