Published on

AutoGPT 메모리 폭주 해결 - Redis TTL·요약·압축

Authors

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 메모리 사용량을 낮춤

운영 관점에서 가장 안전한 순서는:

  1. TTL로 상한선을 만들고
  2. 요약으로 품질을 유지하며
  3. 압축으로 비용을 줄입니다.

Redis TTL로 메모리 수명 관리하기

어떤 키에 TTL을 걸어야 하나

에이전트 메모리를 보통 3계층으로 나누면 관리가 쉬워집니다.

  1. 세션/런 단위 작업 로그: 반드시 TTL
  • 예: run:{runId}:events, run:{runId}:tool_calls
  1. 중간 산출물 캐시: TTL 권장
  • 예: 웹 페이지 원문, 임시 파일 메타, 임베딩 캐시
  1. 장기 지식(사실/결정/요약): 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가지

  1. TTL을 “일부 키”에만 걸고, 핵심 로그 키는 무기한 방치
  2. 요약을 만들고도 원문을 계속 유지(이중 저장)
  3. 요약 프롬프트가 ‘대화 요약’이라 작업 상태가 사라짐
  4. 요약 호출이 몰리며 429가 폭증, 요약 파이프라인이 멈춤
  5. 압축을 모든 값에 적용해 CPU만 낭비
  6. 슬라이딩 TTL만 사용해 핫키가 영구화
  7. 디버깅을 위해 TTL을 꺼둔 채 그대로 운영 반영

결론: “기억을 잘하는 에이전트”는 “잊는 정책”이 있다

AutoGPT 메모리 폭주는 단순 버그가 아니라, 장기 실행 에이전트의 구조적 문제입니다. 해결의 핵심은 다음 한 줄로 요약됩니다.

  • TTL로 수명을 강제하고, 요약으로 의미를 유지하며, 압축으로 비용을 줄인다.

이 3가지를 함께 적용하면 토큰 비용과 Redis 메모리 사용량이 안정화되고, 에이전트의 품질도 오히려 좋아지는 경우가 많습니다(불필요한 노이즈가 줄어들기 때문).

다음 단계로는 “요약을 벡터화해 검색”하거나, 장기 지식을 별도 스토리지로 분리하는 아키텍처까지 확장할 수 있습니다. 다만 그 전에, 이 글의 TTL·요약·압축 3종 세트를 먼저 적용해 폭주를 멈추는 것이 가장 높은 ROI를 제공합니다.