- Published on
AutoGPT 메모리 누수? 벡터DB TTL·압축 실전
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서론
AutoGPT 같은 에이전트는 실행 시간이 길어질수록 “메모리 누수”처럼 보이는 현상을 자주 만듭니다. 프로세스 RSS가 계속 오르거나, 벡터DB 디스크가 폭증하고, 검색이 느려지며, 결국 OOM 또는 지연 폭발로 이어집니다. 하지만 실제로는 전통적인 의미의 메모리 누수(해제되지 않는 힙 객체)보다 기억(메모리) 설계가 누적형으로 되어 있는 문제가 훨씬 많습니다.
이 글에서는 AutoGPT류 에이전트에서 흔히 발생하는 누적 지점을 분해하고, 벡터DB에서 TTL(만료) 과 압축(quantization, PQ 등) 을 적용해 비용을 제어하는 방법을 실전 관점으로 정리합니다. 또한 운영에서 자주 부딪히는 청소 작업(VACUUM, compaction)과 관측 포인트까지 함께 다룹니다.
관련 성능 튜닝으로는 Milvus의 IVF_PQ 설정이 특히 도움이 됩니다. 자세한 검색 성능 튜닝은 Milvus IVF_PQ 튜닝으로 Pinecone급 검색속도도 같이 참고하면 좋습니다. 그리고 PostgreSQL을 메타데이터 저장소로 같이 쓰는 구성이라면 디스크 bloat 대응은 PostgreSQL VACUUM 안 돌아가 디스크 폭증 해결도 연결해서 보길 권합니다.
“메모리 누수”처럼 보이는 5가지 누적 원인
에이전트 시스템에서 메모리/디스크가 계속 증가하는 원인을 구분하면 해결책이 명확해집니다.
1) 벡터DB에 영구 누적되는 임베딩
AutoGPT는 대화/작업 산출물을 임베딩으로 저장하고 유사도 검색으로 재활용합니다. 문제는 기본 설계가 “계속 쌓기”인 경우가 많다는 점입니다.
- 동일한 사실/요약이 여러 번 저장됨(중복 임베딩)
- 스크래치 패드, 중간 추론, 실패한 시도까지 전부 저장됨
- 사용자 세션이 끝나도 삭제가 없음
결과적으로 디스크는 늘고, 인덱스/세그먼트가 커져 검색도 느려집니다.
2) in-memory 캐시/세션이 TTL 없이 누적
FastAPI, Node, Spring 등 어떤 런타임이든 캐시는 “맞으면 빠르지만” TTL이 없으면 결국 메모리 압력으로 돌아옵니다.
lru_cache류가 무제한 키를 가질 때- 사용자별 컨텍스트를 프로세스 메모리에 붙잡아둘 때
- 작업 큐 결과를 영구 보관할 때
3) 로그/트레이스/이벤트 스토어의 폭증
LLM 프롬프트/응답을 통째로 로깅하면 관측은 좋아지지만 비용이 급증합니다.
- 프롬프트, 툴 호출 인자, 응답 전문 저장
- 토큰 사용량, intermediate chain 저장
이 데이터는 “운영에 필요한 기간”이 지나면 가치가 급격히 떨어집니다. TTL과 샘플링이 핵심입니다.
4) 벡터 인덱스의 compaction 지연
Milvus, Qdrant, Weaviate 등은 삭제/업데이트가 즉시 디스크 반환으로 이어지지 않는 경우가 많습니다.
- 삭제는 tombstone으로 남고
- 일정 조건에서 compaction이 일어나며
- compaction이 밀리면 디스크와 검색 성능이 동시에 악화
5) 애플리케이션 레벨의 진짜 메모리 누수
물론 실제 누수도 있습니다.
- 대형 리스트에 append만 하고 제거하지 않음
- asyncio task 누적
- 스트리밍 응답 버퍼가 계속 남음
하지만 운영에서 체감되는 “계속 커짐”의 상당수는 1~4번 누적 설계 문제입니다.
목표: 기억을 “유지 비용이 있는 자산”으로 취급하기
에이전트 메모리는 무조건 많이 저장한다고 좋아지지 않습니다. 실전에서는 다음 3가지를 동시에 만족해야 합니다.
- 최근성: 최근 대화/작업은 잘 찾아야 함
- 중요도: 핵심 사실/사용자 선호는 오래 유지
- 비용 상한: 디스크/인덱스/메모리 사용량이 예측 가능
이를 위해 가장 효과적인 조합이 TTL + 계층화 + 압축 + 중복 제거입니다.
TTL 설계: 무엇을 언제 지울 것인가
TTL은 단순히 “N일 후 삭제”가 아니라, 메모리 유형별로 다르게 가져가야 합니다.
메모리 유형을 3계층으로 나누기
- Episodic(에피소드): 세션/작업 단위 메모리. 대부분 TTL 대상
- Semantic(의미): 요약된 사실/규칙/선호. 길게 유지
- Procedural(절차): 툴 사용법, 워크플로우. 거의 변하지 않으니 최소 저장
실전 추천 TTL 예시:
- 에피소드: 7일 또는 30일
- 의미 메모리: TTL 없음(단, 품질/중복 기준으로 정리)
- 절차: 코드/문서로 관리하고 벡터화는 최소화
TTL은 “삭제”만이 아니라 “다운그레이드”도 포함
바로 삭제하지 말고, 비용이 싼 형태로 바꾸는 전략이 효과적입니다.
- 원문 chunk 임베딩
512차원 보관30일 30일 이후에는 요약문 1개만 남기고 원문 chunk 삭제- 요약문은 더 작은 차원 임베딩 또는 PQ 압축 인덱스로 이동
이렇게 하면 검색 품질을 크게 해치지 않으면서 디스크를 안정화할 수 있습니다.
벡터DB별 TTL 구현 패턴
엔진마다 TTL 지원이 다릅니다. “네이티브 TTL이 없으면” 메타데이터와 배치 삭제로 구현합니다.
패턴 A: 네이티브 TTL이 있는 경우
예: Qdrant는 payload에 timestamp를 넣고 필터로 삭제 작업을 돌리는 방식이 일반적입니다(버전에 따라 TTL 기능/권장 패턴이 다름).
핵심은 아래 두 가지입니다.
- 검색 쿼리에 항상
expires_at필터를 넣어 만료 데이터가 검색에 섞이지 않게 하기 - 백그라운드에서 실제 삭제를 수행해 디스크를 회수하기
패턴 B: 네이티브 TTL이 없거나 제한적인 경우
Milvus/pgvector 조합처럼 TTL이 “자동”이 아닐 때는 보통 다음으로 갑니다.
- 메타데이터 테이블에
expires_at저장 - 쿼리 시
expires_at조건으로 후보를 제한 - 주기적으로 만료 ID를 모아 delete
- compaction/VACUUM 등 후처리로 디스크 회수
pgvector로 TTL 구현 예제 (PostgreSQL)
PostgreSQL에 pgvector로 임베딩을 저장하는 경우, TTL은 SQL로 명확하게 구현할 수 있습니다.
스키마 예시
CREATE TABLE agent_memory (
id bigserial PRIMARY KEY,
tenant_id text NOT NULL,
session_id text,
kind text NOT NULL, -- episodic | semantic | procedural
content text NOT NULL,
embedding vector(1536) NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
expires_at timestamptz,
importance int NOT NULL DEFAULT 0
);
CREATE INDEX agent_memory_tenant_kind_created_idx
ON agent_memory (tenant_id, kind, created_at DESC);
-- 벡터 인덱스는 운영 요구에 맞게 선택
-- ivfflat 예시 (데이터가 충분히 쌓인 뒤 build 권장)
CREATE INDEX agent_memory_embedding_ivfflat
ON agent_memory USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 200);
검색 시 만료 제외
expires_at 이 NULL 이거나 아직 미래인 것만 대상으로 합니다.
SELECT id, content
FROM agent_memory
WHERE tenant_id = $1
AND kind IN ('episodic','semantic')
AND (expires_at IS NULL OR expires_at > now())
ORDER BY embedding <=> $2
LIMIT 20;
만료 데이터 삭제 배치
대량 삭제는 락/부하를 유발할 수 있으니 chunk 단위로 지웁니다.
WITH doomed AS (
SELECT id
FROM agent_memory
WHERE expires_at IS NOT NULL
AND expires_at <= now()
ORDER BY id
LIMIT 5000
)
DELETE FROM agent_memory
WHERE id IN (SELECT id FROM doomed);
삭제 후 디스크가 바로 줄지 않는다면 VACUUM이 필요합니다. VACUUM이 밀리면 bloat로 디스크가 계속 커질 수 있으니, 운영에서는 PostgreSQL VACUUM 안 돌아가 디스크 폭증 해결을 참고해 자동/수동 VACUUM 정책을 점검하세요.
Milvus에서 TTL·압축을 함께 쓰는 운영 패턴
Milvus는 대규모 벡터에 강하고, 압축 인덱스(예: IVF_PQ)를 통한 비용 절감이 현실적으로 큽니다.
1) “컬렉션 분리”로 TTL을 단순화
가장 운영 친화적인 방식은 컬렉션을 역할별로 나누는 것입니다.
mem_episodic_hot: 최근 7일, 원문 chunk, HNSW 또는 IVF_FLATmem_episodic_cold: 7일~30일, 요약 위주, IVF_PQmem_semantic: 장기, 요약/사실 위주, IVF_PQ 또는 더 작은 차원
TTL이 지난 데이터는 delete도 가능하지만, 실제로는 “컬렉션 드롭”이 가장 확실한 정리입니다.
- 날짜 파티션(예:
mem_episodic_2026_02_01)을 만들어두고 - 기간이 지나면 해당 파티션/컬렉션을 통째로 drop
이 방식은 compaction 부담과 tombstone 누적을 크게 줄입니다.
2) PQ 압축으로 디스크와 캐시 압력을 줄이기
IVF_PQ는 벡터를 코드북 기반으로 압축해 저장합니다.
- 장점: 디스크 감소, 메모리 캐시 부담 감소, 대규모에서 비용 효율적
- 단점: 근사 오차로 recall이 떨어질 수 있어 파라미터 튜닝 필요
튜닝 포인트(개념):
nlist(또는 lists): inverted list 수m: PQ 서브벡터 개수nbits: 코드 비트 수- 검색 시
nprobe: 탐색할 리스트 수
실제 파라미터 튜닝은 데이터 분포와 QPS에 따라 달라서, 기본값으로 고정하면 품질/속도/비용이 모두 애매해지기 쉽습니다. 이 부분은 Milvus IVF_PQ 튜닝으로 Pinecone급 검색속도에서 더 깊게 다룬 내용을 그대로 적용할 수 있습니다.
3) “핫은 정확, 콜드는 싸게” 2단계 검색
운영에서 가장 많이 쓰는 패턴입니다.
- 핫 컬렉션에서 먼저 검색(정확도 우선)
- 결과가 부족하거나 점수가 낮으면 콜드 컬렉션에서 추가 검색(비용 우선)
- 결과를 합쳐 rerank
이렇게 하면 콜드 인덱스의 근사 오차가 전체 UX를 망치는 것을 막으면서, 디스크 비용은 크게 줄일 수 있습니다.
중복 제거: “저장 전에” 줄이는 게 가장 싸다
TTL과 압축은 사후 처리입니다. 가장 큰 절감은 저장 전에 일어납니다.
1) 콘텐츠 해시로 동일 chunk 차단
chunk 텍스트를 정규화한 뒤 해시를 저장해 중복 삽입을 막습니다.
import hashlib
def normalize(text: str) -> str:
return " ".join(text.split()).strip().lower()
def content_hash(text: str) -> str:
return hashlib.sha256(normalize(text).encode("utf-8")).hexdigest()
DB에 tenant_id + hash 유니크 제약을 걸면, 실수로 같은 데이터를 여러 번 넣는 문제를 크게 줄일 수 있습니다.
2) 유사 중복은 “저장 전 근사 검색”으로 차단
완전 동일이 아니라 유사한 문장(예: 같은 요약을 여러 번 생성)도 문제입니다.
- 저장 전에 같은 세션/테넌트 내에서 top
k검색 - cosine 유사도 임계치(예:
0.92) 이상이면 “업데이트” 또는 “스킵”
이때 임계치는 도메인별로 다르니 A/B로 잡는 게 안전합니다.
압축의 또 다른 축: 차원 축소와 요약
PQ만이 답은 아닙니다. 벡터 자체를 줄이는 방법도 함께 고려하면 좋습니다.
1) 임베딩 차원 축소
1536차원 모델을 무조건 쓰지 말고- 더 작은 임베딩 모델로 교체하거나
- PCA 같은 후처리로 차원을 줄이는 방식도 있습니다
다만 차원 축소는 품질 영향이 크므로, “콜드 티어”에만 적용하는 것이 안전합니다.
2) 요약 기반 저장
가장 강력한 비용 절감은 “원문을 다 저장하지 않는 것”입니다.
- 세션이 끝날 때 요약 1개 생성
- 사실/선호/금기사항 같은 구조화된 메모리로 추출
- 원문 chunk는 짧은 TTL만 부여
이렇게 하면 장기 기억은 더 안정적으로 유지되고, 검색 공간도 작아집니다.
운영 체크리스트: 누수처럼 보일 때 어디부터 볼까
1) 관측 지표
- 벡터DB: 컬렉션별 row 수, 세그먼트 수, compaction backlog
- 앱: RSS, GC 시간, request latency p95/p99
- Postgres: table size, index size, dead tuples, autovacuum 동작
2) “삭제했는데 디스크가 안 줄어드는” 경우
- 벡터DB는 compaction이 필요할 수 있음
- Postgres는 VACUUM(또는 VACUUM FULL)이 필요할 수 있음
VACUUM이 오래 걸리거나 끝나지 않는 상황도 흔합니다. 이런 경우에는 PostgreSQL VACUUM 안 끝남? bloat·락 7단계 진단 같은 체크리스트로 원인을 좁히는 게 빠릅니다.
3) 삭제 배치의 안전장치
- 한 번에 너무 많이 지우지 말고
LIMIT기반 chunk 삭제 - 삭제 작업은 별도 워커로 분리
- 피크 시간대 회피
- 삭제 후 검색 품질/지연이 악화되는지 모니터링
예시 아키텍처: TTL·압축·요약을 합친 실전 구성
아래는 “장시간 돌아가는 AutoGPT”에서 비용 상한을 걸기 좋은 구성입니다.
- 실행 중: 작업 로그/중간 산출물은 object storage에 저장(짧은 TTL)
- 세션 메모리(핫): 최근 chunk 임베딩을
hot컬렉션에 저장(정확 인덱스) - 세션 종료: 요약/사실 추출 후
semantic컬렉션에 저장(압축 인덱스) - 배치 작업:
hot에서 만료된 파티션 dropcold는 IVF_PQ로 유지하며 주기 compaction- Postgres 메타데이터는 autovacuum 튜닝
핵심은 “검색 품질이 필요한 구간만 비싼 저장소/인덱스”를 쓰고, 나머지는 TTL과 압축으로 비용을 강제하는 것입니다.
결론
AutoGPT에서 보이는 메모리 누수는 종종 런타임 버그가 아니라 기억 데이터의 무제한 누적에서 시작합니다. 해결은 단일 기술이 아니라 조합입니다.
- TTL로 수명주기를 설계하고
- 핫/콜드 계층으로 비용을 분리하며
- IVF_PQ 같은 압축 인덱스로 저장 비용을 낮추고
- 중복 제거와 요약으로 “저장 전”부터 줄이기
이 네 가지를 적용하면, 장시간 실행되는 에이전트도 디스크와 성능이 안정적으로 유지됩니다. 운영에서 가장 중요한 것은 “얼마나 기억하느냐”가 아니라 “얼마나 예측 가능하게 잊게 하느냐”입니다.