- Published on
AutoGPT 툴 호출 무한루프 끊는 Stop조건 설계
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서론
AutoGPT 스타일의 에이전트는 생각(계획) 과 행동(툴 호출) 을 반복하며 목표를 달성합니다. 문제는 목표가 모호하거나, 툴이 불완전하거나, 관측 가능한 진척 신호가 부족하면 에이전트가 같은 툴을 계속 호출하는 무한루프에 빠진다는 점입니다. 이때 단순히 max_steps=20 같은 하드컷만 넣으면 비용 폭주를 막을 수는 있어도, 성공률이 떨어지고 “왜 멈췄는지”를 설명하지 못해 운영이 어려워집니다.
이 글에서는 AutoGPT 툴 호출 루프를 “제어 가능한 시스템”으로 보고, Stop 조건을 다음 5개 축으로 설계하는 방법을 정리합니다.
- 예산 기반: 토큰, 비용, 시간, 툴 호출 횟수
- 진척 기반: 상태 변화, 정보 이득, 목표 거리 감소
- 반복 감지: 동일 툴·동일 인자·동일 결과의 패턴
- 품질/검증 기반: 성공 판정, 실패 판정, 재시도 정책
- 안전 기반: 위험 툴, 외부 사이드이펙트, 권한 상승 차단
재시도 폭주/데드라인 폭주와 유사한 운영 사고로 묶어 생각하면 설계가 쉬워집니다. 분산 시스템에서 재시도 스톰을 막는 접근은 에이전트에도 거의 그대로 적용됩니다. 참고로 재시도 폭주를 다룬 글은 gRPC MSA에서 데드라인·재시도 폭주 막는 법 이고, 모델/툴 과부하 시 재시도·큐잉 설계는 Claude API 529 과부하 대응 - 재시도·큐잉 설계 도 같이 보면 좋습니다.
1. 무한루프가 생기는 전형적인 원인
1) 관측 가능한 종료 신호가 없다
예: “웹 검색해서 답을 찾아” 같은 목표인데, 검색 결과를 요약해도 done 판정 기준이 없어서 계속 검색만 반복합니다.
2) 툴이 비결정적이거나 실패를 모호하게 반환한다
예: 브라우저 툴이 간헐적으로 타임아웃을 내는데, 에이전트는 이를 “조금만 더 하면 성공”으로 해석해 무한 재시도합니다.
3) 계획이 목표를 쪼개지 못한다
하나의 큰 목표를 작은 체크포인트로 분해하지 못하면 “진척”을 측정할 수 없고, 결국 동일 행동을 반복합니다.
4) 동일 행동 반복을 감지하는 메모리가 없다
에이전트가 직전 N회의 툴 호출과 결과를 구조적으로 기억하지 못하면, 같은 호출을 “새로운 시도”로 착각합니다.
2. Stop 조건은 단일 규칙이 아니라 “정책 레이어”다
운영에서 유효한 Stop 설계는 보통 다음 레이어로 구성됩니다.
- 하드 예산(안전장치): 시간/비용/스텝 상한
- 반복 감지(루프 차단): 동일 호출/동일 실패 패턴
- 진척 측정(목표 지향): 정보 이득, 상태 변화
- 품질 게이트(완료 판정): 검증 통과 시 종료
- 휴먼 핸드오프(복구): 모호한 실패는 사람에게 넘김
핵심은 “멈춰야 할 때 멈추되, 멈춘 이유가 로깅과 메트릭으로 설명 가능”해야 한다는 점입니다.
3. 예산 기반 Stop: 가장 먼저 넣어야 하는 최소 안전장치
다음 4가지는 거의 필수입니다.
max_steps: LLM 루프의 최대 반복 수max_tool_calls: 툴 호출 상한max_wall_time_ms: 전체 실행 시간 상한max_cost: 토큰 또는 비용 상한
예산은 실패를 줄이진 못하지만 폭주를 막습니다. 특히 툴이 외부 API를 때릴 때 비용 폭탄을 막는 마지막 방어선입니다.
from dataclasses import dataclass
import time
@dataclass
class Budget:
max_steps: int = 30
max_tool_calls: int = 20
max_wall_time_ms: int = 60_000
class BudgetGuard:
def __init__(self, budget: Budget):
self.budget = budget
self.start = time.time()
self.steps = 0
self.tool_calls = 0
def on_step(self):
self.steps += 1
if self.steps > self.budget.max_steps:
return False, "stop: max_steps exceeded"
if (time.time() - self.start) * 1000 > self.budget.max_wall_time_ms:
return False, "stop: max_wall_time exceeded"
return True, None
def on_tool_call(self):
self.tool_calls += 1
if self.tool_calls > self.budget.max_tool_calls:
return False, "stop: max_tool_calls exceeded"
return True, None
운영 팁:
max_steps를 너무 낮게 두면 “조금만 더 하면 되는” 케이스를 놓칩니다. 대신진척 기반 Stop을 같이 넣고max_steps는 넉넉히 두는 편이 실무에서 성공률이 좋습니다.
4. 반복 감지 Stop: “같은 짓”을 기계적으로 끊어라
무한루프의 대부분은 반복 패턴입니다.
- 동일 툴 이름 + 동일 인자
- 동일 툴 이름 + 거의 동일 인자(공백/정렬만 다름)
- 동일 실패 코드(예:
timeout)의 연속 - 동일 결과(요약 텍스트가 사실상 동일)
4.1 호출 서명(signature) 기반 차단
툴 호출을 정규화해서 해시로 만들고, 최근 N개에서 같은 서명이 K번 나오면 중단합니다.
import json
import hashlib
from collections import deque, Counter
def canonicalize(obj):
return json.dumps(obj, sort_keys=True, ensure_ascii=False)
def call_signature(tool_name: str, args: dict) -> str:
raw = tool_name + "|" + canonicalize(args)
return hashlib.sha256(raw.encode("utf-8")).hexdigest()
class LoopDetector:
def __init__(self, window=10, max_same=3):
self.window = window
self.max_same = max_same
self.sig_window = deque(maxlen=window)
def observe_call(self, tool_name, args):
sig = call_signature(tool_name, args)
self.sig_window.append(sig)
c = Counter(self.sig_window)
if c[sig] >= self.max_same:
return False, f"stop: repeated tool call signature x{c[sig]}"
return True, None
이 방식은 간단하지만 강력합니다. 다만 “반복이 필요한 작업”(예: 페이지네이션 크롤링)도 차단할 수 있으니 예외 규칙이 필요합니다.
4.2 실패 코드 기반 백오프 + 중단
실패가 연속될 때는 분산 시스템의 재시도 정책을 그대로 가져오면 됩니다.
- 동일 실패 유형 연속
n회면 중단 exponential backoff와jitter적용- 영구 실패(permanent)와 일시 실패(transient) 분리
import random
import time
TRANSIENT = {"timeout", "rate_limit", "overloaded"}
PERMANENT = {"invalid_auth", "not_found", "bad_request"}
class RetryPolicy:
def __init__(self, max_retries=4, base_delay=0.5):
self.max_retries = max_retries
self.base_delay = base_delay
self.fail_count = 0
def on_failure(self, error_code: str):
if error_code in PERMANENT:
return False, f"stop: permanent error {error_code}"
self.fail_count += 1
if self.fail_count > self.max_retries:
return False, f"stop: retries exceeded for {error_code}"
if error_code in TRANSIENT:
delay = self.base_delay * (2 ** (self.fail_count - 1))
delay = delay * (0.5 + random.random()) # jitter
time.sleep(delay)
return True, f"retrying after {delay:.2f}s"
# 알 수 없는 오류는 보수적으로 중단하거나 휴먼 핸드오프
return False, f"stop: unknown error {error_code}"
5. 진척 기반 Stop: “정보 이득”이 없으면 멈춰라
예산/반복 감지는 안전장치이고, 진척 기반은 성공률을 올리는 핵심입니다. 관점은 단순합니다.
- 지금 행동이 목표에 가까워지고 있는가
- 새로 얻은 정보가 있는가
- 상태가 변했는가
5.1 체크포인트(서브골)와 상태 머신
목표를 다음과 같은 상태로 쪼갭니다.
DISCOVER: 필요한 정보 수집PLAN: 실행 계획 확정EXECUTE: 툴 실행VERIFY: 결과 검증DONE또는FAIL
각 상태에서 “다음 상태로 넘어갈 조건”이 충족되지 않으면, 같은 상태에서 반복하지 말고 중단 또는 전략 변경을 합니다.
from enum import Enum
class Phase(str, Enum):
DISCOVER = "discover"
PLAN = "plan"
EXECUTE = "execute"
VERIFY = "verify"
DONE = "done"
FAIL = "fail"
class ProgressTracker:
def __init__(self):
self.phase = Phase.DISCOVER
self.no_progress_steps = 0
self.max_no_progress = 5
self.last_artifact_hash = None
def artifact_hash(self, text: str) -> str:
import hashlib
return hashlib.md5(text.strip().encode("utf-8")).hexdigest()
def observe_artifact(self, artifact_text: str):
h = self.artifact_hash(artifact_text)
if h == self.last_artifact_hash:
self.no_progress_steps += 1
else:
self.no_progress_steps = 0
self.last_artifact_hash = h
if self.no_progress_steps >= self.max_no_progress:
return False, "stop: no progress (artifact unchanged)"
return True, None
여기서 artifact 는 “에이전트가 축적하는 작업 산출물”입니다. 예를 들면 다음과 같습니다.
- 수집한 링크 목록
- 추출한 핵심 bullet
- 생성한 코드 패치
- 테스트 결과 요약
산출물이 변하지 않는데 툴 호출만 반복되면 루프일 가능성이 큽니다.
5.2 목표 거리(distance) 추정
정교하게 하려면 “목표 만족도”를 점수화할 수 있습니다.
- 요구사항 체크리스트 충족률
- 테스트 케이스 통과율
- 스키마 검증 통과 여부
테스트/검증이 가능한 작업(코드 수정, 데이터 변환)은 특히 효과가 좋습니다.
6. 품질/검증 기반 Stop: 완료 판정이 없으면 끝나지 않는다
에이전트가 멈추지 않는 가장 흔한 이유는 “완료의 정의”가 없기 때문입니다. 따라서 Done 조건을 먼저 설계해야 합니다.
6.1 Done을 기계적으로 판정하는 방법
- JSON 스키마 검증
- 유닛 테스트/통합 테스트 통과
- 정적 분석 통과
- 특정 필드 존재 여부(예:
answer,sources)
LLM 출력이 JSON으로 깨지는 문제를 경험했다면, 강제 출력과 검증을 결합해야 합니다. 관련해서는 OpenAI API JSON 깨짐, LangChain 강제출력으로 해결 도 참고할 만합니다.
import json
REQUIRED_KEYS = {"final_answer", "confidence", "citations"}
def validate_final(output_text: str):
try:
obj = json.loads(output_text)
except Exception as e:
return False, f"invalid_json: {e}"
missing = REQUIRED_KEYS - set(obj.keys())
if missing:
return False, f"missing_keys: {sorted(list(missing))}"
if not isinstance(obj["citations"], list):
return False, "citations_must_be_list"
return True, None
검증이 실패하면 “다시 생성”이 아니라 “어떤 키가 없으니 채워라”처럼 수정 지시를 해야 루프가 줄어듭니다.
6.2 Fail-fast 조건도 명시하라
Done만 있으면 에이전트는 실패를 인정하지 못하고 계속 시도합니다. 아래 중 하나라도 만족하면 FAIL 로 종료하는 정책이 필요합니다.
- 필수 리소스에 접근 불가(권한/네트워크)
- 입력 데이터 자체가 없음
- 영구 실패 코드
- 핵심 가정이 깨짐
7. 안전 기반 Stop: 사이드이펙트 툴은 더 엄격해야 한다
AutoGPT에서 위험한 루프는 비용보다 “외부 시스템에 반복 쓰기”입니다.
- 이메일 발송, 결제, DB 업데이트, 이슈 생성
- 인프라 변경, 배포 트리거
이런 툴은 기본적으로 다음 정책을 권합니다.
- 동일 목적의 쓰기 작업은
idempotency_key필수 dry_run모드 제공- 휴먼 승인 게이트(특정 단계에서만 실행)
import uuid
def make_idempotency_key(task_id: str, tool_name: str, args: dict) -> str:
# 같은 task_id와 같은 args면 같은 키가 나오도록 설계
import hashlib, json
raw = task_id + "|" + tool_name + "|" + json.dumps(args, sort_keys=True)
return hashlib.sha256(raw.encode("utf-8")).hexdigest()
# 예: 결제 툴 호출 시
# headers = {"Idempotency-Key": make_idempotency_key(task_id, "charge", args)}
이 설계가 없으면, 루프 한 번에 동일 결제가 여러 번 발생할 수 있습니다.
8. Stop 조건을 “오케스트레이터”로 합성하기
실무에서는 Stop 로직이 여기저기 흩어지면 디버깅이 불가능해집니다. 한 곳에서 합성하고, 항상 동일 포맷으로 로그를 남기세요.
class StopController:
def __init__(self, budget_guard, loop_detector, progress_tracker, retry_policy):
self.budget = budget_guard
self.loop = loop_detector
self.progress = progress_tracker
self.retry = retry_policy
def before_step(self):
return self.budget.on_step()
def before_tool(self, tool_name, args):
ok, reason = self.budget.on_tool_call()
if not ok:
return ok, reason
return self.loop.observe_call(tool_name, args)
def after_tool_result(self, artifact_text: str):
return self.progress.observe_artifact(artifact_text)
def on_tool_error(self, error_code: str):
return self.retry.on_failure(error_code)
권장 로그 필드(최소):
task_idstepphasetool_nameargs_hashstop_reasonbudget_remaining
Stop 이유가 쌓이면, 그 자체가 운영 개선 백로그가 됩니다. 예를 들어 stop: no progress 가 많으면 “진척 신호(artifact) 정의가 빈약”하다는 뜻이고, stop: repeated signature 가 많으면 “계획 분해 실패” 또는 “툴 결과 해석 실패” 가능성이 큽니다.
9. 실전 체크리스트
- Done 조건을 먼저 정의했는가(검증 가능해야 함)
- Fail-fast 조건이 있는가(영구 실패 분리)
- 예산 상한이 있는가(시간/스텝/툴 호출)
- 반복 감지(서명/실패 패턴)가 있는가
- 진척 신호(artifact)와 무진척 중단이 있는가
- 위험 툴에
idempotency_key와 승인 게이트가 있는가 - Stop 사유가 구조화 로그로 남는가
결론
AutoGPT 툴 호출 무한루프는 “모델이 멍청해서”가 아니라, 시스템이 종료 조건을 충분히 제공하지 못해서 발생하는 경우가 많습니다. 따라서 Stop 조건은 단일 파라미터가 아니라, 예산·반복·진척·검증·안전의 다층 정책으로 설계해야 합니다.
가장 효과적인 조합은 다음 순서로 적용하는 것입니다.
- 예산 하드컷으로 폭주 방지
- 반복 감지로 즉시 루프 차단
- 진척 기반으로 성공률 유지
- 검증 기반 Done/Fail로 종료를 명확화
- 위험 툴은 승인과 멱등성으로 별도 통제
이렇게 설계하면 “왜 멈췄는지 설명 가능한 에이전트”가 되고, 운영에서 Stop 이벤트를 기반으로 프롬프트·툴·플래너를 지속적으로 개선할 수 있습니다.