- Published on
AutoGPT 메모리 누수? 벡터DB TTL·압축 설계
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
AutoGPT를 운영하다 보면 “메모리 누수”라는 표현을 자주 쓰게 됩니다. 프로세스 RSS가 계속 오르거나, 디스크가 끝없이 늘어나거나, 검색이 점점 느려지고 환각이 늘어나는 현상이 한꺼번에 나타나기 때문입니다. 하지만 많은 케이스에서 진짜 누수는 런타임 힙이 아니라 벡터DB(혹은 임베딩 스토어)의 무한 누적과 삭제 부재로 인한 인덱스/스토리지 단편화, 그리고 재검색 범위 폭증에서 시작됩니다.
이 글에서는 AutoGPT류 에이전트에서 흔히 겪는 “메모리 팽창”을 TTL(만료), 압축(컴팩션), 요약/계층화로 통제하는 실전 설계를 다룹니다. 단순히 오래된 데이터를 지우는 수준이 아니라, 검색 품질을 보존하면서 비용과 지연을 낮추는 방법에 초점을 맞춥니다.
- 관련 글: AutoGPT 메모리 팽창·환각 줄이는 RAG+벡터DB
- 런타임/스토리지 관점 보완: AutoGPT 메모리 누수, SQLite 체크포인트로 잡기
1) “메모리 누수”처럼 보이는 3가지 원인
1-1. 벡터DB에 이벤트 로그를 그대로 쌓는 패턴
대화 턴, 툴 호출 결과, 웹 스크래핑 결과, 중간 추론 메모까지 전부 임베딩해 넣으면 벡터DB는 사실상 append-only 로그가 됩니다. 이때 문제가 되는 건 단순 용량뿐 아니라 아래가 동시에 발생한다는 점입니다.
- 검색 후보 수가 늘어 top
k탐색 비용 증가 - 유사도 근접한 “오래된/부정확한” 조각이 섞여 컨텍스트 오염
- 삭제가 있어도 내부 세그먼트가 남아 단편화
1-2. 삭제는 했는데 디스크가 줄지 않는 이유
많은 벡터 스토어는 삭제를 “tombstone(삭제 마커)”로 처리하고, 실제 물리 공간 회수는 컴팩션/머지 같은 백그라운드 작업에 의존합니다. 즉, TTL로 논리 삭제만 해서는 디스크가 줄지 않거나, 성능이 회복되지 않을 수 있습니다.
1-3. 임베딩 차원/정밀도 선택이 과한 경우
임베딩을 float32로 저장하고, 차원이 크고, 문서 조각이 과도하게 잘게 쪼개지면 저장량이 기하급수로 늘어납니다. 예를 들어 1536차원 float32는 벡터 1개당 대략 1536 * 4 = 6144 바이트(약 6KB)입니다. 여기에 메타데이터와 인덱스 오버헤드까지 붙습니다.
2) TTL 설계: “삭제”가 아니라 “기억의 수명”을 정의하라
TTL은 단순 만료 시간이 아니라, 에이전트 메모리를 계층화하기 위한 핵심 정책입니다. 추천하는 접근은 “단일 TTL”이 아니라 메모리 타입별 TTL입니다.
2-1. 메모리 분류(권장)
- Episodic(에피소드): 최근 작업/대화/툴 결과. 휘발성. TTL 짧게.
- Task(작업 지식): 특정 목표에만 유효한 요약/결론. 목표 종료 시 만료.
- Semantic(장기 지식): 재사용 가능한 사실/문서 근거. TTL 길게 또는 무기한.
- Safety/Policy(정책): 안전/규칙/금지사항. 사실상 무기한.
핵심은 “대부분의 데이터는 장기 기억이 될 자격이 없다”는 전제를 시스템에 박는 것입니다.
2-2. TTL을 결정하는 3가지 신호
- Recency(최근성): 마지막 참조 시점 기준으로 만료.
last_access_at기반. - Utility(유용성): 검색에 실제로 채택된 빈도.
hit_count기반. - Confidence(신뢰도): 출처/검증 여부. 웹 스크랩은 짧게, 내부 DB는 길게.
즉, 단순히 created_at + 7d 같은 고정 TTL보다, “안 쓰이면 빨리 죽고, 자주 쓰이면 살아남는” 정책이 효과적입니다.
2-3. TTL 메타데이터 스키마 예시
아래는 벡터DB에 넣을 메타데이터 최소 세트 예시입니다.
-- 논리 스키마(벡터DB 메타 필드로 저장)
-- angle bracket를 쓰지 않기 위해 타입 표기는 생략
id
namespace
text
embedding
memory_type -- episodic | task | semantic | policy
created_at
last_access_at
expire_at
hit_count
source -- tool:web | tool:db | user | system
task_id
confidence -- 0.0 ~ 1.0
checksum -- 중복 방지용
expire_at은 고정값으로 저장해도 되지만, 운영 중 정책을 바꾸기 쉽도록 ttl_seconds를 저장하고 배치에서 재계산하는 방식도 좋습니다.
3) TTL만으로는 부족하다: 컴팩션(압축)과 세그먼트 정리
TTL은 “논리 삭제”를 만들 뿐, 스토리지/인덱스 회수는 별도입니다. 여기서 말하는 압축은 두 층위가 있습니다.
- 스토리지 컴팩션: tombstone 제거, 세그먼트 병합, 물리 공간 회수
- 정보 압축: 많은 조각을 요약/대표 벡터로 치환
3-1. 스토리지 컴팩션 운영 체크리스트
- 삭제/만료 비율이 일정 임계치(예: 20% 이상) 넘으면 컴팩션 실행
- 컴팩션은 I/O를 잡아먹으므로 트래픽 낮을 때 스케줄
- 컴팩션 후 인덱스 재빌드가 필요한 엔진은 별도 윈도우 확보
엔진별로 명령이 다르므로, 여기서는 “패턴”만 제시합니다.
배치 컴팩션 의사 코드
import time
from datetime import datetime, timedelta
EXPIRED_SCAN_INTERVAL_SEC = 60
COMPACTION_THRESHOLD = 0.20
def scan_and_tombstone(vector_store, now):
expired_ids = vector_store.query_ids(filter={"expire_at": {"$lte": now}})
if expired_ids:
vector_store.delete(expired_ids) # 논리 삭제(tombstone)
return len(expired_ids)
def maybe_compact(vector_store):
stats = vector_store.stats() # total, deleted, segments 같은 값이 있다고 가정
deleted_ratio = (stats["deleted"] / max(stats["total"], 1))
if deleted_ratio >= COMPACTION_THRESHOLD:
vector_store.compact() # 엔진별 구현
def ttl_worker(vector_store):
while True:
now = datetime.utcnow()
scan_and_tombstone(vector_store, now)
maybe_compact(vector_store)
time.sleep(EXPIRED_SCAN_INTERVAL_SEC)
포인트는 delete만 호출하고 끝내지 말고, deleted ratio를 관측해서 컴팩션을 트리거하는 것입니다.
3-2. 정보 압축: “요약+대표 벡터”로 장기 저장량 줄이기
에이전트 메모리에서 가장 효과가 큰 최적화는 “오래된 에피소드 원문”을 그대로 유지하지 않고, 요약본으로 치환하는 것입니다.
- 원문 N개(대화 턴/툴 로그)를 하나의 “에피소드 요약”으로 합치기
- 요약 텍스트를 다시 임베딩해서 대표 벡터 1개로 저장
- 원문은 짧은 TTL로 두거나 아카이브(저렴한 스토리지)로 이동
요약 압축 파이프라인 예시
from datetime import datetime, timedelta
def compress_old_episodic(vector_store, llm, now=None):
now = now or datetime.utcnow()
# 1) 오래된 episodic 중 아직 요약되지 않은 것 수집
candidates = vector_store.query(
filter={
"memory_type": "episodic",
"created_at": {"$lte": now - timedelta(days=2)},
"compressed": False,
},
limit=200,
)
# 2) task_id 단위로 묶어서 요약
by_task = {}
for item in candidates:
by_task.setdefault(item["task_id"], []).append(item)
for task_id, items in by_task.items():
raw_text = "\n".join([it["text"] for it in items])
summary = llm.summarize(
raw_text,
instruction="핵심 결정, 근거, 실패 원인, 다음 액션만 남겨서 요약"
)
# 3) 요약을 semantic 또는 task 메모리로 승격
vector_store.upsert(
text=summary,
metadata={
"memory_type": "task",
"task_id": task_id,
"created_at": now,
"expire_at": now + timedelta(days=14),
"source": "system:compressor",
"confidence": 0.7,
},
)
# 4) 원문은 단축 TTL 적용 또는 삭제
ids = [it["id"] for it in items]
vector_store.update_metadata(ids, {"compressed": True, "expire_at": now + timedelta(hours=6)})
이 방식은 단순 삭제보다 검색 품질을 더 안정적으로 유지합니다. “과거를 잊는” 것이 아니라 “과거를 요약해서 기억”하게 만드는 것이기 때문입니다.
4) TTL과 검색 품질의 트레이드오프를 줄이는 설계
TTL을 강하게 걸면 당장 저장량은 줄지만, “필요한 근거가 사라져서” 환각이 늘 수 있습니다. 이를 막는 장치가 필요합니다.
4-1. 2단계 저장소: 벡터DB와 원문 아카이브 분리
- 벡터DB: 검색을 위한 최소 텍스트(요약/청크)와 메타만 유지
- 원문 스토어(S3, GCS, DB): TTL 길게(또는 비용 기반 아카이브)
검색 결과가 나오면, 메타의 source_uri로 원문을 다시 가져오는 “late materialization” 패턴을 씁니다.
# 검색은 벡터DB에서
hits = vector_store.similarity_search(query, k=8)
# 필요한 것만 원문 조회
docs = []
for h in hits:
uri = h["metadata"].get("source_uri")
if uri:
docs.append(object_store.get(uri))
else:
docs.append(h["text"]) # 요약만 있는 경우
4-2. namespace/tenant 분리로 폭발 반경 줄이기
AutoGPT를 멀티유저/멀티에이전트로 돌리면, 한 에이전트의 폭주가 전체 인덱스를 오염시킵니다.
namespace = user_id + agent_id로 물리/논리 분리- TTL/컴팩션도 namespace 단위로 수행
- 핫한 namespace만 더 자주 컴팩션
4-3. “최근성 가중” 재랭킹
TTL을 길게 가져가야 하는 경우, 검색 결과에서 오래된 조각이 상위로 뜨는 문제를 재랭킹으로 완화할 수 있습니다.
- 유사도 점수
sim - 최근성 점수
recency = exp(-age_hours / tau) - 최종 점수
score = sim * (0.7 + 0.3 * recency)같은 형태
주의: 이때도 수식에 > 같은 기호를 본문에 그대로 쓰면 MDX에서 문제가 될 수 있으니, 문서에서는 인라인 코드로 표기합니다. 예를 들어 score = sim * (0.7 + 0.3 * recency)처럼요.
5) 벡터 자체 압축: 정밀도·차원·중복 제거
5-1. float16 또는 양자화(가능한 엔진에서)
임베딩을 float32로 저장할 이유가 없는 경우가 많습니다. 엔진이 지원한다면 float16 또는 8-bit 양자화를 고려하세요.
- 장점: 저장량 절감, 캐시 적중률 상승
- 단점: 근사 오차로 유사도 품질이 약간 떨어질 수 있음
운영 팁은 “전부 양자화”가 아니라, memory_type = episodic부터 적용하는 것입니다.
5-2. 중복 제거: 체크섬 기반 upsert
같은 툴 결과나 같은 문서 조각이 반복 삽입되는 경우가 많습니다. 텍스트 정규화 후 체크섬을 만들어 중복 삽입을 차단합니다.
import hashlib
def normalize_text(s: str) -> str:
return " ".join(s.strip().split())
def checksum(s: str) -> str:
return hashlib.sha256(normalize_text(s).encode("utf-8")).hexdigest()
def upsert_dedup(vector_store, text, metadata):
cs = checksum(text)
existing = vector_store.query(filter={"checksum": cs}, limit=1)
if existing:
# 이미 있으면 last_access_at만 갱신
vector_store.update_metadata([existing[0]["id"]], {"last_access_at": metadata.get("created_at")})
return existing[0]["id"]
metadata = dict(metadata)
metadata["checksum"] = cs
return vector_store.upsert(text=text, metadata=metadata)
6) 운영에서 진짜 중요한 것: 지표와 가드레일
TTL/압축은 “한 번 설계하면 끝”이 아니라, 지표를 보고 계속 조정해야 합니다.
6-1. 반드시 모니터링할 지표
vector_count(namespace별)deleted_ratio(tombstone 비율)segment_count또는index_size- 검색 P95 지연
hit_count분포(상위 몇 개만 계속 쓰이는지)- 컨텍스트 길이(토큰)와 비용
비용 폭주까지 같이 엮이면, 토큰/툴 호출 한도도 함께 설계해야 합니다. 이 주제는 AutoGPT 비용 폭주 막기 - 토큰·툴 호출 한도 설계와 같이 보면 연결이 잘 됩니다.
6-2. 가드레일 예시
- namespace당 최대 벡터 수
max_vectors_per_ns초과 시 강제 압축 실행 deleted_ratio가 임계치 이상인데 컴팩션 실패하면 쓰기 차단(장애 전파 방지)- 작업(task) 종료 이벤트가 오면 해당 task의 episodic TTL을 즉시 단축
MAX_VECTORS_PER_NS = 200_000
def enforce_guardrails(vector_store, namespace: str):
stats = vector_store.stats(namespace=namespace)
if stats["total"] >= MAX_VECTORS_PER_NS:
# 오래된 episodic부터 압축/만료를 강제
vector_store.enqueue_job("compress_old_episodic", {"namespace": namespace})
if stats.get("deleted_ratio", 0.0) >= 0.35:
ok = vector_store.compact(namespace=namespace)
if not ok:
vector_store.set_readonly(namespace=namespace, readonly=True)
7) 추천 아키텍처: TTL + 요약 압축 + 아카이브
정리하면, AutoGPT 메모리 팽창을 안정적으로 막는 조합은 아래입니다.
- 단기(episodic): 짧은 TTL, 자주 삭제, 자주 컴팩션
- 중기(task): 요약 압축으로 승격, 목표 종료 후 만료
- 장기(semantic/policy): 엄선된 근거만 남김, 중복 제거 필수
- 원문 아카이브: 벡터DB 밖으로 분리, 필요 시 재수집
이 구성을 적용하면 “메모리 누수”처럼 보이던 현상이 대개 아래처럼 바뀝니다.
- 디스크 증가가 계단형으로 완만해짐(컴팩션 주기마다 회수)
- 검색 지연이 안정화됨(top
k후보 풀 축소) - 오래된 잡음이 사라져 환각이 줄어듦(컨텍스트 오염 감소)
8) 마무리: TTL은 삭제 정책이 아니라 학습된 망각이다
에이전트의 메모리는 사람의 기억과 비슷합니다. 전부 기억하면 똑똑해지는 게 아니라, 오히려 검색이 느려지고 맥락이 흐려집니다. TTL과 압축(컴팩션)은 “데이터를 지우는 기술”이 아니라 유효한 기억만 남기기 위한 설계입니다.
다음 순서로 적용하면 시행착오가 적습니다.
- 1단계:
memory_type분류와 TTL 메타데이터 도입 - 2단계: 만료 스캐너 + tombstone + deleted ratio 기반 컴팩션
- 3단계: 에피소드 요약 압축(대표 벡터) + 원문 아카이브 분리
- 4단계: 중복 제거, 정밀도/양자화, 최근성 재랭킹
이미 런타임/DB 레벨에서 팽창이 심하다면, 벡터DB만 보지 말고 체크포인트/컴팩션을 포함한 저장소 전반을 함께 점검하세요. 특히 SQLite나 로컬 스토어가 섞여 있다면 AutoGPT 메모리 누수, SQLite 체크포인트로 잡기에서 다룬 체크포인트/로그 회수 이슈가 같이 터지는 경우가 많습니다.