- Published on
AutoGPT 루프 폭주 막는 메모리·가드레일 설계
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
AutoGPT류 에이전트는 계획(Plan)과 도구 호출(Tool)을 반복하며 목표를 달성합니다. 문제는 이 반복이 스스로 멈추지 못하는 구조로 쉽게 변한다는 점입니다. 프롬프트가 애매하거나, 메모리가 오염되거나, 도구 결과가 불안정하면 에이전트는 같은 행동을 변형만 하며 재시도하고, 결국 무한 루프, 토큰/비용 폭주, 외부 시스템 부하로 이어집니다.
이 글에서는 루프 폭주의 전형적인 원인을 분해하고, 이를 막기 위한 메모리 계층 설계와 가드레일(Guardrails) 실행 제어를 실전 관점에서 정리합니다. 분산 시스템의 타임아웃/보상 트랜잭션 개념을 에이전트 실행에 이식하는 방식도 함께 다룹니다.
참고로, 에이전트의 폭주는 “시간 제한 없는 RPC 재시도”와 유사합니다. 타임아웃과 취소 전파가 없는 호출은 결국 deadline exceeded 류 장애로 돌아옵니다. 관련 맥락은 Go gRPC context deadline exceeded 원인 7가지도 함께 보면 감이 빨리 잡힙니다.
1) AutoGPT 루프 폭주가 발생하는 6가지 전형
1-1. 목표 함수가 불명확하거나 측정 불가
에이전트는 “완료”를 판정할 수 있어야 멈춥니다. 하지만 목표가 최대한 조사해줘, 완벽히 작성해줘처럼 종료 조건이 없는 최적화 문제면, 모델은 계속 개선을 시도합니다.
1-2. 도구 결과가 비결정적이거나 실패를 정상으로 오인
웹 검색, 크롤링, API 호출은 종종 실패합니다. 실패 응답이 빈 문자열이나 200 OK + 에러 메시지 텍스트처럼 애매하면, 에이전트는 성공으로 간주하고 다음 단계로 넘어가며 더 큰 혼란을 만듭니다.
1-3. 메모리 오염: 잘못된 가정이 장기 기억에 박힘
한 번의 hallucination이 “사실”처럼 요약되어 장기 메모리에 들어가면 이후 루프는 그 가정을 강화합니다. 결과적으로 같은 결론을 반복하거나, 실패한 접근을 계속 재시도합니다.
1-4. 관측 불가능성: 왜 반복하는지 사람이 모름
로그가 thought 중심이거나, tool input/output가 누락되면 운영자가 원인을 파악하지 못해 더 오래 방치됩니다.
1-5. 재시도 정책 부재
네트워크/레이트리밋/일시 장애는 재시도가 필요하지만, 재시도에 횟수, 백오프, 대체 경로, 중단 조건이 없으면 루프가 됩니다.
1-6. 안전 가드레일 부재: 도구 남용과 권한 과다
파일 삭제, 결제, 배포, DB 변경 같은 고위험 도구가 “한 번의 LLM 판단”으로 실행되면, 루프는 곧 사고로 이어집니다.
2) 멈추게 만드는 설계: 메모리 계층 3단 + 실행 상태 머신
루프 폭주를 막는 핵심은 메모리를 덜 믿고, 실행을 더 엄격히 상태로 관리하는 것입니다. 추천하는 기본 구조는 다음 3가지 메모리 계층입니다.
2-1. 작업 메모리(Working Memory): 단기 컨텍스트
- 현재 스텝의 입력/출력
- 최신 관측값(도구 결과)
- 당장 필요한 변수
특징: 쉽게 버리고, 길이를 제한합니다.
2-2. 에피소드 메모리(Episodic Memory): 실행 로그 요약
step_id,tool,tool_input_hash,tool_output_hash,status,latency_ms- 실패 원인 코드(예:
RATE_LIMIT,TIMEOUT,BAD_SCHEMA)
특징: 루프 탐지와 재시도 제어에 직접 사용합니다. 자연어 요약만 저장하면 탐지가 어려우니, 구조화 필드를 반드시 포함합니다.
2-3. 장기 메모리(Long-term / Vector Memory): 사실 후보 저장소
- 문서/정책/과거 결과
- 단, “사실”이 아니라 “근거가 있는 후보”로 취급
특징: 장기 메모리는 오염되기 쉬우므로, **출처/신뢰도/만료( TTL )**를 함께 저장합니다.
2-4. 실행 상태 머신(필수)
에이전트를 while true로 굴리면 언젠가 폭주합니다. 다음처럼 명시적 상태를 둡니다.
PLAN: 다음 행동 계획 생성ACT: 도구 실행OBSERVE: 결과 검증(스키마/정합성)EVALUATE: 목표 달성 여부 판단RECOVER: 실패 복구(대체 도구, 백오프)STOP: 종료
이 구조는 분산 트랜잭션에서 “실패를 정상 흐름으로 모델링”하는 사고와 닮았습니다. 복구 단계가 없으면 실패가 곧 루프가 됩니다. 분산 관점의 복구 설계는 MSA Saga 보상 트랜잭션 설계와 디버깅 실전에서의 접근을 에이전트에도 그대로 적용할 수 있습니다.
3) 가드레일 10종 세트: 루프·비용·도구 남용 차단
아래는 운영에서 효과가 컸던 가드레일 목록입니다. 1~3개만 넣어도 체감이 크지만, 조합이 중요합니다.
3-1. 스텝 예산(Step budget)
- 전체 실행
max_steps - 목표별
max_steps_per_goal - 상태별
max_retries_per_tool
종료 시에는 “부분 결과 + 다음에 사람이 해야 할 일”을 출력하게 합니다.
3-2. 토큰/비용 예산(Token budget)
- 요청 단위
max_tokens - 실행 단위
max_total_tokens - 비용 추정치 기반
max_cost
토큰 상한은 루프 방지뿐 아니라 “긴 프롬프트로 인한 품질 저하”도 막습니다.
3-3. 시간 제한(Deadline)과 취소 전파
각 도구 호출에 timeout_ms를 부여하고, 전체 실행에 deadline을 둡니다. 타임아웃은 “실패”가 아니라 “관측값”입니다. 이후 RECOVER로 넘어가야 합니다.
3-4. 반복 패턴 탐지(Loop detector)
다음 신호를 조합합니다.
- 동일 tool + 동일 input hash 반복
- 유사한 계획 문장 n회 반복(문장 임베딩 cosine 유사도)
- 실패 코드가 같은 재시도 반복
탐지되면 STOP 또는 “전략 변경 프롬프트”로 강제 전환합니다.
3-5. 도구 호출 스키마 검증(Contract)
도구 입력/출력은 반드시 JSON 스키마로 검증합니다.
- 입력 누락, 타입 불일치
- 출력이 텍스트 에러인데 200으로 온 경우
검증 실패는 OBSERVE에서 즉시 차단합니다.
3-6. 권한 분리와 위험 등급(RBAC + Risk tier)
도구를 read-only, write, destructive로 나누고,
destructive는 사람 승인 필요write는 dry-run 우선read-only만 자동 허용
3-7. Dry-run / Plan-only 모드
실제 실행 전에 “예상 변경 사항”을 출력하게 하고, 승인 후 실행합니다.
3-8. 외부 시스템 레이트리밋·서킷브레이커
- 429/5xx 급증 시 잠시 차단
- 백오프 + 지수 증가
- 대체 데이터 소스 사용
3-9. 메모리 TTL과 신뢰도 스코어
장기 메모리에 저장할 때 다음을 함께 저장합니다.
source_url또는tool_run_idconfidence(0~1)expires_at
만료된 기억은 검색 결과에서 제외하거나 가중치를 낮춥니다.
3-10. 관측성(Observability) 필수 필드
운영에서 가장 중요한 가드레일은 “빨리 알아차리는 것”입니다.
run_id,step_id,state- tool input/output 크기, 해시
- latency, tokens, cost
- stop reason(예:
MAX_STEPS,LOOP_DETECTED)
배포 자동화 환경이라면, 상태 드리프트나 동기화 실패가 에이전트의 “복구 루프”를 유발하기도 합니다. 운영 중 배포/동기화 이슈 점검은 Argo CD Sync 실패 - OutOfSync·Degraded 해결 같은 체크리스트가 도움이 됩니다.
4) 구현 예시: 상태 머신 + 루프 탐지 + 예산
아래 예시는 Python 스타일의 의사코드입니다. 핵심은 while을 돌리더라도 상태/예산/탐지/검증이 모두 끼어있어야 한다는 점입니다.
import time
import hashlib
from dataclasses import dataclass
def sha(x: str) -> str:
return hashlib.sha256(x.encode("utf-8")).hexdigest()[:16]
@dataclass
class Budgets:
max_steps: int = 20
max_total_tokens: int = 20000
max_total_cost_usd: float = 2.0
deadline_ts: float = 0.0 # epoch seconds
@dataclass
class ToolCall:
name: str
input_json: str
@dataclass
class StepRecord:
step_id: int
state: str
tool_name: str | None
tool_input_hash: str | None
tool_output_hash: str | None
status: str
tokens_used: int
cost_usd: float
latency_ms: int
class LoopDetector:
def __init__(self, window=6, same_call_threshold=3):
self.window = window
self.same_call_threshold = same_call_threshold
self.recent = [] # list of (tool, input_hash)
def push(self, tool: str, input_hash: str):
self.recent.append((tool, input_hash))
if len(self.recent) > self.window:
self.recent.pop(0)
def is_looping(self) -> bool:
if len(self.recent) < self.same_call_threshold:
return False
last = self.recent[-1]
return sum(1 for x in self.recent if x == last) >= self.same_call_threshold
def validate_tool_output(tool_name: str, output_json: str) -> tuple[bool, str]:
# 실제로는 JSON schema validator 사용 권장
if not output_json or "error" in output_json.lower():
return (False, "BAD_OUTPUT")
return (True, "OK")
def run_agent(goal: str, budgets: Budgets):
state = "PLAN"
step_id = 0
total_tokens = 0
total_cost = 0.0
detector = LoopDetector()
history: list[StepRecord] = []
while True:
now = time.time()
if now >= budgets.deadline_ts:
return {"status": "STOP", "reason": "DEADLINE", "history": history}
if step_id >= budgets.max_steps:
return {"status": "STOP", "reason": "MAX_STEPS", "history": history}
if total_tokens >= budgets.max_total_tokens:
return {"status": "STOP", "reason": "MAX_TOKENS", "history": history}
if total_cost >= budgets.max_total_cost_usd:
return {"status": "STOP", "reason": "MAX_COST", "history": history}
if state == "PLAN":
# LLM에게 다음 행동 계획을 요청 (여기서는 생략)
planned = ToolCall(name="web_search", input_json='{"q":"' + goal + '"}')
state = "ACT"
elif state == "ACT":
t0 = time.time()
tool = planned.name
inp_hash = sha(planned.input_json)
detector.push(tool, inp_hash)
if detector.is_looping():
return {"status": "STOP", "reason": "LOOP_DETECTED", "history": history}
# 도구 실행 (여기서는 더미)
output = '{"result":"..."}'
latency_ms = int((time.time() - t0) * 1000)
ok, code = validate_tool_output(tool, output)
history.append(
StepRecord(
step_id=step_id,
state="ACT",
tool_name=tool,
tool_input_hash=inp_hash,
tool_output_hash=sha(output),
status=code,
tokens_used=200,
cost_usd=0.01,
latency_ms=latency_ms,
)
)
total_tokens += 200
total_cost += 0.01
state = "OBSERVE" if ok else "RECOVER"
elif state == "OBSERVE":
# 결과를 목표 달성 관점에서 평가 (여기서는 생략)
done = True
state = "STOP" if done else "PLAN"
elif state == "RECOVER":
# 백오프/대체 도구/프롬프트 수정 등
time.sleep(0.5)
state = "PLAN"
elif state == "STOP":
return {"status": "STOP", "reason": "DONE", "history": history}
step_id += 1
이 예시에서 루프 폭주를 막는 장치는 다음과 같습니다.
Budgets로 스텝/토큰/비용/시간을 모두 상한LoopDetector로 동일 도구 호출 반복을 탐지validate_tool_output으로 실패를 조기에RECOVER로 라우팅- 모든 스텝을
StepRecord로 구조화 기록(관측성)
5) 메모리 설계 디테일: “요약”만 믿으면 망한다
5-1. 장기 메모리에 넣기 전, 사실성 게이트를 둔다
장기 메모리에는 다음 조건을 통과한 것만 저장하는 편이 안전합니다.
- 출처가 명확하다(문서 URL, tool run id)
- 동일 사실이 2개 이상의 독립 소스에서 확인됨
- 혹은 내부 정책 문서처럼 신뢰 가능한 단일 소스
이를 코드로 표현하면 “저장 함수가 곧 가드레일”입니다.
def should_promote_to_long_term(source_type: str, confidence: float, has_citation: bool) -> bool:
if not has_citation:
return False
if source_type in ["policy", "internal_doc"]:
return True
return confidence >= 0.75
5-2. 메모리 TTL과 회수(garbage collection)
에이전트가 오래 돌수록 “오래된 결론”이 현재 상황과 충돌합니다. 따라서 장기 메모리는 expires_at 기반으로 회수하고, 검색 시에도 만료를 반영해야 합니다.
5-3. 실행 로그는 벡터DB가 아니라 이벤트 스토어에
루프 탐지/감사/비용 추적에는 벡터 검색보다 “정확한 필드 검색”이 유리합니다.
tool_name = XANDstatus = TIMEOUTANDcount급증input_hash중복
즉, 에피소드 메모리는 Postgres나 로그 스토리지(예: Loki, OpenSearch)에 두는 편이 운영이 쉽습니다.
6) 운영 체크리스트: 폭주를 ‘사고’가 아니라 ‘신호’로
6-1. Stop reason을 반드시 남긴다
DONE 말고도 최소한 아래는 필요합니다.
MAX_STEPSMAX_TOKENSMAX_COSTDEADLINELOOP_DETECTEDTOOL_SCHEMA_INVALIDTOOL_TIMEOUT
6-2. 실패 코드를 표준화한다
도구마다 오류 문자열이 제각각이면, 에이전트도 운영자도 학습이 불가능합니다.
RATE_LIMITTIMEOUTBAD_OUTPUTUNAUTHORIZEDNOT_FOUND
6-3. 인간 개입 지점(HITL)을 명시한다
다음 상황에서는 자동 실행을 멈추고 승인/질문으로 전환합니다.
- 파괴적 작업
- 동일 실패 3회 이상
- 비용 예산 80% 도달
- 결과가 목표에 대한 근거를 제공하지 못함
7) 결론: 루프를 막는 건 프롬프트가 아니라 “제어면(Control Plane)”
AutoGPT 루프 폭주는 대개 “모델이 멍청해서”가 아니라, 실행 제어가 없는 자동화에서 발생합니다. 해결책은 프롬프트 튜닝 단독이 아니라 다음의 결합입니다.
- 메모리 계층화: 작업/에피소드/장기 메모리를 분리
- 상태 머신:
PLAN-ACT-OBSERVE-EVALUATE-RECOVER-STOP - 예산: 스텝/토큰/비용/시간 상한
- 반복 탐지: 해시/유사도/실패 코드 기반
- 스키마 검증과 권한 분리: 도구를 안전하게
- 관측성: stop reason과 구조화 로그
이 정도를 갖추면, 에이전트는 “무한히 시도하는 자동화”가 아니라 “실패를 관리하는 실행기”로 바뀝니다. 그때부터는 루프 폭주가 장애가 아니라, 개선 가능한 데이터가 됩니다.