- Published on
AutoGPT 메모리 폭주 해결 - Redis TTL·요약·압축
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
AutoGPT류 에이전트는 “계획-실행-검증” 루프를 오래 돌릴수록 메모리가 누적되고, 결국 토큰 비용 폭증, Redis/벡터DB 용량 증가, 응답 지연, 심하면 프로세스 OOM까지 이어집니다. 특히 장기 실행(크롤링, 리서치, 코드 생성, 티켓 처리)에서는 메모리를 ‘무한히 쌓는’ 모델이 아니라 정책적으로 수명과 해상도를 관리하는 시스템이 필요합니다.
이 글에서는 AutoGPT 메모리 폭주의 전형적인 원인을 짚고, 현업에서 바로 적용 가능한 3가지 축을 조합해 해결합니다.
- Redis TTL로 “기억의 수명”을 강제
- **요약(Summarization)**으로 “기억의 해상도”를 단계적으로 낮춤
- **압축(Compression)**으로 “저장 비용”을 낮추고 네트워크/IO를 줄임
또한 운영 중 자주 겪는 Redis 연결 타임아웃이나 API 레이트리밋 같은 주변 이슈까지 함께 고려합니다.
왜 AutoGPT 메모리가 폭주하는가
에이전트 메모리 폭주는 보통 아래 패턴으로 발생합니다.
1) 이벤트 로그를 그대로 영구 저장
대화/툴 호출/관찰(observation) 로그를 원문 그대로 저장하면, 몇 분만 지나도 수천~수만 토큰이 쌓입니다. “나중에 필요할 수도”라는 심리로 전부 남기면, 결국 검색/프롬프트 주입 비용이 치솟습니다.
2) 동일 정보의 중복 저장
- 매 스텝마다 동일한 시스템 프롬프트/정책/툴 스펙을 저장
- 웹 페이지 원문을 중복으로 저장
- 요약본을 만들면서도 원문을 계속 유지
이 경우 메모리는 선형이 아니라 중복으로 인한 준-기하급수로 커집니다.
3) “장기 기억”과 “작업 기억”의 경계가 없음
인간도 작업 기억(working memory)과 장기 기억(long-term memory)을 분리합니다. AutoGPT도 동일하게:
- 작업 기억: 현재 목표 달성에 필요한 최근 컨텍스트
- 장기 기억: 재사용 가능한 사실/결론/결정 기록
을 분리하지 않으면, 모든 것이 장기 기억처럼 취급되어 폭주합니다.
해결 전략 개요: TTL·요약·압축을 함께 써야 하는 이유
세 가지는 서로 대체재가 아니라 보완재입니다.
- TTL: “언젠가 지워진다”를 보장해 최악을 막음
- 요약: 지우기 전에 정보 밀도를 높여 유용성을 유지
- 압축: 저장/전송 비용을 즉시 절감하고 Redis 메모리 사용량을 낮춤
운영 관점에서 가장 안전한 순서는:
- TTL로 상한선을 만들고
- 요약으로 품질을 유지하며
- 압축으로 비용을 줄입니다.
Redis TTL로 메모리 수명 관리하기
어떤 키에 TTL을 걸어야 하나
에이전트 메모리를 보통 3계층으로 나누면 관리가 쉬워집니다.
- 세션/런 단위 작업 로그: 반드시 TTL
- 예:
run:{runId}:events,run:{runId}:tool_calls
- 중간 산출물 캐시: TTL 권장
- 예: 웹 페이지 원문, 임시 파일 메타, 임베딩 캐시
- 장기 지식(사실/결정/요약): TTL 선택
- 업무 도메인에 따라 영구 보관하거나, “마지막 사용 기준 TTL”을 두는 편이 안전합니다.
TTL 설계 팁: “고정 TTL” + “슬라이딩 TTL” 혼합
- 고정 TTL: 생성 시점 기준
EXPIRE 86400같은 방식 - 슬라이딩 TTL: 조회/사용 시 TTL을 연장해 “최근 사용한 기억”을 유지
다만 슬라이딩 TTL은 핫키를 계속 살려두므로, 장기적으로 다시 폭주할 수 있습니다. 따라서 상한 TTL(예: 최대 30일) 같은 가드레일을 둡니다.
Node.js 예제: 이벤트 로그를 Redis Stream/List로 저장 + TTL
아래는 “런 단위 이벤트 로그”를 Redis에 저장하고 TTL을 부여하는 간단한 패턴입니다.
import { createClient } from "redis";
const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();
const RUN_TTL_SEC = 60 * 60 * 6; // 6시간
export async function appendRunEvent(runId: string, event: unknown) {
const key = `run:${runId}:events`;
const payload = JSON.stringify({ ts: Date.now(), event });
// List에 append
await redis.rPush(key, payload);
// 최초 생성/갱신 시 TTL 보장
const ttl = await redis.ttl(key);
if (ttl < 0) {
await redis.expire(key, RUN_TTL_SEC);
}
}
운영 포인트
ttl이-1이면 만료가 없는 상태이므로 반드시expire를 걸어줍니다.- 대량 이벤트라면 List 대신 Stream을 고려하세요. 다만 어떤 자료구조든 핵심은 “런 단위 키에 TTL을 건다”입니다.
Redis가 느려지거나 타임아웃이 날 때
EKS 등 환경에서 Pod에서 Redis(ElastiCache)로의 네트워크/커넥션 문제가 섞이면, 메모리 정책이 있어도 시스템이 흔들립니다. 특히 “10분 타임아웃” 같은 증상이 보이면 VPC 라우팅, 보안그룹, 커넥션 풀, DNS 등을 함께 점검해야 합니다.
관련 점검 체크리스트는 이 글이 도움이 됩니다: EKS Pod→ElastiCache Redis 10분 타임아웃 진단법
요약으로 컨텍스트 해상도 낮추기(그러나 유용성 유지)
TTL은 결국 삭제입니다. 삭제 전에 “정보를 압축한 요약”을 남기면, 비용을 크게 줄이면서도 에이전트 성능을 유지할 수 있습니다.
요약의 핵심: ‘대화 요약’이 아니라 ‘작업 상태 요약’을 남겨라
단순히 대화를 줄이면, 나중에 필요한 결정 근거/해야 할 일/제약 조건이 사라집니다. 에이전트에게 유용한 요약은 보통 다음 필드를 포함합니다.
- 목표/성공 조건
- 현재까지 확정된 사실(근거 포함)
- 남은 할 일과 우선순위
- 실패한 시도와 배운 점(재시도 방지)
- 중요한 출력물 위치(파일/URL/ID)
즉 “스토리”가 아니라 “상태(state)”를 남깁니다.
요약 트리거: 토큰/이벤트 임계치 기반
- 이벤트가 N개 쌓이면 요약
- 최근 K개만 남기고 나머지는 요약으로 흡수
- 프롬프트에 넣는 컨텍스트 토큰이 임계치에 근접하면 요약
예제: 최근 30개만 유지하고 나머지는 요약으로 치환
type RunMemory = {
summary: string; // 누적 요약
recentEvents: string[]; // 최근 이벤트 JSON 문자열
};
function shouldSummarize(recentEventsCount: number) {
return recentEventsCount > 30;
}
export async function compactRunMemory(runId: string, llmSummarize: (input: string) => Promise<string>) {
const eventsKey = `run:${runId}:events`;
const summaryKey = `run:${runId}:summary`;
const events = await redis.lRange(eventsKey, 0, -1);
if (!shouldSummarize(events.length)) return;
const oldSummary = (await redis.get(summaryKey)) ?? "";
// 최근 30개만 남기고 앞부분을 요약 대상으로
const cut = events.length - 30;
const toSummarize = events.slice(0, cut).join("\n");
const keep = events.slice(cut);
const prompt = [
"너는 에이전트 실행 로그를 작업 상태로 요약한다.",
"반드시 아래 항목을 포함해라:",
"- 목표/성공 조건\n- 확정 사실(근거)\n- 남은 할 일(TODO)\n- 실패/주의점\n- 산출물 위치",
"기존 요약:",
oldSummary,
"새 로그:",
toSummarize,
].join("\n\n");
const newSummary = await llmSummarize(prompt);
// 요약 저장, 이벤트는 최근분만 유지
const multi = redis.multi();
multi.set(summaryKey, newSummary);
multi.del(eventsKey);
if (keep.length > 0) multi.rPush(eventsKey, keep);
multi.expire(summaryKey, 60 * 60 * 24 * 7); // 요약은 7일 유지(예시)
multi.expire(eventsKey, 60 * 60 * 6);
await multi.exec();
}
요약 품질을 올리는 팁
- “중요도 기준”을 프롬프트에 명시합니다(결정, 수치, 제약, 실패 원인).
- 요약이 누적되면 드리프트가 생기므로, 일정 주기마다 “전체 재요약”을 수행합니다.
- 요약 결과는 구조화(JSON)도 좋지만, JSON에 부등호가 섞이거나 스키마가 깨지면 운영이 어려울 수 있어 “마크다운 bullet + 섹션 헤더”가 실무적으로 안전한 경우가 많습니다.
요약 호출이 429(레이트리밋)을 유발할 때
요약은 배치로 몰리기 쉬워 API 429를 자주 만듭니다. 재시도·백오프를 표준화해 두면 에이전트가 더 안정적으로 동작합니다.
실전 패턴은 이 글을 참고하세요: OpenAI API 429 재시도·백오프 패턴 실전 가이드
압축으로 Redis 메모리 사용량과 IO 줄이기
요약이 “의미 압축”이라면, 압축은 “바이트 압축”입니다. 특히 웹 페이지 원문, 도구 출력, 에러 스택트레이스처럼 텍스트가 길고 반복이 많은 데이터는 압축 효율이 매우 좋습니다.
무엇을 압축할까
- 원문 HTML/텍스트 본문
- 툴 출력(검색 결과, 크롤링 결과)
- 중간 계획/체인 출력
반대로 이미 짧은 값(수십~수백 바이트)은 압축 오버헤드가 더 클 수 있습니다.
Redis에 압축 저장하는 패턴
Redis는 바이트 배열을 저장할 수 있으므로, 애플리케이션에서 gzip 또는 zstd로 압축해 넣는 방식이 흔합니다.
- gzip: 범용, 구현 쉬움
- zstd: 더 좋은 압축률/속도(환경에 따라 라이브러리 필요)
Node.js 예제: gzip으로 압축해 저장
import { gzipSync, gunzipSync } from "node:zlib";
function toGzipBase64(s: string) {
return gzipSync(Buffer.from(s, "utf-8")).toString("base64");
}
function fromGzipBase64(b64: string) {
return gunzipSync(Buffer.from(b64, "base64")).toString("utf-8");
}
export async function setCompressed(key: string, value: string, ttlSec: number) {
const gz = toGzipBase64(value);
await redis.set(key, gz, { EX: ttlSec });
}
export async function getCompressed(key: string) {
const gz = await redis.get(key);
if (!gz) return null;
return fromGzipBase64(gz);
}
주의사항
- Base64는 약
33%정도 부피가 늘 수 있습니다. 그럼에도 긴 텍스트는 gzip 이득이 더 큽니다. - 가능하면 Redis 클라이언트가 바이너리를 직접 다룰 수 있게 하고 Base64를 피하는 편이 더 좋습니다(언어/라이브러리 제약에 따라 선택).
압축 + TTL 조합이 강력한 이유
- TTL이 없으면 압축해도 결국 쌓입니다.
- TTL만 있으면 “그 기간 동안”은 여전히 비쌉니다.
따라서 긴 원문은 압축 + 짧은 TTL, 요약은 비압축 또는 약한 압축 + 긴 TTL 같은 식으로 계층화하면 비용 대비 효율이 좋습니다.
운영 설계: 메모리 정책을 ‘규칙’으로 만들기
권장 메모리 계층(예시)
- L0: 최근 대화/최근 이벤트 20~50개, TTL 6시간
- L1: 런 요약(상태 요약), TTL 7~30일
- L2: 장기 지식(결정/사실), TTL 90일 또는 영구(단, 접근 로그 기반 정리)
- L3: 원문/첨부/크롤링 결과, gzip + TTL 1~24시간
삭제(만료)로 인한 품질 저하를 막는 장치
- 요약 생성 성공 후에만 원문 삭제
- 요약 실패 시에는 TTL만 걸고 다음 기회에 재요약
- “최소 보존 기간”을 둬서 디버깅 가능성 확보(예: 최근 1시간은 원문 유지)
관측(Observability): 폭주를 수치로 잡아라
다음 지표를 대시보드로 만들어 두면, 폭주를 조기에 감지할 수 있습니다.
- run별 Redis 키 개수, 총 바이트
- run별 이벤트 수, 평균 이벤트 크기
- 요약 호출 횟수/실패율/지연
- 프롬프트 토큰 사용량(입력/출력) 추이
또한 Redis maxmemory-policy를 설정하지 않으면, TTL이 있어도 순간 폭주에서 장애가 날 수 있습니다. 운영 Redis라면 메모리 상한과 eviction 정책을 반드시 검토하세요.
자주 하는 실수 7가지
- TTL을 “일부 키”에만 걸고, 핵심 로그 키는 무기한 방치
- 요약을 만들고도 원문을 계속 유지(이중 저장)
- 요약 프롬프트가 ‘대화 요약’이라 작업 상태가 사라짐
- 요약 호출이 몰리며 429가 폭증, 요약 파이프라인이 멈춤
- 압축을 모든 값에 적용해 CPU만 낭비
- 슬라이딩 TTL만 사용해 핫키가 영구화
- 디버깅을 위해 TTL을 꺼둔 채 그대로 운영 반영
결론: “기억을 잘하는 에이전트”는 “잊는 정책”이 있다
AutoGPT 메모리 폭주는 단순 버그가 아니라, 장기 실행 에이전트의 구조적 문제입니다. 해결의 핵심은 다음 한 줄로 요약됩니다.
- TTL로 수명을 강제하고, 요약으로 의미를 유지하며, 압축으로 비용을 줄인다.
이 3가지를 함께 적용하면 토큰 비용과 Redis 메모리 사용량이 안정화되고, 에이전트의 품질도 오히려 좋아지는 경우가 많습니다(불필요한 노이즈가 줄어들기 때문).
다음 단계로는 “요약을 벡터화해 검색”하거나, 장기 지식을 별도 스토리지로 분리하는 아키텍처까지 확장할 수 있습니다. 다만 그 전에, 이 글의 TTL·요약·압축 3종 세트를 먼저 적용해 폭주를 멈추는 것이 가장 높은 ROI를 제공합니다.