- Published on
AutoGPT 메모리 폭주·루프 방지 - 벡터DB+가드레일
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
AutoGPT 계열 에이전트(툴 호출 + 계획 + 실행 루프)는 조금만 방치해도 메모리 폭주(컨텍스트가 계속 비대해짐)와 루프(같은 툴/프롬프트를 반복)로 쉽게 무너집니다. 특히 “생각-행동-관찰” 패턴을 쓰는 에이전트는 관찰 결과를 전부 대화에 누적시키기 쉬워 토큰 비용이 급증하고, 실패 원인을 제대로 모델링하지 않으면 재시도만 반복하다가 API 한도나 비용 예산을 태웁니다.
이 글은 문제를 “프롬프트를 잘 쓰자” 수준에서 끝내지 않고, 벡터DB 기반 장기기억 + 가드레일을 결합해 메모리/루프를 구조적으로 차단하는 설계를 다룹니다.
왜 AutoGPT는 메모리 폭주와 루프에 취약한가
1) 단일 컨텍스트에 모든 것을 누적하는 구조
많은 구현이 “지금까지의 대화/관찰/로그”를 그대로 다음 턴에 넣습니다. 이때 에이전트는 다음과 같은 악순환에 빠집니다.
- 관찰 로그가 길어짐
- 모델이 핵심을 못 잡고 더 장황한 계획을 생성
- 툴 호출이 늘어남
- 관찰 로그가 더 길어짐
즉, 메모리 관리를 “대화창 스크롤”처럼 하면 토큰이 선형이 아니라 상호증폭합니다.
2) 실패를 “상태”로 저장하지 않으면 재시도 루프가 생긴다
예를 들어 웹 크롤링이 403으로 막혔는데, 에이전트가 이를 “정책적으로 막혔다”가 아니라 “일시적 실패”로만 이해하면 같은 요청을 계속 반복합니다. 실패를 원인-결과-대안 형태의 구조화된 메모리로 남기지 않으면, 에이전트는 매 턴 같은 결론으로 회귀합니다.
3) 툴 호출은 비결정적이고 관찰은 장황하다
검색/브라우징/코드 실행 결과는 길고, 매번 결과가 달라질 수 있습니다. 이 비결정성이 루프 탐지를 더 어렵게 만듭니다.
목표 아키텍처: 단기기억은 얇게, 장기기억은 검색으로
핵심 원칙은 두 가지입니다.
- 단기기억(컨텍스트): “현재 목표를 달성하는 데 필요한 최소 정보”만 유지
- 장기기억(벡터DB): 필요할 때만 검색해서 주입
이를 위해 메모리를 다음처럼 분리합니다.
- Working Memory: 현재 스텝의 목표, 제약, 최근 1~3개의 관찰 요약
- Episodic Memory: 작업 단위의 사건(성공/실패/결정/근거)
- Semantic Memory: 사실/정의/사용자 선호 같은 재사용 지식
장기기억은 벡터DB에 저장하고, 매 턴 “질문-상황”에 맞춰 Top-k만 불러옵니다. 벡터DB 선택과 튜닝에서 리콜이 급락하면 “기억이 있는데도 못 찾는” 문제가 생기므로, 운영 중 리콜/지연을 꼭 점검해야 합니다. 관련 내용은 Pinecone·Milvus HNSW 리콜 급락 원인 6가지와 Milvus HNSW 튜닝으로 recall↑ latency↓를 함께 참고하면 좋습니다.
벡터DB 메모리 설계: “무엇을 저장하고, 어떻게 꺼낼 것인가”
1) 저장 단위: 로그가 아니라 “카드”로 저장
관찰 로그 원문을 그대로 저장하면 검색 결과도 장황해집니다. 대신 아래처럼 메모리 카드를 만듭니다.
type:decision|failure|fact|plan|tool_resulttask_id,run_id,stepsummary: 1~3문장 요약evidence: 핵심 근거(짧게)tags: 도메인/툴/오류코드ttl_days: 수명(예: 실패 패턴은 길게, 임시 결과는 짧게)
2) 검색 전략: “현재 목표 + 실패 히스토리”를 함께 질의
루프는 보통 실패를 반복하면서 생깁니다. 따라서 검색 쿼리는 단순히 “현재 목표”만 넣지 말고, 최근 실패 요약을 함께 넣어야 합니다.
- 쿼리 구성 예시
- 목표: “S3 버킷 정책 점검 자동화”
- 최근 실패: “권한 부족
AccessDenied로 AWS API 호출 실패” - 제약: “읽기 전용, 비용 제한”
3) Top-k + MMR + 스코어 컷오프
Top-k만 쓰면 비슷한 카드가 몰릴 수 있습니다. **MMR(Maximal Marginal Relevance)**로 다양성을 확보하고, 스코어가 낮으면 아예 주입하지 않는 컷오프를 둡니다.
가드레일 1: 예산(토큰/비용/시간/툴 호출) 기반 차단
메모리 폭주와 루프는 결국 “예산을 태우는 문제”입니다. 따라서 예산을 시스템 레벨에서 강제해야 합니다.
- 토큰 예산: 턴당 입력/출력 토큰 상한
- 비용 예산: 런 전체 비용 상한
- 시간 예산: 런 전체 wall-clock 상한
- 툴 호출 예산: 툴별 최대 호출 횟수
특히 레이트 리밋/재시도 폭주까지 겹치면 비용이 급증합니다. API 429 대응(백오프, 큐잉)을 에이전트 런타임에 넣어야 안정적입니다. 관련 실전 패턴은 OpenAI API 429 폭주 해결 - LangChain 백오프·큐를 참고하세요.
코드 예제: 런 예산과 툴 호출 예산
type Budget = {
maxTotalMs: number;
maxToolCalls: number;
maxTokensIn: number;
maxTokensOut: number;
};
type RunState = {
startedAt: number;
toolCalls: number;
tokensIn: number;
tokensOut: number;
};
export function assertBudget(b: Budget, s: RunState) {
const elapsed = Date.now() - s.startedAt;
if (elapsed > b.maxTotalMs) throw new Error("BudgetExceeded: time");
if (s.toolCalls > b.maxToolCalls) throw new Error("BudgetExceeded: tool_calls");
if (s.tokensIn > b.maxTokensIn) throw new Error("BudgetExceeded: tokens_in");
if (s.tokensOut > b.maxTokensOut) throw new Error("BudgetExceeded: tokens_out");
}
export function recordToolCall(s: RunState) {
s.toolCalls += 1;
}
이 예산 체크는 “모델이 알아서 멈추겠지”가 아니라, 호스트 애플리케이션이 강제 종료하는 장치입니다.
가드레일 2: 종료 조건(Stop Conditions)을 명시적으로 모델링
루프를 막으려면 “언제 성공/실패로 종료할지”를 코드로 정의해야 합니다.
- 성공 종료
- 목표 산출물이 생성됨(파일/레포트/PR 링크 등)
- 검증 테스트 통과
- 실패 종료
- 동일 오류 N회 반복
- 신규 정보 획득 없는 반복 N회
- 예산 초과
코드 예제: 동일 오류 반복 감지
type FailureSig = {
tool: string;
code?: string;
messageNorm: string;
};
function normalizeMsg(msg: string) {
return msg
.toLowerCase()
.replaceAll(/\d+/g, "<num>")
.replaceAll(/https?:\/\/\S+/g, "<url>")
.slice(0, 300);
}
export function failureSignature(tool: string, err: unknown): FailureSig {
const e = err as { code?: string; message?: string };
return {
tool,
code: e.code,
messageNorm: normalizeMsg(e.message ?? String(err)),
};
}
export function shouldAbortByRepeatedFailure(
sigs: FailureSig[],
current: FailureSig,
threshold: number
) {
const same = sigs.filter(
(s) => s.tool === current.tool && s.code === current.code && s.messageNorm === current.messageNorm
);
return same.length + 1 >= threshold;
}
주의: 정규식 치환에서 "<num>" 같은 문자열은 본문에 부등호가 보이지만, 코드 블록 내부이므로 MDX 빌드 에러를 유발하지 않습니다. 본문 일반 텍스트에서는 반드시 < > 또는 인라인 코드로 처리해야 합니다.
가드레일 3: “진전(Progress)”이 없는 반복을 끊기
동일 오류가 아니어도, 실질적으로는 같은 행동을 반복할 수 있습니다.
- 검색만 반복하고 요약/결론이 없음
- 같은 URL을 다시 크롤링
- 같은 파일을 읽고 또 읽음
이를 막으려면 매 스텝마다 “진전 지표”를 기록합니다.
- 새로 획득한 사실 수
- 새로 생성한 산출물 수
- 미해결 이슈 수의 감소
- 계획의 변경 여부
코드 예제: 액션 시그니처로 루프 탐지
import crypto from "node:crypto";
type Action = {
name: string;
args: Record<string, unknown>;
};
type StepMeta = {
actionHash: string;
progressScore: number;
};
export function hashAction(a: Action) {
const payload = JSON.stringify({ name: a.name, args: a.args }, Object.keys(a.args).sort());
return crypto.createHash("sha256").update(payload).digest("hex");
}
export function shouldAbortByActionLoop(history: StepMeta[], current: StepMeta) {
const recent = history.slice(-6);
const sameActionCount = recent.filter((h) => h.actionHash === current.actionHash).length;
const noProgress = recent.every((h) => h.progressScore <= 0) && current.progressScore <= 0;
return sameActionCount >= 3 && noProgress;
}
핵심은 “같은 행동 3회” 같은 단순 룰이 아니라, 진전이 없을 때만 강하게 종료하는 것입니다. 정상적인 반복(예: 페이지네이션 크롤링)은 진전 점수가 계속 올라가므로 중단되지 않습니다.
가드레일 4: 메모리 주입량 제한과 요약 파이프라인
벡터DB를 쓰더라도 “검색 결과를 전부 붙여 넣는” 순간 메모리 폭주가 재발합니다. 따라서 주입 파이프라인을 둡니다.
- 검색 결과 Top-k
- 각 카드
summary만 1차 주입 - 정말 필요할 때만
evidence원문을 추가 fetch - 최종적으로 컨텍스트에 들어가는 메모리 토큰 상한 적용
코드 예제: 검색 결과를 컨텍스트 예산에 맞게 압축
type MemoryCard = {
id: string;
type: string;
summary: string;
evidence?: string;
score: number;
};
export function buildMemoryContext(cards: MemoryCard[], maxChars: number) {
const picked: string[] = [];
let used = 0;
for (const c of cards.sort((a, b) => b.score - a.score)) {
const line = `- [${c.type}] ${c.summary}`;
if (used + line.length + 1 > maxChars) break;
picked.push(line);
used += line.length + 1;
}
return picked.join("\n");
}
실전에서는 문자 수 대신 토큰 수를 쓰는 게 더 정확하지만, 대략적인 상한을 두는 것만으로도 폭주를 크게 줄일 수 있습니다.
벡터DB 운영 팁: “기억이 안 난다”는 대부분 검색 품질 문제
에이전트가 같은 실수를 반복할 때, 원인이 “가드레일 부재”뿐 아니라 기억 검색 실패인 경우가 많습니다.
- 임베딩 모델이 도메인과 안 맞음
- 청크가 너무 길거나 너무 짧음
- 메타데이터 필터가 과도함
- HNSW 파라미터가 보수적이라 리콜이 낮음
이때는 “에이전트가 멍청해서”가 아니라, 검색 계층이 병목입니다. 앞서 링크한 HNSW 리콜/튜닝 글들을 기준으로, 리콜과 지연을 함께 관측하세요.
실전 프롬프트 가드레일: 모델에게도 규칙을 ‘선언’하되, 강제는 코드로
프롬프트에는 다음을 명시합니다.
- 같은 툴 호출을 반복하기 전에 “왜 실패했는지” 가설을 갱신할 것
- 2회 연속 실패 시 대체 전략을 제시할 것
- 새 정보가 없으면 요약하고 종료 조건을 평가할 것
하지만 프롬프트는 어디까지나 “협조 요청”입니다. 실제 차단은 예산/종료 조건/루프 탐지 코드가 담당해야 합니다.
레퍼런스 구현 흐름(턴 단위)
아래는 한 턴의 권장 흐름입니다.
- 목표/제약/최근 요약을 Working Memory로 구성
- 벡터DB에서 관련 메모리 카드 검색(MMR + 컷오프)
- 컨텍스트 예산 내에서 메모리 주입(요약 우선)
- 모델이 액션 제안
- 액션 시그니처 해시 생성, 과거 반복 여부 확인
- 툴 실행
- 관찰을 구조화(요약 + 태그 + 실패 시그니처)
- 장기기억에 저장(카드)
- 예산/종료 조건 평가 후 다음 턴 또는 종료
체크리스트: 메모리 폭주·루프를 실제로 줄이는 항목
- 단기기억에 원문 로그를 누적하지 않고 “요약만” 유지
- 장기기억은 벡터DB에 카드 형태로 저장
- 검색은 목표뿐 아니라 최근 실패를 포함
- Top-k만이 아니라 MMR/스코어 컷오프 적용
- 컨텍스트 주입량 상한(토큰/문자) 강제
- 동일 오류 반복 종료, 동일 액션 반복 종료, 무진전 반복 종료
- 툴별 호출 예산과 런 전체 예산 강제
-
429등 외부 요인으로 인한 재시도 폭주 방지(백오프/큐)
마무리: “기억”과 “규칙”을 분리하면 에이전트가 안정화된다
AutoGPT의 메모리 폭주와 루프는 대개 한 가지 처방으로 해결되지 않습니다. 벡터DB로 기억을 얇게 만들고, 가드레일로 반복을 강제 차단해야 합니다. 특히 “실패를 구조화해서 저장”하고, “진전 없는 반복을 종료”하는 두 축이 들어가면 에이전트는 훨씬 덜 헤매고, 비용은 예측 가능해집니다.
다음 단계로는 (1) 메모리 카드 품질을 높이는 요약 정책, (2) 벡터 검색 리콜 모니터링, (3) 액션 선택을 평가하는 간단한 스코어러를 붙이면 운영 환경에서도 안정적으로 굴릴 수 있습니다.