Published on

AutoGPT 무한루프·비용폭탄 막는 종료조건 7가지

Authors

서버에서 AutoGPT류 에이전트를 돌리다 보면 가장 무서운 장애는 예외가 아니라 “정상적으로 계속 돈을 태우는 상태”입니다. 모델이 스스로 계획을 세우고 도구를 호출하는 구조는 생산성을 높이지만, 종료조건이 빈약하면 실패를 인지하지 못한 채 같은 행동을 반복하거나, 목표를 조금씩 바꾸며 끝없이 탐색합니다.

이 글은 “에이전트가 언제 멈춰야 하는가”를 코드 레벨에서 강제하는 7가지 종료조건을 정리합니다. 핵심은 단일 조건이 아니라, 서로 다른 실패 모드를 커버하는 다층 방어선을 만드는 것입니다.

왜 AutoGPT는 무한루프에 빠질까

에이전트 루프의 전형적인 구조는 생각행동(툴 호출)관찰다음 계획의 반복입니다. 이때 무한루프는 보통 아래 중 하나로 발생합니다.

  • 목표의 성공 판정이 모호: “보고서 작성”처럼 완료 기준이 불명확하면 계속 다듬습니다.
  • 도구 실패를 학습하지 못함: 같은 파라미터로 같은 API를 반복 호출합니다.
  • 탐색 공간이 너무 큼: 웹 검색, 코드 생성, 리팩터링 등에서 끝이 없습니다.
  • 자기 수정 루프: “방금 답이 부족하니 다시 개선”을 무한 반복합니다.
  • 비용 모델 부재: 토큰, 외부 API 비용, 실행 시간에 상한이 없습니다.

따라서 종료조건은 “정답을 찾았을 때 멈추기”만으로는 부족하고, “정답을 못 찾는 상태를 감지해 멈추기”가 더 중요합니다.

종료조건 7가지 (운영에서 바로 쓰는 체크리스트)

아래 7가지는 서로 대체재가 아니라 보완재입니다. 최소한 1, 2, 3은 기본 안전장치로 넣고, 외부 API나 브라우징이 있으면 4, 5를 강하게 권장합니다.

1) 최대 스텝 수 제한 (Hard Cap)

가장 단순하지만 가장 강력한 안전장치입니다. 에이전트가 몇 번의 행동을 수행하면 무조건 종료합니다.

  • 장점: 구현이 쉽고 확실합니다.
  • 단점: 목표가 큰 작업이면 너무 이르게 끊길 수 있습니다.
MAX_STEPS = 30

for step in range(MAX_STEPS):
    action = agent.plan(observation)
    observation = tools.run(action)
else:
    raise RuntimeError("Terminated: max steps reached")

운영 팁: MAX_STEPS는 고정값보다 “작업 유형별 프로파일”로 두는 게 좋습니다. 예를 들어 요약 작업은 10, 리서치는 40처럼요.

2) 시간 제한 (Wall Clock Timeout)

스텝 수는 낮아도, 각 스텝이 웹 크롤링이나 대형 파일 처리면 시간이 길어질 수 있습니다. 따라서 전체 실행 시간을 제한해야 합니다.

import time

TIMEOUT_SEC = 180
start = time.time()

for step in range(10_000):
    if time.time() - start > TIMEOUT_SEC:
        raise TimeoutError("Terminated: wall clock timeout")
    action = agent.plan(observation)
    observation = tools.run(action)

운영 팁: 타임아웃은 “대기 시간”도 포함해야 합니다. 특히 브라우저 자동화나 네트워크 호출은 지연이 누적됩니다.

3) 토큰 예산 제한 (Token Budget)

비용폭탄의 1차 원인은 토큰입니다. prompt + completion 토큰을 누적해 예산을 넘으면 종료합니다.

  • 모델 SDK에서 토큰 사용량을 반환하면 그대로 누적합니다.
  • 반환이 없으면 대략적인 토큰 추정기를 붙여도 됩니다.
TOKEN_BUDGET = 120_000
used_tokens = 0

while True:
    resp = llm.chat(messages)
    used_tokens += resp.usage.total_tokens

    if used_tokens >= TOKEN_BUDGET:
        raise RuntimeError("Terminated: token budget exceeded")

    messages.append({"role": "assistant", "content": resp.content})

운영 팁: 토큰 예산은 “세션당”뿐 아니라 “사용자당 일일 예산”도 함께 두면 좋습니다. 과금형 SaaS라면 필수입니다.

4) 외부 API 호출 예산 제한 (Tool Call Budget)

검색 API, 지도 API, 결제 API, 크롤링 프록시 등은 토큰보다 더 빠르게 비용이 커질 수 있습니다. 도구별 호출 횟수와 비용 상한을 둡니다.

TOOL_CALL_LIMITS = {
    "web_search": 10,
    "browser_fetch": 30,
    "paid_api": 3,
}

tool_calls = {k: 0 for k in TOOL_CALL_LIMITS}

def run_tool(name, **kwargs):
    tool_calls[name] += 1
    if tool_calls[name] > TOOL_CALL_LIMITS[name]:
        raise RuntimeError(f"Terminated: tool call limit exceeded for `{name}`")
    return tools[name](**kwargs)

운영 팁: 도구 호출 예산은 “전체”뿐 아니라 “연속 실패 횟수”와 결합해야 효과가 큽니다. 아래 6) 실패 누적 종료와 함께 쓰세요.

5) 목표 달성 판정(Acceptance Criteria) 기반 종료

무한루프의 근본 원인은 완료 기준이 모호한 것입니다. 사람이 읽기 쉬운 자연어 목표만 두지 말고, 검증 가능한 체크리스트를 목표에 포함시키거나, 별도의 검증기(validator)를 둡니다.

예시:

  • 산출물이 JSON이라면 schema validation 통과 시 종료
  • 코드 생성이라면 tests pass 시 종료
  • 문서 생성이라면 필수 섹션 포함 여부 검사 후 종료
REQUIRED_SECTIONS = ["개요", "설치", "예제", "제한사항"]

def is_done(markdown: str) -> bool:
    return all(section in markdown for section in REQUIRED_SECTIONS)

for step in range(50):
    draft = agent.write()
    if is_done(draft):
        break
else:
    raise RuntimeError("Terminated: acceptance criteria not met")

운영 팁: “LLM이 스스로 완료 판정”하게 하면 자의적이 됩니다. 가능한 한 규칙 기반 또는 테스트 기반으로 만드세요.

6) 실패 누적 종료 (Consecutive Failure / Error Budget)

도구 호출이 계속 실패하는데도 같은 행동을 반복하면 비용이 폭발합니다. 연속 실패 횟수, 동일 에러 반복, 동일 입력 반복을 감지해 종료합니다.

MAX_CONSECUTIVE_FAILS = 5
consecutive_fails = 0
last_errors = []

for step in range(100):
    try:
        action = agent.plan(observation)
        observation = tools.run(action)
        consecutive_fails = 0
    except Exception as e:
        consecutive_fails += 1
        last_errors.append(str(e))
        last_errors = last_errors[-3:]

        if consecutive_fails >= MAX_CONSECUTIVE_FAILS:
            raise RuntimeError("Terminated: too many consecutive failures")

        # 동일 에러가 반복되면 더 빨리 중단
        if len(set(last_errors)) == 1 and len(last_errors) == 3:
            raise RuntimeError("Terminated: repeated identical error")

운영 팁: 네트워크나 플랫폼 문제로 실패가 반복될 수 있습니다. 이때는 “즉시 종료” 대신 “백오프 후 종료”가 더 안전합니다. 인프라 관점의 장애 패턴은 KServe+Istio 503와 콜드스타트 지연 해결 가이드처럼 콜드스타트나 게이트웨이 이슈가 원인이 될 수도 있습니다.

7) 진행 정체(Progress Stagnation) 감지 종료

가장 실전적인 종료조건입니다. 에이전트가 뭔가를 하고는 있지만 “진전이 없는 상태”를 수치화해 멈춥니다.

진행 정체의 신호 예시:

  • 같은 쿼리로 검색을 반복
  • 산출물의 길이만 늘고 핵심 요구사항 충족률이 안 오름
  • 요약/리라이트를 반복하지만 품질 점수가 개선되지 않음

간단한 방법은 “상태 해시”를 저장하고 같은 상태가 반복되면 종료하는 것입니다.

import hashlib

def fingerprint(text: str) -> str:
    return hashlib.sha256(text.strip().encode("utf-8")).hexdigest()

seen = {}
MAX_REPEAT = 3

for step in range(80):
    action = agent.plan(observation)
    observation = tools.run(action)

    fp = fingerprint(str(observation))
    seen[fp] = seen.get(fp, 0) + 1
    if seen[fp] >= MAX_REPEAT:
        raise RuntimeError("Terminated: progress stagnation detected")

운영 팁: 관찰(observation) 전체를 해시하면 노이즈(타임스탬프 등) 때문에 반복 감지가 안 될 수 있습니다. “의미 있는 요약 상태”만 추출해 지문을 만들면 훨씬 잘 동작합니다.

7가지 종료조건을 한 번에 묶는 실전 템플릿

아래는 “스텝, 시간, 토큰, 도구 호출, 실패, 정체, 완료 판정”을 한 루프에 묶은 예시입니다. 실제 서비스에서는 여기에 사용자별 레이트 리밋과 감사 로그를 추가하세요.

import time
import hashlib

MAX_STEPS = 40
TIMEOUT_SEC = 240
TOKEN_BUDGET = 80_000
MAX_CONSEC_FAILS = 4
MAX_REPEAT_STATE = 3

TOOL_CALL_LIMITS = {"web_search": 8, "browser_fetch": 20}

def fp_state(x: str) -> str:
    return hashlib.sha256(x.strip().encode("utf-8")).hexdigest()

start = time.time()
used_tokens = 0
consec_fails = 0
state_seen = {}

for step in range(MAX_STEPS):
    if time.time() - start > TIMEOUT_SEC:
        raise TimeoutError("Terminated: timeout")

    try:
        resp = llm.chat(messages)
        used_tokens += resp.usage.total_tokens
        if used_tokens > TOKEN_BUDGET:
            raise RuntimeError("Terminated: token budget")

        action = parse_action(resp.content)

        # tool budget
        if action.tool_name in TOOL_CALL_LIMITS:
            action.count = action.count if hasattr(action, "count") else 1
            TOOL_CALL_LIMITS[action.tool_name] -= action.count
            if TOOL_CALL_LIMITS[action.tool_name] < 0:
                raise RuntimeError("Terminated: tool call budget")

        observation = tools.run(action)
        consec_fails = 0

        # stagnation
        fp = fp_state(summarize_observation(observation))
        state_seen[fp] = state_seen.get(fp, 0) + 1
        if state_seen[fp] >= MAX_REPEAT_STATE:
            raise RuntimeError("Terminated: stagnation")

        # acceptance
        if is_done(observation):
            break

    except Exception as e:
        consec_fails += 1
        if consec_fails >= MAX_CONSEC_FAILS:
            raise RuntimeError("Terminated: consecutive failures") from e
else:
    raise RuntimeError("Terminated: max steps")

운영에서 자주 놓치는 포인트 4가지

로그는 “종료 이유”가 남아야 한다

종료조건을 넣어도 운영자가 원인을 모르면 다시 비용이 샙니다. 최소한 아래는 구조화 로그로 남기세요.

  • 종료 타입: timeout, token_budget, stagnation
  • 누적 토큰, 누적 도구 호출 수
  • 마지막 3개 액션
  • 마지막 에러 메시지

종료는 실패가 아니라 “정상 제어”다

에이전트는 불확실한 탐색을 합니다. 종료조건은 실패 처리가 아니라 안전장치이며, 사용자에게는 “추가 지시가 필요” 같은 형태로 반환하는 것이 UX에 좋습니다.

프런트엔드에서도 폭주를 막아라

서버에서만 막으면 늦습니다. 사용자가 버튼을 연타하거나 스트리밍 연결이 끊겼는데 백엔드가 계속 돌 수도 있습니다. 특히 RSC나 스트리밍 UI에서는 상태 불일치가 비용으로 이어질 수 있으니, 세션 종료와 취소 신호를 명확히 하세요. 관련 이슈는 Next.js 14 RSC에서 hydration mismatch 해결법도 함께 참고할 만합니다.

GPU 로컬 LLM도 “비용”이 있다

클라우드 과금이 아니어도, 로컬 LLM은 GPU 점유와 전력, 다른 워크로드 지연이라는 비용이 있습니다. “토큰 예산”을 “시간 예산”과 함께 두는 이유가 여기에 있습니다. 로컬 환경에서 GPU 미사용으로 성능이 늘어지는 경우는 Ollama 로컬 LLM이 GPU를 안 쓰는 이유 9가지처럼 환경 점검이 필요합니다.

마무리: 종료조건은 1개가 아니라 7겹 안전벨트

AutoGPT 무한루프와 비용폭탄은 대개 “종료조건이 약해서”가 아니라 “종료조건이 단일해서” 터집니다. 스텝과 시간 같은 하드 리밋으로 최악을 막고, 토큰과 도구 예산으로 비용을 통제하며, 실패 누적과 진행 정체 감지로 루프의 형태를 끊고, 마지막으로 검증 가능한 완료 판정으로 깔끔하게 종료시키는 구성이 가장 안정적입니다.

다음 단계로는 종료조건을 단순히 raise로 끝내지 말고, 종료 사유별로 “사용자에게 요구할 추가 정보”를 템플릿화하면 에이전트가 멈춘 뒤에도 작업을 빠르게 재개할 수 있습니다.