- Published on
AutoGPT 메모리 폭주, Redis TTL·요약으로 잡기
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
AutoGPT 류 에이전트를 장시간 돌리다 보면 어느 순간부터 응답 품질은 떨어지고, 비용은 오르고, 프로세스 메모리와 Redis 사용량이 같이 치솟는 현상을 자주 겪습니다. 원인은 단순합니다. 에이전트가 쌓아두는 “작업 메모리(working memory)”와 “대화/관찰 로그”가 무제한으로 누적되는데, 이를 그대로 다음 프롬프트에 계속 넣거나(토큰 폭증), 벡터/키-값 저장소에 계속 적재하기(스토리지 폭증) 때문입니다.
이 글은 AutoGPT 메모리 폭주를 Redis TTL로 자동 만료시키고, 남겨야 할 정보는 요약(압축) 메모리로 치환해 장기 실행을 안정화하는 패턴을 설명합니다. “무조건 저장”이 아니라 저장 수명과 해상도(원문 vs 요약)를 분리하는 것이 핵심입니다.
운영 중 API 호출이 증가하면서 재시도까지 겹치면 폭주가 더 빨라집니다. 재시도 설계는 별도 글인 Claude API 529·429 재시도 전략과 구현 패턴도 함께 참고하면 좋습니다.
왜 AutoGPT 메모리가 폭주하는가
1) “메모리”가 사실상 로그 스트림이기 때문
대부분의 에이전트는 다음을 계속 저장합니다.
- 사용자/에이전트 대화 메시지
- 툴 호출 입력·출력(웹 검색 결과, 파일 내용, DB 결과 등)
- 중간 추론(체인 오브 쏘트는 저장하지 않더라도 관찰/계획 텍스트)
- 작업 상태(플랜, 태스크 큐, 스크래치패드)
문제는 이 데이터가 시간에 따라 단조 증가한다는 점입니다. “최근 N개만 유지” 같은 제한이 없으면, 프롬프트 컨텍스트는 토큰 한도에 부딪히고, 저장소는 비용과 지연이 증가합니다.
2) Redis를 쓰면 더 빨리 ‘보이는’ 문제
Redis는 빠르고 편해서 “일단 다 Redis에 넣자”가 되기 쉽습니다. 하지만 TTL이 없으면 Redis는 사실상 무한 성장하는 메모리 DB가 됩니다. 특히 다음 조합이 위험합니다.
- 세션/런 단위 키가 계속 생성됨
- 툴 출력(HTML, JSON, 문서 텍스트)이 큰 값으로 저장됨
- 리스트/스트림 구조로 append만 수행됨
3) 요약이 없으면 ‘선택적 기억’이 불가능
사람은 모든 대화를 원문으로 기억하지 않습니다. 에이전트도 마찬가지로,
- 단기: 최근 몇 분~몇 시간의 원문(고해상도)
- 장기: 핵심 사실/결정/제약만 요약(저해상도)
이 두 층이 있어야 장기 실행이 가능합니다.
해결 전략 한 장 요약: TTL + 요약 + 예산
- TTL로 자동 삭제: “원문 로그/관찰”은 짧은 TTL로 유지
- 요약으로 압축: 중요한 정보만 장기 메모리로 승격
- 예산(Quota) 강제: 키 개수/바이트/토큰 예산을 넘으면 강제 축소
실무에서는 이 세 가지를 같이 써야 합니다. TTL만 쓰면 중요한 정보도 날아가고, 요약만 쓰면 요약 대상 원문이 무한히 쌓입니다.
Redis 키 설계: 세션, 원문, 요약을 분리
권장 키 네임스페이스
agent:run:{run_id}:events: 이벤트 로그(관찰/툴 출력 등)agent:run:{run_id}:messages: 대화 메시지agent:run:{run_id}:summary:v1: 누적 요약(장기)agent:run:{run_id}:facts: 구조화된 사실(선택)
여기서 중요한 건 원문과 요약을 절대 같은 컬렉션에 섞지 않는 것입니다. 섞이면 TTL을 걸기 어렵고, retrieval 시에도 불필요한 원문이 섞여 토큰이 다시 폭주합니다.
TTL 정책 예시
events,messages: TTL 6시간~24시간summary: TTL 7일~30일 또는 무제한(대신 크기 제한)facts: TTL 길게(단, 업데이트 정책 필요)
“사용자가 다시 돌아올 가능성”과 “런타임 디버깅 필요 기간”을 기준으로 원문 TTL을 정합니다.
Redis TTL 적용: 원문은 자동 만료시키기
다음은 Python redis 클라이언트로 리스트/스트림에 이벤트를 추가하고 TTL을 보장하는 예시입니다.
import json
import time
import redis
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
RUN_ID = "run_123"
KEY_EVENTS = f"agent:run:{RUN_ID}:events"
TTL_SECONDS = 60 * 60 * 12 # 12h
def append_event(event: dict):
payload = {
"ts": int(time.time()),
"type": event.get("type", "unknown"),
"data": event.get("data", {}),
}
# LPUSH/RPUSH 어느 쪽이든 상관없지만, trim과 같이 쓰는 게 중요
pipe = r.pipeline()
pipe.rpush(KEY_EVENTS, json.dumps(payload))
# 이벤트가 늘어날수록 메모리 폭주하므로 길이 제한도 함께
pipe.ltrim(KEY_EVENTS, -2000, -1) # 최근 2000개만 유지
# TTL은 매번 재설정해도 되고, 최초 생성 시에만 설정해도 됨
pipe.expire(KEY_EVENTS, TTL_SECONDS)
pipe.execute()
append_event({"type": "tool_output", "data": {"text": "..."}})
포인트는 두 가지입니다.
EXPIRE만 걸면 “키는 남는데 값이 너무 커짐” 문제가 생길 수 있습니다. 그래서LTRIM같은 길이 제한을 같이 둡니다.- TTL을 “키 생성 시 1회만” 설정하려면
SET계열에서EX옵션을 쓰거나,EXPIRE를NX조건으로 걸고 싶을 수 있습니다. Redis 7+라면EXPIRE key seconds NX같은 옵션을 사용할 수 있습니다. (클라이언트 지원 여부 확인)
요약(압축) 메모리: 원문을 줄이고 의미를 남기기
요약을 언제 수행할까
요약 트리거는 보통 세 가지 중 하나로 잡습니다.
- 이벤트/메시지 개수 기준: 최근 200개를 넘으면 요약
- 토큰 예산 기준: 컨텍스트 토큰이
budget의 70%를 넘으면 요약 - 시간 기준: 10분마다 배치 요약
실전에서는 토큰 예산 기준이 가장 안전합니다. “메모리 폭주”의 체감 문제는 대부분 토큰 폭주로 먼저 나타나기 때문입니다.
요약의 출력 형태
요약은 자유 텍스트보다 다음 형태가 운영에 유리합니다.
- 결정 사항: 무엇을 하기로 했는가
- 제약/요구사항: 반드시 지켜야 하는 규칙
- 진행 상태: 완료/진행/대기
- 실패 원인: 어떤 시도가 왜 실패했는가
- 다음 액션: 우선순위 높은 다음 단계
이렇게 만들면 retrieval 시에도 “요약만 넣어도” 에이전트가 맥락을 빠르게 복원합니다.
누적 요약 패턴(rolling summary)
요약을 매번 새로 만들면 비용이 큽니다. 대신 기존 요약에 새 구간 요약을 병합하는 누적 요약이 효과적입니다.
summary_old+recent_chunk→summary_new
이를 통해 요약 길이를 일정하게 유지할 수 있습니다.
요약 파이프라인 구현 예시(Python)
아래 예시는
- 최근 이벤트 일부를 가져와
- LLM으로 “구간 요약”을 만들고
- 기존 누적 요약과 병합해
- Redis에 저장
하는 흐름입니다. (LLM 호출 함수는 의사 코드)
import json
import redis
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
def llm_summarize(prompt: str) -> str:
# 실제로는 OpenAI/Claude 등 호출
# 여기서는 예시로만 둠
raise NotImplementedError
def get_recent_events(run_id: str, n: int = 200) -> list[dict]:
key = f"agent:run:{run_id}:events"
raw = r.lrange(key, -n, -1)
return [json.loads(x) for x in raw]
def get_summary(run_id: str) -> str:
key = f"agent:run:{run_id}:summary:v1"
return r.get(key) or ""
def set_summary(run_id: str, summary: str, ttl_seconds: int = 60 * 60 * 24 * 14):
key = f"agent:run:{run_id}:summary:v1"
# 요약은 길게 보관하되, 무한 보관이 부담이면 TTL을 길게
r.set(key, summary, ex=ttl_seconds)
def summarize_run(run_id: str):
events = get_recent_events(run_id, n=200)
summary_old = get_summary(run_id)
# 툴 출력이 너무 길면 요약 전에 1차로 자르거나 필터링
compact_lines = []
for e in events:
t = e.get("type")
data = e.get("data", {})
text = data.get("text") or data.get("content") or ""
if len(text) > 800:
text = text[:800] + "..."
compact_lines.append(f"- {t}: {text}")
chunk = "\n".join(compact_lines)
prompt = (
"You are a memory compressor for an autonomous agent.\n"
"Update the running summary with new events.\n\n"
"Rules:\n"
"- Keep it under 2500 characters.\n"
"- Preserve decisions, constraints, failures, and next actions.\n"
"- Remove redundant details.\n\n"
f"Current summary:\n{summary_old}\n\n"
f"New events:\n{chunk}\n\n"
"Return the updated summary only."
)
summary_new = llm_summarize(prompt)
set_summary(run_id, summary_new)
return summary_new
운영 팁:
- 요약 프롬프트에 “문자 수 제한”을 강제하면 저장소/토큰 예산을 안정적으로 지킬 수 있습니다.
- 이벤트 텍스트는 1차로 잘라서 LLM 입력 비용을 줄입니다.
- 요약이 실패하면(LLM 오류) 재시도 폭주가 날 수 있으니, 재시도는 지수 백오프와 상한을 둡니다. 재시도 설계는 Python 데코레이터로 async 타임아웃·재시도 구현도 참고할 만합니다.
TTL만으로 부족한 이유: “중요 정보”가 같이 날아간다
TTL을 짧게 걸면 원문이 사라져 디버깅이 어려워질 수 있습니다. 그래서 다음 중 하나를 추가합니다.
- 요약을 장기 보관: 원문은 삭제돼도 요약은 남음
- 구조화된 사실 저장: 예를 들어 목표 URL, 계정/환경, 완료된 태스크 ID 같은 핵심만 별도 키에 저장
- 샘플링 보관: 모든 이벤트가 아니라 오류/실패 이벤트만 길게 보관
특히 “실패 이벤트만 장기 보관”은 비용 대비 효율이 좋습니다.
메모리 예산 모델: 토큰/바이트/키 개수 3종
AutoGPT 메모리 폭주는 한 가지 지표만 보면 놓치기 쉽습니다.
- 토큰 예산: 프롬프트에 넣는 컨텍스트 크기
- 바이트 예산: Redis
used_memory, 키 별 value 크기 - 키 개수 예산: run_id가 계속 늘며 키가 폭증
권장하는 최소 관측 지표는 다음입니다.
used_memory,mem_fragmentation_ratiokeyspace_hits,keyspace_missesexpired_keys,evicted_keys- run_id 당 평균 키 수, 평균 value 크기
evicted_keys가 증가한다면 TTL이 아니라 maxmemory 정책에 의한 강제 퇴출이 발생 중일 수 있습니다. 이 경우 “중요 요약 키”도 날아갈 수 있으니, Redis maxmemory 정책과 분리(별도 Redis 인스턴스 또는 DB 번호 분리)를 고려하세요.
Redis 운영 설정 체크리스트
1) maxmemory와 eviction policy
maxmemory를 명시적으로 설정maxmemory-policy는 워크로드에 맞게 선택- 캐시 성격이면
allkeys-lru고려 - TTL 기반이면
volatile-ttl또는volatile-lru고려
- 캐시 성격이면
요약 키를 반드시 지켜야 한다면, 요약은 Redis가 아니라 영속 DB(Postgres 등)에 두고 Redis는 캐시로만 쓰는 것도 방법입니다.
2) 큰 값 저장 금지(툴 출력 필터링)
가장 흔한 폭주 원인은 “웹 페이지 전체 HTML” 같은 대형 텍스트입니다.
- 원문은 파일/오브젝트 스토리지로 보내고 Redis에는 포인터만 저장
- Redis에는
content_hash와 요약만 저장
3) 세션 종료 시 정리
런이 끝났는데 키가 남아 있다면 TTL이 길거나, 일부 키에 TTL이 누락된 겁니다.
agent:run:{run_id}:*를 스캔해서 TTL 누락 키를 탐지- 종료 훅에서
UNLINK로 비동기 삭제(대형 키 삭제 시 지연 완화)
UNLINK는 대형 키 삭제로 메인 스레드가 멈추는 현상을 줄여줍니다.
Retrieval 단계에서의 토큰 폭주 방지
저장만 잘해도, retrieval에서 “원문을 몽땅 끌어오면” 다시 폭주합니다. 다음 우선순위를 추천합니다.
- 누적 요약(
summary)을 기본 컨텍스트로 사용 - 최근 메시지 N개만 추가
- 필요할 때만 이벤트 원문 일부를 추가(예: 마지막 실패 로그)
벡터 검색을 쓰는 경우에도 “요약 임베딩”과 “원문 임베딩”을 분리하면 비용과 품질이 안정됩니다. 하이브리드 검색 튜닝은 Pinecone·Milvus 하이브리드검색 튜닝 - RRF+MMR에서 RRF, MMR로 중복을 줄이는 접근이 도움이 됩니다.
실전 적용 순서(가장 효과 큰 것부터)
- 원문 키에 TTL을 걸고, 길이 제한(
LTRIM)을 추가 - 요약 키를 분리하고, 누적 요약(rolling) 파이프라인 구축
- 툴 출력 필터링(대형 텍스트 저장 금지, 포인터화)
- 관측 지표 대시보드화(
expired_keys,evicted_keys, run_id 당 메모리) - retrieval에서 요약 우선 정책 적용(원문은 필요할 때만)
이 순서대로 하면 “Redis 메모리 폭주”와 “컨텍스트 토큰 폭주”를 동시에 잡을 수 있습니다.
마무리: 기억은 쌓는 게 아니라 ‘관리’하는 것
AutoGPT의 메모리 문제는 LLM 성능 문제가 아니라 데이터 수명 관리 문제인 경우가 대부분입니다. Redis TTL로 원문을 자동 만료시키고, 남겨야 할 정보는 요약으로 승격해 해상도를 낮춰 저장하면 장기 실행이 가능해집니다.
정리하면 다음 한 줄입니다.
- 원문은 짧게, 요약은 길게, retrieval은 최소로
이 원칙을 지키면 에이전트가 오래 일해도 비용과 지연이 선형에 가깝게 유지됩니다.