- Published on
AutoGPT 메모리 루프 폭주 진단과 상태·툴콜 차단
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
AutoGPT 류 에이전트를 운영하다 보면, 어느 순간부터 같은 생각을 되풀이하며 진행이 멈추거나 특정 툴을 무한 호출하는 현상을 마주칩니다. 흔히 “메모리 루프 폭주”라고 부르지만, 실제 원인은 메모리 자체라기보다 상태(state) 관리 실패와 툴콜(tool call) 재시도 정책 부재가 겹치는 경우가 많습니다.
이 글은 다음을 목표로 합니다.
- 루프 폭주를 증상별로 분류하고, 로그에서 원인을 빠르게 좁히는 방법
- 에이전트 실행을 상태 머신으로 구조화해 루프를 원천 차단
- 툴콜을 정책 기반으로 차단/감쇠(circuit breaker, backoff, budget)하는 구현 패턴
유사한 “무한 반복” 디버깅은 OAuth 리다이렉트 루프에서도 자주 보입니다. 원인 분해 방식이 비슷하니 함께 참고하면 좋습니다: Keycloak OAuth 로그인 무한 리다이렉트 원인 7가지
1) 메모리 루프 폭주, 어떤 형태로 나타나나
현장에서 자주 관측되는 폭주 패턴은 대략 4가지로 나뉩니다.
1-1. 동일 계획 재수립 루프
- 증상: 매 스텝마다
plan또는next action이 거의 동일 - 원인: 목표 달성 조건(termination condition)이 모호하거나, “완료 판정”을 모델에게만 맡김
1-2. 동일 툴콜 재시도 루프
- 증상: 같은 입력으로
web_search,browser,db_query등을 반복 호출 - 원인: 툴 실패를 “정보 부족”으로 오인하거나, 실패 결과가 메모리에 저장되어도 다음 스텝에서 무시됨
1-3. 메모리 오염 루프
- 증상: 잘못된 사실이 메모리에 들어가 이후 판단을 계속 오염시켜 같은 결론으로 회귀
- 원인: “관찰(observation)”과 “추론”을 구분하지 않고 저장, 또는 신뢰도/출처 메타데이터 부재
1-4. 예산(budget) 무시 루프
- 증상: 토큰/시간/툴 호출 수가 임계치를 넘어도 멈추지 않음
- 원인: 상위 오케스트레이터가 강제 종료를 못 하거나, 종료 신호가 프롬프트 레벨에만 존재
2) 진단의 핵심: 상태·툴콜·메모리를 분리해서 로그를 본다
루프를 잡는 가장 빠른 방법은 “무엇이 반복되는지”를 세 축으로 분리해 보는 것입니다.
- 상태(state): 지금 에이전트가
PLAN인지ACT인지EVAL인지 - 툴콜(tool call): 어떤 툴을 어떤 파라미터로 호출했는지, 결과가 무엇인지
- 메모리(memory): 어떤 정보가 저장/조회되었는지, 근거가 무엇인지
2-1. 최소 로그 스키마
아래처럼 스텝 단위로 구조화해 남기면, 반복 패턴이 즉시 보입니다.
{
"run_id": "r-2026-02-26-001",
"step": 17,
"state": "ACT",
"goal": "Summarize incident and propose fix",
"tool_call": {
"name": "web_search",
"args_hash": "sha256:...",
"args": {"q": "AutoGPT memory loop runaway"}
},
"tool_result": {
"ok": false,
"error_type": "RATE_LIMIT",
"status": 429
},
"memory": {
"retrieved": ["m-12", "m-09"],
"written": ["m-33"],
"write_tags": ["observation", "tool_error"]
},
"decision": {
"next_state": "ACT",
"reason": "Need more sources"
}
}
포인트는 다음 2가지입니다.
args_hash: “같은 툴콜이 반복되는지”를 해시로 즉시 판별tool_result.error_type: 실패의 종류(예:RATE_LIMIT,TIMEOUT,SCHEMA_ERROR)를 분류
2-2. 루프 감지 지표
운영에서는 아래 지표 3개만 있어도 대부분 잡힙니다.
same_tool_args_hash_count: 같은args_hash가 N회 이상 반복state_oscillation:PLAN과ACT사이를 M회 왕복no_progress_steps: “새로운 아티팩트 생성” 없이 K스텝 경과
3) 상태 폭주를 막는 1차 처방: 상태 머신으로 강제 구조화
에이전트를 “대화형 루프”로만 두면, 종료 조건을 모델이 애매하게 해석할 때 폭주합니다. 해결은 간단합니다. 상태 머신을 코드로 고정하고, 각 상태에서 가능한 전이만 허용합니다.
3-1. 추천 상태 모델
PLAN: 목표를 작업 단위로 분해ACT: 툴 호출 또는 작업 수행EVAL: 결과 평가 및 완료 판정RECOVER: 실패 복구(대체 툴, backoff, 사용자 질문)STOP: 종료
3-2. 전이 규칙 예시
PLAN은 최대 1회 또는 2회만 허용ACT에서 동일 툴콜이 반복되면RECOVER로 강제 전이EVAL에서 완료 조건 만족 시STOP
아래는 TypeScript 기반의 간단한 오케스트레이터 예시입니다.
type State = "PLAN" | "ACT" | "EVAL" | "RECOVER" | "STOP";
interface Ctx {
step: number;
planAttempts: number;
toolRepeatCount: number;
lastToolArgsHash?: string;
progressScore: number; // 0..1
budget: { maxSteps: number; maxToolCalls: number; toolCalls: number };
}
function nextState(state: State, ctx: Ctx): State {
if (ctx.step >= ctx.budget.maxSteps) return "STOP";
switch (state) {
case "PLAN":
if (ctx.planAttempts >= 2) return "ACT";
return "ACT";
case "ACT":
if (ctx.toolRepeatCount >= 3) return "RECOVER";
return "EVAL";
case "EVAL":
if (ctx.progressScore >= 0.9) return "STOP";
return "ACT";
case "RECOVER":
return "ACT";
default:
return "STOP";
}
}
핵심은 “모델이 다음 상태를 정하게 두지 말고”, 모델은 상태별 산출물만 만들게 하는 것입니다.
4) 툴콜 폭주를 막는 2차 처방: 차단 규칙과 회로 차단기
툴콜 루프는 보통 아래 중 하나입니다.
- 동일 인자 재시도
- 실패 원인(429, 5xx, timeout)을 무시한 재시도
- 스키마 오류를 고치지 못하고 반복
이를 막기 위한 장치는 크게 4가지입니다.
4-1. 동일 인자 반복 차단
tool_name 과 args_hash 기준으로 최근 N개 호출을 기억하고, 반복이면 차단합니다.
import crypto from "crypto";
type ToolCall = { name: string; args: unknown };
function hashArgs(args: unknown): string {
return crypto.createHash("sha256").update(JSON.stringify(args)).digest("hex");
}
class ToolCallGuard {
private window: Array<{ key: string; at: number }> = [];
constructor(private size = 20, private maxRepeat = 2) {}
canCall(tc: ToolCall): { ok: boolean; reason?: string } {
const key = `${tc.name}:${hashArgs(tc.args)}`;
const repeats = this.window.filter(x => x.key === key).length;
if (repeats >= this.maxRepeat) {
return { ok: false, reason: "REPEATED_SAME_ARGS" };
}
this.window.push({ key, at: Date.now() });
if (this.window.length > this.size) this.window.shift();
return { ok: true };
}
}
차단되면 RECOVER 로 전이시키고, 모델에게는 “다른 접근”을 강제합니다.
4-2. 실패 유형별 정책(backoff, switch, stop)
실패를 하나로 뭉개지 말고, 실패 타입에 따라 다른 처방을 적용합니다.
RATE_LIMIT: 지수 backoff 후 재시도하되 총 재시도 횟수 제한TIMEOUT: 타임아웃 상향 또는 더 가벼운 쿼리로 축소SCHEMA_ERROR: 재시도 금지, 즉시 프롬프트/파서 수정 또는 모델에게 “스키마 수선” 태스크 부여
type ToolErrorType = "RATE_LIMIT" | "TIMEOUT" | "SCHEMA_ERROR" | "UNKNOWN";
function policyFor(err: ToolErrorType) {
switch (err) {
case "RATE_LIMIT":
return { action: "BACKOFF_RETRY", maxRetries: 2, baseMs: 800 } as const;
case "TIMEOUT":
return { action: "RETRY_WITH_SIMPLER_ARGS", maxRetries: 1 } as const;
case "SCHEMA_ERROR":
return { action: "STOP_AND_FIX", maxRetries: 0 } as const;
default:
return { action: "ESCALATE", maxRetries: 0 } as const;
}
}
4-3. 툴 예산(budget)과 강제 종료
“토큰 예산”만 두면 툴 폭주는 못 잡습니다. 아래처럼 툴 호출 예산을 별도로 두세요.
- run 전체
maxToolCalls - 툴별
maxCallsPerTool - 동일 도메인(예: 외부 HTTP)
maxExternalCalls
예산 초과 시 STOP 또는 “사용자 질문”으로 전환합니다.
4-4. 회로 차단기(circuit breaker)
특정 툴이 연속 실패하면 일정 시간 “열림(open)” 상태로 두고 호출을 막습니다.
- 연속 실패 3회면 open
- 60초간 차단
- half-open 에서 1회만 테스트
이 패턴은 인프라/성능 문제를 겪을 때 특히 유용합니다. 성능 병목을 진단하는 사고방식은 아래 글과도 유사합니다: Chrome Forced reflow 경고 원인·해결 7단계
5) 메모리 루프를 막는 3차 처방: “저장 규칙”과 “조회 규칙”을 분리
메모리 폭주는 대체로 “너무 많이 저장”하거나 “잘못 저장”해서 생깁니다.
5-1. 메모리에 저장할 것과 저장하지 말 것
저장 권장
- 툴 결과 중 사실(fact): 수치, 에러 코드, URL, 실행 결과
- 의사결정 근거: 왜 이 결론을 선택했는지(단, 장황한 내부 추론 전체가 아니라 요약)
- 실패 이력: 어떤 툴이 어떤 이유로 실패했는지
저장 비권장
- 매 스텝의 장문 추론 텍스트
- “아마도”, “추측”만 있는 내용
- 같은 문장 반복
5-2. 메모리 엔트리에 신뢰도와 출처를 붙이기
메모리를 단순 문자열로 저장하지 말고, 최소한 아래 메타데이터를 붙이세요.
type:observation,decision,constraintsource:tool:web_search,tool:db,humanconfidence: 0..1
이렇게 하면 “오염된 메모리”를 조회 단계에서 걸러낼 수 있습니다.
5-3. 조회 시 중복 억제와 최신성 가중
RAG 기반 메모리는 유사도 상위 K개를 그대로 넣으면, 같은 내용이 여러 조각으로 반복 주입되어 루프를 강화합니다.
- 유사도 기반 top K 이후 MMR 또는 중복 제거
- 최근 N분/최근 M스텝 이내 메모리 우선
tool_error태그는 별도 채널로 제공해 “같은 실패 반복”을 막기
추론 품질을 올리되 불필요한 내부 추론 노출을 줄이는 패턴은 아래 글을 함께 참고하세요: CoT 노출 없이 추론 정확도 올리는 실전 패턴
6) 상태·툴콜 차단을 프롬프트가 아니라 “런타임”에서 강제하라
많은 팀이 “다시 시도하지 마” 같은 지시를 시스템 프롬프트에 넣고 끝내는데, 폭주 상황에서는 잘 안 먹힙니다. 이유는 간단합니다.
- 모델은 실패를 “정보 부족”으로 재해석할 수 있음
- 프롬프트는 강제가 아니라 권고
- 네트워크/외부 API 실패는 모델이 통제 불가
따라서 차단은 반드시 런타임에서 강제해야 합니다.
6-1. 실행 루프 의사코드
while (state !== "STOP") {
const guardStop = ctx.budget.toolCalls >= ctx.budget.maxToolCalls;
if (guardStop) break;
if (state === "ACT") {
const tc = await model.proposeToolCall(/* state-scoped prompt */);
const allowed = toolCallGuard.canCall({ name: tc.name, args: tc.args });
if (!allowed.ok) {
state = "RECOVER";
continue;
}
const res = await tools.execute(tc);
ctx.budget.toolCalls++;
if (!res.ok) {
const p = policyFor(res.errorType);
// 정책에 따라 backoff, args 축소, 또는 RECOVER/STOP
state = p.action === "STOP_AND_FIX" ? "STOP" : "RECOVER";
continue;
}
memory.write({ type: "observation", source: `tool:${tc.name}`, confidence: 0.9, data: res.data });
state = "EVAL";
continue;
}
if (state === "EVAL") {
const evalResult = await model.evaluate(/* include artifacts + key memories */);
ctx.progressScore = evalResult.progressScore;
state = nextState("EVAL", ctx);
continue;
}
if (state === "RECOVER") {
// 실패 요약을 만들고 대체 플랜 또는 사용자 질문으로 전환
const recovery = await model.recover(/* include tool_error memories */);
// 필요 시 계획 수정, 툴 변경, 입력 축소
state = "ACT";
continue;
}
state = nextState(state, ctx);
}
이 구조의 장점은 명확합니다.
- 모델은 “제안”만 하고, 실행과 차단은 코드가 한다
- 같은 툴콜 반복, 예산 초과, 실패 유형 무시는 런타임에서 원천 봉쇄
7) 현장 체크리스트: 폭주를 재현하고, 고치고, 재발 방지하기
7-1. 재현
- 동일 입력으로 5회 이상 실행해 “확률적 폭주”를 확인
- 외부 API에 429를 강제로 유도(낮은 rate limit 환경)해 재시도 정책 검증
- 툴 결과를 의도적으로 스키마 깨뜨려
SCHEMA_ERROR처리 확인
7-2. 고치기
- 상태 머신 도입:
PLAN횟수 제한,EVAL기반 종료 - 툴콜 가드: 동일
args_hash반복 차단 - 실패 정책:
RATE_LIMITbackoff,SCHEMA_ERROR즉시 중단 - 예산 분리: 토큰과 툴콜 예산을 별개로 강제
7-3. 재발 방지
- 루프 감지 지표를 알람화
- “폭주 샘플 run 로그”를 회귀 테스트로 고정
- 툴별 SLO와 실패율을 대시보드로 관리
8) 결론: 메모리 루프는 메모리 문제가 아니라 제어 문제다
AutoGPT 메모리 루프 폭주는 대부분 상태 전이의 모호함과 툴콜 재시도 통제 부재에서 시작해, 메모리가 그 문제를 증폭시키는 형태로 나타납니다. 해결의 우선순위는 아래 순서가 가장 안전합니다.
- 상태 머신으로 실행을 구조화하고 종료 조건을 코드로 강제
- 툴콜 가드와 실패 유형별 정책으로 동일 호출 재시도 루프 차단
- 메모리 저장/조회 규칙을 분리하고, 신뢰도·출처 메타데이터로 오염을 줄이기
이 3가지를 적용하면 “모델이 똑똑해지길 기대”하는 방식이 아니라, 시스템이 폭주를 물리적으로 못 하게 만드는 방식으로 운영 안정성을 끌어올릴 수 있습니다.