- Published on
AutoGPT 메모리 폭주? 벡터DB TTL로 안정화
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
AutoGPT 류의 에이전트는 실행 시간이 길어질수록 “기억”이 계속 쌓입니다. 초반에는 유용하지만, 일정 시점부터는 벡터DB(또는 임베딩 스토어)에 문서가 과도하게 누적되며 다음 문제가 동시에 발생합니다.
- 프로세스 메모리(OOM) 또는 컨테이너 메모리 제한 초과
- 벡터 검색 지연 증가로 에이전트 루프가 느려짐
- 임베딩 저장 비용/스토리지 비용 증가
- 오래된 기억이 최신 의사결정을 오염(낡은 컨텍스트가 상위로 검색)
이 글에서는 “메모리 폭주”를 단순히 RAM 문제로 보지 않고, 벡터DB에 누적되는 장기 메모리의 수명 주기 관리 문제로 보고 TTL(time-to-live, 만료) 기반으로 안정화하는 방법을 정리합니다. 핵심은 기억을 영구 자산이 아니라 캐시로 취급하고, 세션 단위로 수명과 우선순위를 부여하는 것입니다.
AutoGPT에서 메모리가 폭주하는 구조
대부분의 에이전트 프레임워크는 다음 파이프라인을 반복합니다.
- 관찰(Observation)과 툴 결과를 텍스트로 축적
- 임베딩 생성
- 벡터DB에 upsert
- 다음 스텝에서 유사도 검색(top-k)으로 관련 기억을 가져와 프롬프트에 주입
문제는 2~4가 “무한 반복”에 가깝게 돌아가는데, 삭제/만료가 기본값으로 제공되지 않거나, 제공되더라도 애플리케이션 레벨에서 설계를 안 하면 사실상 영구 저장이 됩니다.
특히 다음 패턴에서 폭주가 빨라집니다.
- 툴 호출 결과(HTML, 로그, CSV 일부 등)가 길고 빈번함
- 한 작업이 끝나도 동일한 컬렉션/네임스페이스에 계속 적재
top-k를 크게 잡아 프롬프트 주입량이 증가(토큰 비용도 동반 증가)
TTL로 해결하는 핵심 아이디어
TTL은 “이 메모리는 언제까지 유효한가?”를 데이터 레코드에 명시하고, 유효기간이 지나면 자동으로 제거하거나(가능하면 DB 레벨), 제거 대상에서 제외하는(애플리케이션 레벨) 전략입니다.
여기서 중요한 점은 TTL을 단일 값으로 두지 말고, 메모리 종류별로 다르게 두는 것입니다.
- 에피소드 메모리(세션/작업 단위): 짧은 TTL (예: 1시간~24시간)
- 프로젝트 메모리(반복적으로 유용한 지식): 긴 TTL (예: 30일~180일)
- 영구 지식(정적 문서/규칙): TTL 없음 또는 매우 긴 TTL
즉, “모든 걸 영구 저장”이 아니라 “기본은 만료, 예외만 보존”이 폭주를 막는 현실적인 기본값입니다.
설계 1: 세션 스코핑 + TTL(가장 효과적)
먼저 메모리를 session_id 또는 run_id로 강하게 구분하세요. AutoGPT가 장시간 돌거나 여러 작업을 병렬로 수행할 때, 세션 스코핑이 없으면 기억이 서로 오염됩니다.
메타데이터 예시
session_id: 에이전트 실행 단위memory_type:episodic/project/permanentcreated_at: 생성 시각expires_at: 만료 시각(TTL)importance: 중요도(가중치)
이렇게 해두면 검색 시에도 session_id와 expires_at로 필터링이 가능해집니다.
파이썬: TTL 필드 포함해 upsert
아래 예시는 벡터DB가 메타데이터를 지원한다는 가정(대부분 지원)에서, 만료 시각을 명시해 적재하는 패턴입니다.
from datetime import datetime, timedelta, timezone
def build_memory(doc: str, session_id: str, memory_type: str):
now = datetime.now(timezone.utc)
if memory_type == "episodic":
ttl = timedelta(hours=6)
elif memory_type == "project":
ttl = timedelta(days=30)
else:
ttl = None
meta = {
"session_id": session_id,
"memory_type": memory_type,
"created_at": now.isoformat(),
}
if ttl is not None:
meta["expires_at"] = (now + ttl).isoformat()
return {"text": doc, "metadata": meta}
# pseudo code
# embedding = embed(doc)
# vectordb.upsert(id=..., vector=embedding, metadata=meta)
검색 시 만료 필터 적용
벡터 검색은 유사도만으로 끝내면 안 됩니다. “유효한 기억만” 후보로 올려야 합니다.
from datetime import datetime, timezone
now = datetime.now(timezone.utc).isoformat()
# pseudo code: vector_db.query(..., filter={...})
filter_expr = {
"session_id": session_id,
# expires_at이 없으면(permanent) 통과, 있으면 now 이후인 것만 통과
"$or": [
{"expires_at": {"$exists": False}},
{"expires_at": {"$gt": now}},
]
}
# results = vectordb.query(vector=q_embedding, top_k=20, filter=filter_expr)
이 한 가지 조치만으로도 “오래된 기억이 계속 검색되어 프롬프트를 오염시키는 문제”와 “DB 크기 폭주”를 동시에 완화할 수 있습니다.
설계 2: TTL만으로 부족할 때, 요약 메모리로 승격
TTL을 짧게 잡으면 안정화는 되지만, 중요한 정보까지 사라질 수 있습니다. 그래서 만료 전에 요약본을 만들어 장기 메모리로 승격시키는 계층을 둡니다.
- 원문(에피소드 메모리): TTL 6시간
- 요약(프로젝트 메모리): TTL 30일
- 규칙/결론(영구 메모리): TTL 없음
승격 파이프라인 예시
- 에피소드 메모리 중
importance가 높거나, 자주 검색되는 문서(조회수 기반)를 선정 - LLM으로 요약(핵심 사실/결론/결정만)
- 요약본을 새로운 레코드로 저장하고, 원문은 그대로 만료
def should_promote(meta: dict) -> bool:
return meta.get("importance", 0) >= 0.8 or meta.get("hit_count", 0) >= 5
# pseudo code
# for mem in episodic_memories_expiring_soon:
# if should_promote(mem.metadata):
# summary = llm_summarize(mem.text)
# store(summary, memory_type="project", ttl_days=30)
이 방식은 “장기적으로 유용한 지식만 남기고, 나머지는 흘려보내는” 캐시 전략과 유사합니다.
설계 3: 삭제가 아니라 ‘컴팩션’(중복 제거)도 필요
메모리가 폭주하는 또 다른 이유는 중복입니다.
- 같은 URL을 여러 번 크롤링한 결과
- 동일한 툴 에러 로그가 반복 저장
- 계획(Plan)이 스텝마다 조금씩 바뀌며 누적
TTL로도 결국 TTL 기간 동안은 중복이 쌓입니다. 그래서 다음 중 하나를 권합니다.
content_hash(예:sha256)를 메타데이터로 저장하고 중복 upsert 방지- 유사도 기반 중복 제거(새 문서가 기존 문서와 코사인 유사도
0.98이상이면 저장하지 않음)
import hashlib
def content_hash(text: str) -> str:
return hashlib.sha256(text.encode("utf-8")).hexdigest()
# pseudo code
# if vectordb.exists(filter={"content_hash": h, "session_id": session_id}):
# skip
# else:
# upsert
설계 4: 벡터DB TTL 지원 여부에 따른 구현 전략
벡터DB마다 TTL을 “진짜로” 지원하는 방식이 다릅니다.
- DB 레벨 TTL 인덱스/정책 지원: 만료 데이터가 자동 삭제(가장 이상적)
- 메타데이터 필터만 제공: 검색에서 제외는 가능하지만, 물리 삭제는 별도 배치 작업 필요
DB 레벨 TTL이 없다면 다음 2단계가 현실적입니다.
- 쿼리 시
expires_at필터로 논리적 만료 적용 - 배치 잡(크론)으로 물리 삭제 수행
배치 삭제(크론) 의사 코드
from datetime import datetime, timezone
def purge_expired(vectordb, batch_size: int = 1000):
now = datetime.now(timezone.utc).isoformat()
# pseudo: 만료된 id 목록을 페이지네이션으로 조회
while True:
expired = vectordb.list(
filter={"expires_at": {"$lte": now}},
limit=batch_size,
)
if not expired:
break
ids = [row["id"] for row in expired]
vectordb.delete(ids=ids)
만약 메타데이터 조회가 느리다면, 만료 인덱스를 별도 저장소(예: Redis sorted set, RDB 테이블)로 두고 “삭제 대상 id만 빠르게 뽑아” 벡터DB에서 삭제하는 방식도 고려할 수 있습니다.
성능 이슈: TTL 도입 후 쿼리/인덱스도 같이 봐야 함
TTL을 붙이면 필터 조건이 늘어나고, 메타데이터 기반 조회가 많아집니다. 이때 느려지는 지점은 보통 “유사도 검색”이 아니라 필터링과 후보군 스캔입니다.
session_id,expires_at,memory_type는 사실상 필수 필터 키- 해당 필드에 대한 인덱싱/파티셔닝 전략이 없으면, TTL이 오히려 검색을 느리게 만들 수 있음
RDB나 MongoDB를 메타데이터 스토어로 함께 쓴다면, 만료/세션 필터 쿼리를 explain으로 확인하고 인덱스를 잡는 것이 안전합니다. 이 주제는 MongoDB 느린 쿼리 - explain으로 원인 찾고 인덱스 튜닝에서 접근 방법을 참고할 수 있습니다.
운영 관점: 메모리 폭주를 “지표”로 조기 감지하기
TTL을 넣어도 운영 중에는 설정 실수나 예외 케이스로 다시 폭주할 수 있습니다. 다음 지표를 최소한으로 권합니다.
- 컬렉션(또는 네임스페이스)별 문서 수, 총 벡터 수
memory_type별 저장 비율(에피소드가 과도하게 높은지)- 세션별 저장량 상위 N(특정 세션이 폭주하는지)
- 검색 지연 p95/p99
- 삭제 배치의 처리량과 지연(만료 데이터 backlog)
또한 에이전트는 외부 API 호출 실패로 재시도 루프에 빠지면 같은 결과를 반복 저장하며 메모리를 급격히 늘릴 수 있습니다. 재시도/백오프를 제대로 넣어 폭주 트리거를 줄이세요. 관련 패턴은 Claude 3 API 529/503 과부하 재시도·백오프 설계와 유사한 방식으로 적용할 수 있습니다.
실전 권장 설정(안전한 기본값)
팀/프로덕트마다 다르지만, 처음 도입할 때는 아래 정도가 “폭주 방지”에 효과적입니다.
- 세션 스코핑 필수:
session_id없으면 저장 금지 - 에피소드 TTL: 6시간~24시간
- 프로젝트 TTL: 30일
- 영구 메모리: 화이트리스트(규칙/요약/검증된 지식만)
top-k: 10~20부터 시작, 프롬프트 토큰 상한으로 추가 제한- 중복 방지:
content_hash또는 고유 키 기반 upsert - 물리 삭제 배치: 5~15분 주기(트래픽 낮을 때)
예시: “에이전트 작업” 단위로 메모리 분리하기
AutoGPT가 “리서치 작업”을 여러 개 수행한다면, 다음처럼 컬렉션/네임스페이스를 분리하면 사고 반경을 줄일 수 있습니다.
mem_global_permanentmem_project_{project_id}mem_session_{session_id}
그리고 세션이 끝나면 mem_session_{session_id}는 통째로 삭제하거나, TTL이 짧게 걸리도록 둡니다. 이 접근은 TTL이 완벽하지 않은 DB에서도 매우 강력합니다.
정리
AutoGPT의 메모리 폭주는 대부분 “벡터DB에 기억을 영구 누적하는 설계”에서 시작합니다. 해결책은 거창한 최적화보다도 다음 3가지를 기본으로 깔면 체감 효과가 큽니다.
session_id로 강하게 스코핑expires_at기반 TTL로 만료를 기본값으로 설정- 중요한 정보만 요약해 장기 메모리로 승격
여기에 중복 제거, 배치 물리 삭제, 인덱스/쿼리 튜닝, 재시도 백오프 같은 운영 안정화까지 더하면 “오래 돌아가는 에이전트”를 현실적으로 운영 가능한 수준으로 만들 수 있습니다.
다음 단계로는 “요약 승격 기준(importance/hit_count)을 어떻게 자동화할지”, “프롬프트 주입 토큰 예산을 어떻게 동적으로 배분할지”를 잡으면, 비용과 품질을 동시에 안정화할 수 있습니다.