- Published on
AutoGPT 메모리 누수? 벡터DB TTL로 해결하기
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
AutoGPT 류 에이전트를 운영 환경에 붙이면, 처음엔 잘 돌아가다가 시간이 지날수록 점점 느려지고 메모리 사용량이 꾸준히 올라가는 현상을 자주 겪습니다. 흔히 애플리케이션 메모리 누수로 의심하지만, 실제로는 벡터DB에 쌓이는 임베딩과 메타데이터가 무한히 증가하면서 생기는 2차 효과인 경우가 많습니다.
에이전트는 매 스텝마다 관찰, 툴 결과, 요약, 중간 계획을 기록합니다. 이 기록을 장기 기억으로 벡터DB에 넣어두면 검색 품질은 좋아지지만, 적절한 만료 정책이 없을 때 데이터가 끝없이 누적됩니다. 그 결과는 다음과 같이 나타납니다.
- 벡터 인덱스가 커져서 검색 지연이 증가
- 백그라운드 컴팩션, 머지 작업이 늘어나 CPU와 메모리 사용량 상승
- 디스크 사용량 증가로 인한 노드 압박, 컨테이너 재시작,
OOMKilled
운영에서 이 문제는 종종 CrashLoopBackOff 나 OOMKilled 로 표면화됩니다. 컨테이너 레벨 증상과 대응은 별도로 정리해둔 글도 참고할 수 있습니다: K8s CrashLoopBackOff·OOMKilled 원인과 해결
이 글에서는 “에이전트 메모리 누수처럼 보이는 현상”을 벡터DB TTL로 구조적으로 해결하는 접근을 다룹니다.
왜 벡터DB 적재가 메모리 누수처럼 보이나
에이전트가 생성하는 메모리는 크게 두 종류입니다.
- 프로세스 메모리: 파이썬 객체, 캐시, 모델 클라이언트, 배치 버퍼 등
- 외부 상태: 벡터DB, Redis, Postgres, S3 같은 저장소에 쌓이는 상태
문제가 되는 지점은 “외부 상태가 커지면 시스템 전체 리소스를 잠식”한다는 점입니다. 예를 들어 벡터 인덱스가 커질수록 검색 쿼리당 스캔 범위가 늘고, 인덱스 유지 작업이 빈번해집니다. 이 작업이 컨테이너 메모리와 CPU를 계속 먹으면서 마치 누수처럼 보입니다.
특히 AutoGPT 스타일의 루프는 다음 패턴을 갖습니다.
- 짧은 단위의 생각과 관찰을 자주 저장
- 저장된 메모리를 다시 검색해 컨텍스트에 넣음
- 검색이 느려지면 루프가 더 길어지고, 더 많은 로그와 메모리를 생성
즉, “무한 저장”은 “무한 루프”와 결합될 때 폭발적으로 비용을 키웁니다. 에이전트 루프 폭주 자체는 가드레일로도 막을 수 있습니다: LangChain 에이전트 무한루프·툴폭주 차단법
해결 전략: TTL을 중심으로 메모리 수명 설계하기
핵심은 간단합니다.
- 벡터DB에 저장하는 모든 메모리에 “수명”을 부여
- 수명은 메모리 타입과 세션 스코프에 따라 다르게 설정
- 만료된 데이터는 검색 대상에서 제외되고, 물리적으로도 정리
여기서 중요한 포인트는 “삭제”가 아니라 “만료”입니다. 운영 시스템에서 즉시 삭제는 락, 인덱스 재작성, 성능 급락을 유발할 수 있습니다. 보통은 아래 순서가 안전합니다.
- 만료 마킹:
expires_at를 기준으로 검색 필터링 - 비동기 청소: 배치로 삭제 또는 컬렉션 롤오버
- 인덱스 최적화: 컴팩션, 세그먼트 머지
어떤 메모리에 TTL을 걸어야 하나
에이전트 메모리를 세분화하면 TTL 설계가 쉬워집니다.
1) 단기 작업 메모리
- 예: 툴 호출 결과, 중간 관찰, 실패 로그
- 권장 TTL: 10분에서 6시간
- 이유: 디버깅이나 재시도에는 필요하지만 장기 검색 가치는 낮음
2) 세션 메모리
- 예: 사용자 세션에서의 대화 요약, 목표, 제약
- 권장 TTL: 1일에서 7일
- 이유: 동일 사용자 재방문 시 유용하지만 무기한 보관은 비용 대비 효용이 감소
3) 장기 지식 메모리
- 예: 검증된 사실, 문서에서 추출된 지식, 정제된 요약
- 권장 TTL: 길게 또는 무기한
- 조건: 반드시 “승격” 절차를 거쳐야 함
여기서 승격이란, 에이전트가 아무 말이나 저장하는 것이 아니라 “검증된 결과만 장기 컬렉션으로 이동”시키는 파이프라인입니다.
구현 패턴 1: 메타데이터 기반 TTL 필터링
벡터DB가 TTL을 네이티브로 지원하지 않더라도, 대부분 메타데이터 필터는 지원합니다. 가장 범용적인 방식은 expires_at 타임스탬프를 저장하고 검색 시 필터링하는 것입니다.
아래 예시는 파이썬에서 문서를 저장할 때 만료 시각을 함께 넣는 패턴입니다.
import time
from dataclasses import dataclass
@dataclass
class MemoryDoc:
id: str
text: str
session_id: str
kind: str # "tool_result", "observation", "summary" ...
created_at: int
expires_at: int
def ttl_seconds(kind: str) -> int:
if kind in ("tool_result", "observation"):
return 60 * 60 # 1 hour
if kind in ("summary", "session_state"):
return 60 * 60 * 24 * 3 # 3 days
return 60 * 60 * 24 * 365 # 1 year (or treat as long-term)
def build_doc(text: str, session_id: str, kind: str, doc_id: str) -> MemoryDoc:
now = int(time.time())
return MemoryDoc(
id=doc_id,
text=text,
session_id=session_id,
kind=kind,
created_at=now,
expires_at=now + ttl_seconds(kind),
)
검색 시점에는 expires_at 가 현재보다 큰 것만 조회합니다. 벡터DB마다 필터 문법이 다르니, 아래는 개념적 의사코드로 봐야 합니다.
now = int(time.time())
results = vector_db.search(
query_embedding=q,
top_k=20,
where={
"session_id": session_id,
"expires_at": {"$gt": now},
},
)
이 방식의 장점은 즉시 효과가 난다는 점입니다. 물리적으로 데이터가 남아 있어도 검색 대상에서 빠지므로, 컨텍스트 오염과 검색 성능 악화를 먼저 막을 수 있습니다.
구현 패턴 2: 컬렉션 분리와 롤오버
TTL을 문서 단위로 관리하는 대신, 컬렉션을 시간 단위로 분리하는 전략도 있습니다.
- 컬렉션 예시
agent_memory_hot_2026w08agent_memory_hot_2026w09agent_memory_longterm
운영 방식은 간단합니다.
- 최근 1주 컬렉션만 검색
- 2주가 지난 컬렉션은 통째로 드롭
장점
- 삭제가 빠르고 단순함
- 인덱스 단편화가 줄어듦
단점
- 문서별 세밀한 TTL이 어렵고, 주 단위 같은 버킷으로 뭉개짐
트래픽이 많고 메모리 생성량이 큰 에이전트라면 이 방식이 운영 난이도를 크게 낮춥니다.
구현 패턴 3: 주기적 청소 작업과 안전장치
메타데이터 필터링만으로는 디스크와 인덱스가 계속 커질 수 있습니다. 따라서 청소 작업이 필요합니다.
- 주기: 10분에서 1시간 간격
- 전략
- 만료된 문서 ID를 배치로 삭제
- 또는 만료된 컬렉션을 드롭
청소 작업은 반드시 다음을 고려해야 합니다.
- 삭제 배치 크기 제한: 한 번에 너무 많이 지우면 인덱스 재작성으로 지연이 튐
- 피크 타임 회피: 트래픽이 낮을 때 수행
- 실패 내성: 중간 실패해도 다음 실행에서 이어서 정리
간단한 예시는 다음과 같습니다.
import time
BATCH = 500
def cleanup_expired(vector_db, session_id: str | None = None):
now = int(time.time())
where = {"expires_at": {"$lte": now}}
if session_id is not None:
where["session_id"] = session_id
while True:
ids = vector_db.list_ids(where=where, limit=BATCH)
if not ids:
break
vector_db.delete(ids)
list_ids 같은 API가 없다면, 별도의 메타데이터 인덱스를 Postgres에 두고 “만료된 ID 목록”을 관리하는 하이브리드 구조도 실무에서 많이 씁니다.
“TTL만 걸면 끝”이 아닌 이유: 검색 품질과 비용 균형
TTL을 공격적으로 걸면 메모리 누수성 문제는 잡히지만, 에이전트가 맥락을 잃고 성능이 떨어질 수 있습니다. 그래서 보통은 다음 3가지를 같이 합니다.
1) 저장 전 요약 및 중복 제거
- 툴 결과 원문을 그대로 저장하지 말고, 길이 제한 요약을 저장
- 같은 URL, 같은 파일, 같은 쿼리의 반복 결과는 해시로 중복 제거
2) 승격 파이프라인
- 단기 컬렉션에 쌓인 메모리 중 “가치 있는 것”만 선별해 장기 컬렉션으로 이동
- 선별 기준 예시
- 사용자 피드백이 긍정
- 동일 질문 재발 시 재사용률이 높음
- 출처 링크가 있고 검증됨
3) 검색 범위 제한
- 세션 스코프를 기본으로 하고, 필요할 때만 전역 검색
- 전역 검색은 비용이 크므로 top k 를 작게 시작
운영에서 확인해야 할 지표
TTL 적용 후에는 아래를 반드시 관측해야 합니다.
- 벡터DB 컬렉션 크기 증가율이 0에 수렴하는지
- 검색 p95 지연이 시간이 지나도 악화되지 않는지
- 에이전트 루프 스텝 수가 안정화되는지
- 컨테이너 메모리 사용량이 톱니형으로 안정화되는지
만약 Kubernetes 환경에서 여전히 파드가 자주 죽는다면, 애플리케이션 문제와 별개로 리소스 제한, 노드 압박, 스파이크를 함께 점검해야 합니다. 앞서 링크한 CrashLoopBackOff 글이 그 체크리스트에 도움이 됩니다.
실전 체크리스트
아래 체크리스트대로 적용하면, “AutoGPT 메모리 누수처럼 보이는 현상”의 상당수를 빠르게 완화할 수 있습니다.
- 메모리 타입을 최소 2개로 분리: 단기와 장기
- 단기 메모리에
expires_at를 저장하고 검색에서 필터링 - 배치 청소 작업을 추가하거나 컬렉션 롤오버로 물리 삭제
- 중복 제거와 요약으로 저장량 자체를 감축
- 에이전트 무한 루프 방지를 함께 적용
마무리
AutoGPT를 포함한 에이전트 시스템에서 “메모리 누수”는 실제 누수라기보다, 벡터DB에 무제한으로 쌓이는 기억이 시스템 전체를 점진적으로 압박하는 형태로 나타나는 경우가 많습니다. 해결의 핵심은 벡터DB를 단순 저장소가 아니라 “수명이 있는 캐시 겸 지식 베이스”로 보고, TTL과 승격 전략을 설계하는 것입니다.
TTL을 도입하면 비용이 예측 가능해지고, 검색 성능이 시간에 따라 악화되는 문제도 함께 줄어듭니다. 그 위에 요약, 중복 제거, 가드레일을 조합하면 장기 운영에서 안정적인 에이전트 메모리 아키텍처를 만들 수 있습니다.