- Published on
AutoGPT 에이전트 무한재귀·비용폭주 차단법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버에 AutoGPT 스타일 에이전트를 붙이면, 데모에서는 똑똑해 보이는데 운영에서는 갑자기 비용이 튀거나 작업이 끝나지 않는 일이 흔합니다. 원인은 대부분 단순합니다. 에이전트가 목표 달성의 종료 조건을 명확히 갖지 못한 채, 생각-행동-관찰 루프를 계속 돌면서 LLM 호출과 툴 호출을 누적하기 때문입니다.
이 글에서는 무한재귀(무한 루프)와 비용 폭주를 “아키텍처 레벨”에서 차단하는 방법을 다룹니다. 핵심은 에이전트를 더 똑똑하게 만드는 것이 아니라, 더 안전하게 멈추게 만드는 것입니다.
왜 AutoGPT는 무한재귀·비용폭주가 발생할까
AutoGPT 계열은 대체로 다음 흐름을 가집니다.
- 사용자 목표 입력
- 계획 수립
- 툴 호출 또는 LLM 호출로 작업 수행
- 결과 관찰
- 부족하면 2~4 반복
문제는 5번 반복의 종료 조건이 느슨할 때 발생합니다.
1) 종료 조건이 자연어에만 의존
예를 들어 “완료되면 DONE을 출력해” 같은 프롬프트 지시만 있으면, 모델이 애매한 상황에서 계속 “조금 더 확인해보자”로 기울기 쉽습니다.
2) 툴 실패가 재시도를 무한히 유도
네트워크 오류, 429 레이트리밋, 외부 API의 일시 장애가 발생하면 에이전트는 이를 “목표 미달성”으로 해석하고 재시도를 반복합니다. 특히 스트리밍이나 동시 실행이 섞이면 thundering herd 형태로 악화됩니다.
레이트리밋 설계 관점은 이 글도 함께 보면 좋습니다.
3) 관찰 결과가 다시 프롬프트를 오염
에이전트가 웹 페이지, 로그, 에러 메시지를 그대로 컨텍스트에 넣으면 토큰이 커지고, 모델이 핵심 신호를 놓치면서 “다시 조사”를 반복할 가능성이 커집니다. 토큰 증가 자체가 비용 폭주로 직결됩니다.
4) 플래너와 실행기가 같은 모델로 섞여 있음
계획 수립과 실행을 같은 모델이 계속 수행하면, 실패 시 “계획을 바꿔야 한다”보다 “한 번 더 해보자”로 수렴하기 쉽습니다. 즉, 루프 탈출이 설계적으로 어렵습니다.
차단 전략 1: 스텝 한도·시간 한도는 기본, 하지만 충분조건이 아니다
가장 먼저 해야 할 것은 강제 종료 장치입니다.
max_steps: 에이전트 루프 반복 횟수 상한max_wall_time_ms: 전체 실행 시간 상한max_tool_calls: 툴 호출 총량 상한
하지만 이것만으로는 운영 품질이 떨어집니다. 단지 “언젠가 죽는다”일 뿐, 언제, 어떤 상태로, 어떤 메시지로 종료할지가 정의되지 않으면 사용자 경험과 재시도 정책이 꼬입니다.
따라서 강제 종료는 아래 두 가지와 세트로 설계해야 합니다.
- 부분 결과 반환: 지금까지 얻은 산출물과 실패 원인을 구조화해 반환
- 재개 토큰: 동일 작업을 이어서 할 수 있도록 상태 스냅샷 제공
차단 전략 2: 실행 예산(Budget) 기반으로 비용을 하드 캡
운영에서 가장 효과적인 방법은 “돈으로 끊는 것”입니다. 토큰 수, 툴 호출 수, 외부 API 비용을 모두 비용 모델로 환산해 예산을 깎아 나갑니다.
예산 모델 예시
- LLM 입력 토큰 비용
- LLM 출력 토큰 비용
- 검색 API 호출 비용
- 브라우저 자동화 비용(시간 기반)
예산이 0이 되면 즉시 중단하고, 다음을 반환합니다.
- 현재까지의 최선 답
- 다음에 해야 할 작업 리스트
- 왜 중단되었는지(예산 소진)
TypeScript 예시: 토큰·툴 비용 예산 집행
아래 코드는 budget을 중앙에서 집행하고, 모든 호출이 charge를 거치도록 강제합니다.
type BudgetItem = {
name: string;
cost: number;
meta?: Record<string, unknown>;
};
class BudgetExceededError extends Error {
constructor(public remaining: number, public attempted: number) {
super("Budget exceeded");
}
}
class Budget {
private spent = 0;
constructor(private limit: number) {}
get remaining() {
return Math.max(0, this.limit - this.spent);
}
charge(item: BudgetItem) {
if (this.spent + item.cost > this.limit) {
throw new BudgetExceededError(this.remaining, item.cost);
}
this.spent += item.cost;
}
snapshot() {
return { limit: this.limit, spent: this.spent, remaining: this.remaining };
}
}
// 사용 예: LLM 호출 전 예산 차감
async function callLLM(budget: Budget, inputTokens: number, outputTokensCap: number) {
const cost = inputTokens * 0.000001 + outputTokensCap * 0.000002;
budget.charge({ name: "llm_call", cost, meta: { inputTokens, outputTokensCap } });
// 실제 LLM 호출은 생략
return { text: "...", usedOutputTokens: Math.min(300, outputTokensCap) };
}
포인트는 “추정치로 선차감”하는 것입니다. 후차감은 이미 비용이 발생한 뒤라 방어가 늦습니다. 출력 토큰은 상한(max_output_tokens)을 두고 그 상한 기준으로 선차감하면 안전합니다.
차단 전략 3: 종료 조건을 구조화된 스키마로 강제
자연어로 DONE을 말하라고 하는 대신, 매 스텝마다 종료 판정 JSON을 반드시 출력하게 만들고, 파서가 이를 검증합니다.
status:continue또는finalconfidence:0~1missing_info: 남은 요구사항next_action: 다음 툴 호출 계획
프롬프트 규칙(요지)
- 결과는 반드시 JSON 하나
- JSON 외 텍스트 금지
status=final이면final_answer필수
Python 예시: 종료 판정 스키마 검증
import json
from dataclasses import dataclass
@dataclass
class Decision:
status: str
confidence: float
next_action: str | None = None
final_answer: str | None = None
def parse_decision(raw: str) -> Decision:
obj = json.loads(raw)
status = obj.get("status")
if status not in ["continue", "final"]:
raise ValueError("invalid status")
conf = float(obj.get("confidence", 0))
if not (0 <= conf <= 1):
raise ValueError("invalid confidence")
if status == "final" and not obj.get("final_answer"):
raise ValueError("final requires final_answer")
return Decision(
status=status,
confidence=conf,
next_action=obj.get("next_action"),
final_answer=obj.get("final_answer"),
)
이렇게 하면 “모델이 계속 말이 길어지며 루프를 연장”하는 패턴을 구조적으로 차단할 수 있습니다.
차단 전략 4: 재시도는 ‘정책’이 아니라 ‘상태 머신’으로 다뤄라
무한 루프의 70%는 재시도 설계 부실에서 나옵니다. 다음 원칙을 권장합니다.
- 동일 오류 코드에 대한 재시도 횟수 상한
- 지수 백오프 + 지터
429와5xx를 다르게 취급- “재시도 중” 자체도 예산을 소비
- 재시도 후에도 실패하면 대체 경로로 전환(모델 변경, 툴 변경, 캐시 사용)
Node.js 예시: 지수 백오프 + 지터 + 상한
function sleep(ms) {
return new Promise((r) => setTimeout(r, ms));
}
async function retryWithBackoff(fn, opts) {
const {
maxAttempts = 5,
baseDelayMs = 300,
maxDelayMs = 5000,
retryable = (e) => [429, 500, 502, 503, 504].includes(e.status),
} = opts || {};
let lastErr;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn(attempt);
} catch (e) {
lastErr = e;
if (!retryable(e) || attempt === maxAttempts) break;
const exp = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1));
const jitter = Math.floor(Math.random() * 200);
await sleep(exp + jitter);
}
}
throw lastErr;
}
여기에 더해, 에이전트 루프는 재시도 함수를 직접 호출하지 말고 “현재 상태”를 업데이트한 뒤 다음 스텝에서 재시도를 선택하도록 만드는 편이 안전합니다. 그래야 스텝 한도, 예산, 서킷브레이커가 일관되게 적용됩니다.
차단 전략 5: 서킷브레이커로 외부 의존성 장애를 격리
외부 검색 API나 브라우저 자동화가 장애일 때, 에이전트는 계속 두드리며 비용을 태웁니다. 이때는 서킷브레이커가 유효합니다.
- 실패율이 임계치를 넘으면
open - 일정 시간 동안 호출 차단
half-open에서 소량만 시험 호출
서킷브레이커가 open이면 에이전트는 즉시 전략을 바꿉니다.
- 캐시된 결과 사용
- 더 싼 모델로 요약 기반 답변 제공
- 사용자에게 “외부 의존성 장애로 제한된 답변”을 명시
이 패턴은 분산 시스템에서 데드라인과 리트라이 폭주를 막는 방식과 유사합니다.
차단 전략 6: 컨텍스트 오염 방지(요약·샘플링·로그 분리)
무한 루프는 종종 “관찰 데이터가 너무 많아서 모델이 결정을 못 내림”에서 시작합니다. 다음을 적용합니다.
- 원문 로그는 저장하되, 프롬프트에는 요약만 포함
- HTML, JSON 원문을 그대로 넣지 말고 핵심 필드만 추출
- 관찰 데이터는
N개까지만 유지하고 오래된 것은 요약으로 압축
요약 정책 예시
- 최근 3개 관찰: 원문 유지
- 그 이전: 1개 요약 블록으로 압축
- 전체 토큰이 임계치를 넘으면: “근거 링크/식별자”만 남기고 본문 제거
이렇게 하면 모델이 “다시 읽고 다시 확인”하는 루프를 줄일 수 있고, 토큰 비용도 안정화됩니다.
차단 전략 7: 플래너-실행기 분리 + 검증기(critic)로 루프를 끊어라
가장 재현성 높은 패턴은 3역할 분리입니다.
- Planner: 계획 수립, 스텝 예산 배분
- Executor: 툴 호출 수행
- Critic/Verifier: 종료 조건 충족 여부 판정
특히 Verifier는 “정답 생성”이 아니라 “요구사항 충족 검사”만 하게 해야 합니다. 이때 Self-Consistency 같은 접근을 쓰면, 긴 CoT를 강요하지 않고도 판정 안정성을 올릴 수 있습니다.
간단한 검증 체크리스트 예시
- 산출물에 필수 섹션이 모두 있는가
- 사용자 요구 형식(예: JSON, 표, 코드 블록)을 지켰는가
- 외부 데이터가 필요한데 없는 상태로 단정하지 않았는가
검증기가 fail을 내리면, Executor가 아니라 Planner로 되돌려 “계획 수정”을 유도합니다. 이 흐름이 없으면 실패가 곧바로 “재시도”로 이어져 루프가 길어집니다.
차단 전략 8: 관측 가능성(Observability) 없이는 폭주를 못 막는다
운영에서 중요한 것은 “폭주를 빨리 감지하고 자동으로 멈추는 것”입니다. 최소한 아래 메트릭을 찍어야 합니다.
- 세션별 LLM 호출 수
- 세션별 입력/출력 토큰
- 툴별 호출 수, 실패율,
p95지연 - 루프 스텝 수 분포
- 예산 초과 종료 비율
그리고 알람은 “비용”과 “루프” 중심으로 걸어야 합니다.
steps_per_session이 임계치 초과tool_error_rate가 급증tokens_per_session이 급증
쿠버네티스 환경이라면, 오토스케일링이 붙으면서 폭주가 더 증폭될 수 있습니다. 큐 기반으로 완충을 두는 방식이 도움이 됩니다.
실전 권장 조합: “3중 안전장치” 체크리스트
운영에서 많이 쓰는 조합을 요약하면 다음과 같습니다.
- 하드 리밋
max_steps,max_wall_time_ms,max_output_tokens
- 예산
- 세션 예산 + 사용자별 일일 예산 + 워크스페이스 예산
- 실패 제어
- 재시도 상한 + 백오프/지터 + 서킷브레이커
여기에 “구조화된 종료 판정”까지 넣으면, 무한재귀의 대부분은 설계 단계에서 제거됩니다.
마무리: 에이전트는 똑똑해서가 아니라, 안전하게 멈춰서 쓸 수 있다
AutoGPT 에이전트의 무한 루프와 비용 폭주는 모델의 문제가 아니라 제어면(control plane) 부재가 원인인 경우가 많습니다. 강제 종료, 예산 집행, 구조화된 종료 판정, 재시도 상태 머신, 서킷브레이커, 컨텍스트 압축, 검증기 분리를 적용하면 “끝나지 않는 에이전트”를 “예측 가능한 작업자”로 바꿀 수 있습니다.
다음 단계로는, 실제 트래픽에서 어떤 패턴으로 루프가 생기는지 세션 리플레이를 만들고, 실패 케이스를 테스트 벡터로 고정해 회귀 테스트에 넣는 것을 추천합니다. 그래야 비용과 품질이 함께 안정화됩니다.