Published on

AutoGPT 무한루프·비용폭주 막는 가드레일 7가지

Authors

AutoGPT 같은 자율 에이전트는 생각 -> 행동 -> 관찰 루프를 스스로 돌며 목표를 달성합니다. 문제는 이 루프가 실패 모드를 만나면, 사람의 눈치 없이도 계속 실행되며 비용이 기하급수적으로 늘어난다는 점입니다. 대표적으로 (1) 같은 도구를 반복 호출, (2) 검색 결과를 재검색, (3) 에러를 고치려다 더 큰 에러를 만들고 재시도, (4) 프롬프트가 길어져 컨텍스트가 비대해지는 패턴이 자주 발생합니다.

이 글에서는 “에이전트를 똑똑하게 만들기”보다 우선인, 무한루프·비용폭주를 구조적으로 막는 7가지 가드레일을 정리합니다. LangChain이나 OpenAI Agents 같은 프레임워크를 쓰든, 직접 루프를 구현하든 그대로 적용할 수 있는 형태로 설명합니다.

참고로 도구 호출 루프 차단을 더 깊게 보고 싶다면 LangChain Tool Calling 무한루프 차단 7단계도 함께 읽어두면 좋습니다.

1) 하드 리밋: 스텝 수·벽시계 시간·재시도 횟수 3종 제한

에이전트 루프를 안전하게 만드는 가장 확실한 방법은 실행 상한선을 강제하는 것입니다. 스텝 수만 제한하면 네트워크 지연으로 1스텝이 30초가 될 수 있고, 시간만 제한하면 짧은 시간에 수백 스텝을 돌릴 수 있습니다. 그래서 보통 3종 세트를 함께 둡니다.

  • max_steps: 한 목표에 허용되는 행동 수
  • max_wall_time_ms: 총 실행 시간
  • max_retries_per_tool: 도구별 재시도 상한
// TypeScript 예시: 에이전트 루프 하드 리밋

type ToolName = string;

interface Limits {
  maxSteps: number;
  maxWallTimeMs: number;
  maxRetriesPerTool: number;
}

interface State {
  step: number;
  startedAt: number;
  toolRetries: Record<ToolName, number>;
}

function assertLimits(state: State, limits: Limits) {
  if (state.step >= limits.maxSteps) throw new Error("GUARDRAIL: maxSteps exceeded");
  if (Date.now() - state.startedAt >= limits.maxWallTimeMs) {
    throw new Error("GUARDRAIL: maxWallTime exceeded");
  }
}

function canRetryTool(state: State, tool: ToolName, limits: Limits) {
  const n = state.toolRetries[tool] ?? 0;
  return n < limits.maxRetriesPerTool;
}

운영 팁:

  • max_steps는 “정상 성공 케이스의 p95”를 기준으로 잡고, 목표 난이도별로 프로파일을 나누는 게 좋습니다.
  • 실패 시 “중단 + 요약 + 다음 액션 추천”을 남기면 사용자 경험이 크게 좋아집니다.

2) 비용 리밋: 토큰·달러·도구 호출 예산을 런타임에서 차감

무한루프는 결국 비용 문제로 귀결됩니다. 그래서 “실행 리밋”과 별개로 예산 기반 차감 시스템을 두는 게 효과적입니다.

권장 예산 항목:

  • LLM 입력/출력 토큰 예산
  • 외부 API 호출 횟수 예산 (검색, 크롤링, DB, SaaS)
  • 고비용 도구 별도 예산 (예: 브라우저 자동화, 코드 실행, 이미지 생성)
# Python 예시: 간단한 예산 차감기

from dataclasses import dataclass

@dataclass
class Budget:
    max_input_tokens: int
    max_output_tokens: int
    max_tool_calls: int

    used_input_tokens: int = 0
    used_output_tokens: int = 0
    used_tool_calls: int = 0

    def charge_llm(self, in_tok: int, out_tok: int):
        self.used_input_tokens += in_tok
        self.used_output_tokens += out_tok
        if self.used_input_tokens > self.max_input_tokens:
            raise RuntimeError("GUARDRAIL: input token budget exceeded")
        if self.used_output_tokens > self.max_output_tokens:
            raise RuntimeError("GUARDRAIL: output token budget exceeded")

    def charge_tool(self, n: int = 1):
        self.used_tool_calls += n
        if self.used_tool_calls > self.max_tool_calls:
            raise RuntimeError("GUARDRAIL: tool call budget exceeded")

운영 팁:

  • 예산 초과 시 바로 종료하지 말고, “현재까지의 진행 요약”을 생성하게 한 뒤 종료하면 낭비가 줄어듭니다. 요약은 작은 모델로 돌려도 됩니다.
  • RAG를 붙인 경우, 평가 자동화로 비용을 줄이는 접근도 중요합니다. RAG 평가 자동화 - Ragas+Qdrant로 품질·비용 최적화처럼 “품질 대비 비용”을 측정해 예산 정책을 정교화할 수 있습니다.

3) 루프 감지: 동일 행동·동일 관찰·동일 계획의 반복을 차단

무한루프는 대개 “같은 행동을 반복”하거나 “관찰이 변하지 않는데도 재시도”하는 형태로 나타납니다. 따라서 에이전트의 각 스텝을 **서명(signature)**으로 만들고, 최근 k개 내 반복 여부를 확인합니다.

감지 포인트:

  • 같은 tool_name + 같은 인자
  • 관찰 결과가 동일 (또는 유사도 높음)
  • 계획 텍스트가 거의 동일
// TypeScript 예시: 최근 스텝 서명 기반 루프 감지

function stableStringify(obj: any) {
  return JSON.stringify(obj, Object.keys(obj).sort());
}

function stepSignature(tool: string, args: any, observation: string) {
  // observation은 너무 길면 앞부분만
  const obs = observation.slice(0, 300);
  return `${tool}::${stableStringify(args)}::${obs}`;
}

function detectLoop(signatures: string[], windowSize: number) {
  const w = signatures.slice(-windowSize);
  const uniq = new Set(w);
  // window 내 고유값이 너무 적으면 루프 가능성
  return uniq.size <= Math.ceil(windowSize / 3);
}

운영 팁:

  • “완전 동일”만 잡으면 놓치는 케이스가 많습니다. 검색 쿼리의 공백/순서 차이, URL 파라미터 차이 등을 정규화하세요.
  • 루프 감지 시 즉시 중단보다, 전략 전환 프롬프트를 한 번 주는 방식이 효과적입니다. 예: 도구를 더 호출하지 말고, 현재 정보로 가능한 최선의 답을 제시하라.

4) 도구 호출 게이트: 허용 리스트·스키마 검증·쿨다운

AutoGPT류는 도구를 잘못 쓰면 피해가 커집니다. 가드레일의 핵심은 “도구를 똑똑하게 쓰게 하는 것”이 아니라, 도구를 못 쓰게 막아야 할 때 막는 것입니다.

권장 정책:

  • 도구 allowlist (기본은 모두 금지)
  • 입력 스키마 검증 (필수 필드, 길이, 정규식)
  • 쿨다운: 같은 도구를 N초 이내 재호출 금지
  • 파라미터 상한: 예를 들어 검색 결과 top_k 최대 5
# Python 예시: 도구 호출 게이트

import time

ALLOWED_TOOLS = {"search", "fetch_url", "summarize"}
TOOL_COOLDOWN_SEC = {"search": 3, "fetch_url": 5}

last_called_at = {}

def guard_tool_call(tool_name: str, args: dict):
    if tool_name not in ALLOWED_TOOLS:
        raise RuntimeError("GUARDRAIL: tool not allowed")

    # 간단한 스키마/상한 검증
    if tool_name == "search":
        q = args.get("query", "")
        if not (1 <= len(q) <= 120):
            raise RuntimeError("GUARDRAIL: invalid query length")
        if args.get("top_k", 5) > 5:
            raise RuntimeError("GUARDRAIL: top_k too large")

    # 쿨다운
    now = time.time()
    cd = TOOL_COOLDOWN_SEC.get(tool_name)
    if cd is not None:
        prev = last_called_at.get(tool_name, 0)
        if now - prev < cd:
            raise RuntimeError("GUARDRAIL: tool cooldown")
        last_called_at[tool_name] = now

운영 팁:

  • 도구 실패 시 “무조건 재시도”가 아니라, 에러 타입별로 retryable을 분리하세요. 401이나 403은 재시도해도 안 풀리는 경우가 많습니다.

5) 컨텍스트 팽창 방지: 메모리 요약·스냅샷·증거만 보관

비용폭주는 루프 자체뿐 아니라 프롬프트가 길어지는 것에서도 발생합니다. AutoGPT 스타일은 히스토리를 계속 붙이고, 중간 산출물을 다시 붙이며, 웹 페이지 원문을 통째로 넣기 쉽습니다.

권장 패턴:

  • “전체 대화” 대신 “상태(state) + 증거(evidence) + 최근 n턴”만 넣기
  • 웹 페이지는 원문 저장 후, LLM에는 핵심 인용문만 전달
  • 스텝마다 요약을 갱신하는 rolling summary
// TypeScript 예시: 롤링 요약 기반 컨텍스트 구성

interface Memory {
  rollingSummary: string;
  recentTurns: { role: "user" | "assistant"; content: string }[];
  evidence: { source: string; quote: string }[];
}

function buildPrompt(mem: Memory) {
  const recent = mem.recentTurns.slice(-6)
    .map(t => `${t.role}: ${t.content}`)
    .join("\n");

  const evidence = mem.evidence.slice(-8)
    .map(e => `- [${e.source}] ${e.quote}`)
    .join("\n");

  return [
    "SYSTEM: You are a careful agent.",
    `STATE SUMMARY: ${mem.rollingSummary}`,
    "EVIDENCE:",
    evidence || "- (none)",
    "RECENT:",
    recent
  ].join("\n\n");
}

운영 팁:

  • 요약은 “사실/결정/미해결 과제”로 구조화하면 환각과 재작업이 줄어듭니다.
  • RAG를 쓰는 경우, 벡터 검색 파라미터를 잘못 잡으면 불필요한 컨텍스트가 계속 유입됩니다. 대용량 인덱스 튜닝은 Milvus IVF_FLAT vs HNSW 성능 튜닝 실전 같은 글의 관점으로 “검색 품질과 비용”을 함께 보세요.

6) 안전한 실패 설계: 중단 시 요약·체크리스트·사람에게 물어보기

가드레일은 “막기”로 끝나면 UX가 나빠집니다. 중요한 건 안전하게 실패하는 방식입니다.

중단 시 에이전트가 남겨야 할 것:

  • 지금까지의 작업 요약 (무엇을 했고 무엇을 알게 됐는지)
  • 실패 원인 가설 (예: 인증 필요, 도구 불가, 정보 부족)
  • 다음 행동 후보 2~3개
  • 사용자가 답하면 재개 가능한 질문 1개
중단 리포트 템플릿(예시)
- 진행 요약: ...
- 마지막 관찰: ...
- 중단 사유: maxToolCalls exceeded
- 다음 후보:
  1) 도구 호출 없이 현재 정보로 초안 작성
  2) 사용자에게 추가 입력 요청
  3) 특정 도구 권한 승인 요청
- 재개 질문: ...

운영 팁:

  • “사람에게 물어보기”를 하나의 도구로 취급하고, 예산이 남아있어도 루프가 감지되면 우선 질문으로 전환하는 정책이 효과적입니다.

7) 관측 가능성: 스텝 트레이싱·비용 대시보드·알람

무한루프는 사전에 완벽히 막기 어렵습니다. 결국 운영에서 중요한 건 빨리 발견하고, 재현하고, 정책을 업데이트하는 루프입니다.

필수 텔레메트리:

  • 실행 단위 run_id
  • 스텝별 tool, args_hash, latency_ms, tokens_in/out, cost_estimate
  • 종료 사유: success, max_steps, budget_exceeded, loop_detected, tool_error
  • 루프 감지 시점의 최근 서명 목록
{
  "run_id": "run_20260226_001",
  "step": 12,
  "tool": "search",
  "args_hash": "9f2c...",
  "latency_ms": 842,
  "tokens_in": 1820,
  "tokens_out": 220,
  "cost_estimate_usd": 0.012,
  "stop_reason": "loop_detected"
}

운영 팁:

  • 알람은 “비용”과 “반복”을 함께 묶어야 합니다. 예: 5분 내 tool_calls 30회 또는 동일 args_hash 10회.
  • 샘플링으로 로그를 줄이면, 루프 구간이 잘릴 수 있습니다. 루프 감지 이벤트만은 반드시 풀 로깅하세요.

실전 적용 순서(권장)

7가지를 한 번에 넣기 어렵다면 아래 순서가 실패를 가장 빨리 줄입니다.

  1. max_steps + max_wall_time부터 즉시 적용
  2. 토큰/도구 호출 예산 차감 추가
  3. 동일 행동 반복 감지(서명 기반) 추가
  4. 도구 allowlist와 입력 상한(특히 검색 top_k) 적용
  5. 롤링 요약으로 컨텍스트 팽창 차단
  6. 중단 리포트 템플릿으로 안전한 실패 UX 구축
  7. 대시보드/알람으로 운영 루프 완성

마무리

AutoGPT 무한루프와 비용폭주는 “모델이 멍청해서”가 아니라, 시스템이 무한 실행을 허용해서 생깁니다. 따라서 가드레일은 선택이 아니라 기본값이어야 합니다. 특히 하드 리밋과 예산 리밋, 반복 감지는 구현 난이도 대비 효과가 압도적입니다.

다음 단계로는, 여러분의 에이전트가 실제로 어떤 패턴으로 실패하는지 텔레메트리로 수집한 뒤, 루프 감지 규칙과 도구 정책을 지속적으로 업데이트하는 운영 체계를 갖추는 것이 핵심입니다.