- Published on
AutoGPT 메모리 폭주 해결 - Redis TTL·요약 압축
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
AutoGPT류 에이전트(자율 실행 루프, 툴 호출, 장기 메모리 저장)는 잘 돌아가기 시작하면 메모리(=대화/관찰/툴 결과/임베딩/요약 로그)가 기하급수적으로 커지는 문제가 빠르게 드러납니다. 특히 다음 조건이 겹치면 폭주는 거의 필연입니다.
- 툴 호출 결과가 길다(웹 페이지, 로그, JSON 덩어리)
- 매 스텝마다 "기억"을 저장한다
- 검색 기반 리콜(벡터DB, Redis, 파일)에서 TTL 없이 누적한다
- 요약 없이 원문을 계속 붙잡는다
이 글에서는 운영에서 가장 효과가 좋았던 두 축, Redis TTL로 자동 정리하고 요약 압축으로 정보 밀도만 남기는 방법을 하나의 파이프라인으로 묶어 설명합니다.
아울러 LLM 호출이 늘면 타임아웃/재시도 정책까지 같이 손봐야 합니다. 관련해서는 OpenAI Responses API 408 타임아웃 재현과 해결 실전 가이드도 함께 참고하면 좋습니다.
메모리 폭주의 전형적인 원인 4가지
1) 키 설계가 "무한 append"에 가깝다
예: memory:{agent_id} 리스트에 계속 RPUSH만 하는 방식. 삭제 정책이 없다면 결국 메모리와 디스크(혹은 Redis 메모리)가 함께 증가합니다.
2) 장기 메모리와 작업 캐시의 경계가 없다
- 장기 메모리: 며칠~몇 달 보관, 요약/정제된 지식
- 작업 캐시: 현재 작업 중 임시 관찰/중간 산출물, 수분~수시간
이 둘이 섞이면 임시 로그가 장기 보관으로 승격되며 폭주가 시작됩니다.
3) 동일 정보가 여러 형태로 중복 저장된다
- 원문
- 청크
- 임베딩
- 요약
- 툴 로그
중복 자체는 필요하지만 수명(TTL)과 계층(핫/웜/콜드) 이 없으면 중복은 곧 폭발입니다.
4) 검색 리콜이 "장기"를 계속 불러오면서 컨텍스트가 비대해진다
컨텍스트가 커지면 토큰 비용뿐 아니라, 에이전트가 또 더 많은 중간 로그를 남기고, 그 로그가 다시 저장되는 악순환이 생깁니다.
해결 전략 개요: TTL로 누수 막고, 요약으로 밀도 높이기
권장 아키텍처는 다음 2단계로 단순화할 수 있습니다.
- Redis TTL로 작업 캐시를 자동 만료
- 요약 압축으로 장기 메모리의 크기를 제한
핵심은 "저장"을 하나의 동작으로 보지 않고, 저장 계층을 분리하는 것입니다.
ephemeral:*: 작업 캐시(툴 결과, 관찰, 중간 reasoning 메타) — TTL 필수memory:*: 장기 메모리(요약/정제/태깅된 지식) — TTL 선택(또는 주기적 압축)index:*: 검색 인덱스(벡터, 역색인) — 재구축 가능하도록 설계
Redis TTL 설계: 무엇에 TTL을 걸 것인가
TTL을 걸어야 하는 데이터
- 툴 호출 원문 결과(웹 페이지, DB 덤프, 로그)
- 스텝별 관찰(observation) 원문
- 임시 청크(요약 전 분할 텍스트)
- 재시도/디버그 로그
TTL을 신중히 선택할 데이터
- 사용자 프로필/선호(장기)
- 프로젝트별 결정 사항(장기)
- 검증된 지식(장기)
추천 TTL 기준(운영 감각)
- 작업 캐시:
30m~6h - 세션 메모리(대화 컨텍스트 보조):
1d~7d - 장기 메모리: TTL 없음 + 주기 압축, 또는
30d+ 접근 시 연장(sliding)
Redis 키/데이터 모델 예시
아래는 "세션별 작업 캐시"와 "프로젝트별 장기 메모리"를 분리한 예시입니다.
ephemeral:{agent}:{session}:tool:{call_id}: 툴 결과 원문(JSON/text)ephemeral:{agent}:{session}:events: 스텝 이벤트 스트림(리스트)memory:{agent}:{project}:facts: 장기 fact(해시/JSON)memory:{agent}:{project}:summary:{topic}: 토픽 요약
Python 예시: TTL 포함 저장
import json
import time
import redis
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
AGENT = "autogpt"
SESSION = "s_20260226_001"
def save_tool_result(call_id: str, payload: dict, ttl_seconds: int = 3600):
key = f"ephemeral:{AGENT}:{SESSION}:tool:{call_id}"
r.set(key, json.dumps(payload, ensure_ascii=False))
r.expire(key, ttl_seconds)
def append_event(event: dict, ttl_seconds: int = 6 * 3600):
key = f"ephemeral:{AGENT}:{SESSION}:events"
r.rpush(key, json.dumps(event, ensure_ascii=False))
# 리스트 자체에도 TTL을 걸어 세션 종료 후 자동 정리
r.expire(key, ttl_seconds)
# 사용 예
save_tool_result("call_17", {"stdout": "...very long log...", "ts": time.time()}, ttl_seconds=1800)
append_event({"type": "observation", "text": "page fetched", "ts": time.time()})
포인트는 단순합니다.
- 원문은 ephemeral로만 저장하고
- 세션이 끝나면 TTL로 자연 소멸
장기 보관이 필요하면, 아래 요약 압축 단계에서 memory:*로 승격시키면 됩니다.
TTL만으로 부족한 이유: 장기 메모리의 "정보 비만"
TTL은 누수를 막아주지만, 장기 메모리는 여전히 커질 수 있습니다.
- 매 작업마다 요약을 누적
- 토픽이 늘어남
- 요약이 요약을 낳으며 중복/모순이 쌓임
그래서 장기 메모리는 "더 오래"가 아니라 "더 작고 정확"해야 합니다. 여기서 요약 압축(summarization compression) 이 필요합니다.
요약 압축 전략: 언제, 무엇을, 어떤 형태로 압축할까
1) 트리거(언제 요약할까)
다음 중 하나로 트리거를 거는 방식이 운영에서 안정적입니다.
- 이벤트 수 기준:
events길이가 N을 넘으면 압축 - 토큰 추정치 기준: 최근 원문 합산이 특정 토큰을 넘으면 압축
- 시간 기준: 세션 시작 후 10분마다
- 비용 기준: LLM 호출량이 임계치를 넘으면 강제 압축
2) 입력 선택(무엇을 요약할까)
요약에 원문 전체를 다 넣으면 비용이 폭증합니다. 다음처럼 "정보 가치" 기준으로 추립니다.
- 성공한 툴 호출 결과만
- 오류/재시도 로그는 샘플링
- 동일 도메인 페이지는 대표 1개만
- 최종 결정/액션/리스크/가정만 남기기
3) 출력 스키마(어떤 형태로 저장할까)
자연어 요약만 저장하면 나중에 검색/정합성 검증이 어렵습니다. 구조화된 요약을 권장합니다.
facts: 변하지 않는 사실decisions: 결정과 근거open_questions: 미해결 질문todos: 다음 행동sources: 어떤 툴 결과에서 왔는지 포인터
요약 압축 파이프라인 예시
단계 A: ephemeral 이벤트를 모아 요약 입력 만들기
import json
def load_recent_events(limit: int = 50):
key = f"ephemeral:{AGENT}:{SESSION}:events"
raw = r.lrange(key, max(0, r.llen(key) - limit), -1)
return [json.loads(x) for x in raw]
def build_summarize_input(events):
# 관찰/결정/에러를 간단히 분리
lines = []
for e in events:
t = e.get("type")
if t in ("observation", "decision", "action", "error"):
text = e.get("text", "")
lines.append(f"[{t}] {text}")
return "\n".join(lines)
단계 B: LLM으로 구조화 요약 생성
MDX 환경에서 부등호는 인라인 코드 처리해야 하므로, 아래 프롬프트에서 JSON 스키마 표기에도 부등호를 쓰지 않습니다.
SUMMARY_PROMPT = """
너는 에이전트 메모리 압축기다.
아래 로그를 읽고, 중복을 제거하고, 사실/결정/할일/미해결 질문을 구조화해라.
출력은 반드시 JSON 한 덩어리로만.
키: facts, decisions, todos, open_questions, sources
각 값은 문자열 배열.
로그:
"""
def summarize_with_llm(client, text: str) -> dict:
# 예시: OpenAI Responses API 스타일의 가상 코드
resp = client.responses.create(
model="gpt-4.1-mini",
input=SUMMARY_PROMPT + text,
temperature=0.2,
)
# 응답에서 JSON 파싱(실서비스에서는 robust parsing 필요)
return json.loads(resp.output_text)
LLM 호출이 잦아지면 타임아웃/재시도/서킷 브레이커가 중요해집니다. 이 부분은 OpenAI Responses API 408 타임아웃 재현과 해결 실전 가이드에서 운영 패턴을 더 자세히 다룹니다.
단계 C: 장기 메모리에 업서트(upsert)하고, 원문은 TTL에 맡기기
import hashlib
PROJECT = "p_docs_migration"
def upsert_long_term_summary(topic: str, summary: dict):
# 토픽별 요약은 덮어쓰되, 변경 이력을 짧게 유지하는 방식도 가능
key = f"memory:{AGENT}:{PROJECT}:summary:{topic}"
r.set(key, json.dumps(summary, ensure_ascii=False))
def add_facts(summary: dict):
# fact는 content-addressed로 중복 방지
for fact in summary.get("facts", []):
fid = hashlib.sha1(fact.encode("utf-8")).hexdigest()
key = f"memory:{AGENT}:{PROJECT}:fact:{fid}"
r.setnx(key, fact)
def compress_cycle(client, topic: str = "session"):
events = load_recent_events(limit=80)
text = build_summarize_input(events)
if not text.strip():
return
summary = summarize_with_llm(client, text)
upsert_long_term_summary(topic, summary)
add_facts(summary)
# ephemeral은 expire로 자연 정리(여기서 굳이 삭제하지 않음)
여기서 중요한 운영적 이점은 다음입니다.
- 장기 메모리는 요약/팩트만 남아 크기가 제한됨
- 원문은 TTL로 사라지므로 Redis 메모리 상한을 잡기 쉬움
- fact를 해시 키로 두면 중복 저장이 줄어듦
Redis 메모리 폭주를 막는 추가 안전장치
1) Maxmemory 정책 설정
Redis는 maxmemory와 maxmemory-policy로 최악의 상황을 제어할 수 있습니다.
- 캐시 성격이면
allkeys-lru또는volatile-ttl - TTL을 잘 걸어두었다면
volatile-ttl이 직관적
설정 예시(개념):
maxmemory 4gb
maxmemory-policy volatile-ttl
2) 키스페이스 이벤트로 만료 관측
만료가 제대로 일어나는지 관측하지 않으면 TTL은 "있지만 없는" 상태가 됩니다.
expired이벤트를 구독해 만료량 추적- 세션별 키 수가 비정상적으로 늘면 알림
3) 대형 값 저장 금지(가드레일)
툴 결과가 너무 크면 저장 자체를 막고, 대신 포인터만 저장합니다.
- 값 크기 상한: 예를 들어 256KB
- 초과 시: 파일 스토리지(S3 등)에 저장하고 Redis에는 URL과 해시만
이 패턴은 모델 OOM을 다룰 때의 "KV 캐시 상한"과 유사한 사고방식입니다. 로컬 LLM 메모리 최적화 관점은 Transformers 로컬 LLM OOM과 KV 캐시 최적화도 참고할 만합니다.
요약 품질을 망치지 않는 압축 규칙
요약 압축은 잘못하면 "중요한 디테일 손실"로 이어집니다. 다음 규칙이 실전에서 도움이 됩니다.
- 결정(decision)은 반드시 근거와 함께 저장
- 숫자/한계/제약은 facts로 승격 (예: rate limit, timeout, 비용 상한)
- 소스 포인터를 남겨 재검증 가능하게
- 예:
sources에ephemeral:*키 또는 외부 저장소 URL - 단, TTL 만료로 소스가 사라질 수 있으니 장기적으로 필요하면 외부 저장소로 승격
- 예:
- 요약의 요약을 만들 때는 diff 기반으로 갱신
- 매번 전체 재요약 대신, 새 이벤트만 요약하고 기존 요약에 병합
운영 체크리스트: "폭주"를 조기에 잡는 지표
- Redis 메모리 사용량(used_memory)과 키 수
ephemeral:*키의 평균 TTL 분포(만료가 밀리면 TTL 설정이 잘못된 것)- 세션당 이벤트 수, 툴 결과 평균 크기
- 요약 호출 빈도와 토큰 사용량
- 리콜 시 컨텍스트 토큰 크기(에이전트 입력이 계속 커지는지)
추가로, Redis를 운영에서 강하게 쓰다 보면 락/경합 이슈도 같이 마주치곤 합니다. 분산 락이 필요해지는 케이스는 MySQL InnoDB 데드락 폭증, Redis 분산락으로 잠재우기에서 다룬 패턴이 그대로 응용됩니다.
결론: "저장"이 아니라 "수명"과 "밀도"를 설계하라
AutoGPT 메모리 폭주는 단순히 Redis 용량을 늘린다고 해결되지 않습니다. 핵심은 두 가지입니다.
- TTL로 작업 캐시의 수명을 강제해 누수를 구조적으로 차단
- 요약 압축으로 장기 메모리의 정보 밀도를 높여 크기 증가를 제한
이 두 축을 분리해 설계하면, 에이전트가 오래 돌수록 더 똑똑해지는 것이 아니라 더 무거워지는 문제를 상당 부분 제거할 수 있습니다.
실전에서는 여기에 더해 (1) 대형 툴 결과의 외부 스토리지 승격, (2) 요약의 diff 병합, (3) 리콜 컨텍스트 상한을 함께 적용하면 "장기 실행 가능한" 에이전트에 가까워집니다.