Published on

AutoGPT 메모리 폭주 해결 - 벡터DB 압축·TTL

Authors

AutoGPT류 에이전트를 운영하다 보면 초반에는 잘 돌아가다가, 몇 시간~며칠 뒤 갑자기 느려지거나 비용이 폭증하고 심하면 프로세스가 죽는 현상을 겪습니다. 원인은 대부분 메모리(기억) 레이어가 무한히 누적되기 때문입니다.

여기서 말하는 메모리는 RAM만이 아니라, 벡터DB(임베딩 저장소), 요약/노트 저장소, 로그/툴 결과 캐시까지 포함합니다. 에이전트는 매 스텝에서 관찰(observation)과 결과를 기록하고, 이후 검색(RAG)으로 다시 끌어다 쓰는데, 이 과정이 통제되지 않으면 다음 문제가 연쇄적으로 발생합니다.

  • 벡터DB 레코드 수 증가로 검색이 느려짐(특히 topK가 커질수록)
  • 컨텍스트에 붙는 참조가 늘어 프롬프트 토큰이 증가(비용 증가)
  • 임베딩 생성/업서트가 병목이 되어 TPS 하락
  • 로컬 LLM/임베딩 모델을 함께 돌리면 OOM 위험 증가

이 글은 벡터DB 압축(Compression)TTL(Time To Live) 기반 수명 관리를 중심으로, AutoGPT 메모리 폭주를 “운영 가능한 수준”으로 만드는 방법을 정리합니다. 로컬 LLM 메모리 이슈와 결이 비슷하니, OOM 관점 최적화는 Transformers 로컬 LLM 로딩 OOM 9가지 해결도 함께 참고하면 좋습니다.

1) 메모리 폭주의 전형적인 증상과 원인

증상 체크리스트

  • 같은 작업을 반복할수록 점점 더 오래 걸림(검색 + 프롬프트 토큰 증가)
  • 벡터DB 디스크가 선형적으로 증가
  • topK를 줄이면 성능이 급락(관련 문서가 너무 많아 노이즈 증가)
  • “기억”을 많이 쓸수록 오히려 정답률이 떨어짐(오래된 정보가 최신 결정을 오염)

근본 원인

  1. 무한 append: 모든 관찰/결과를 그대로 벡터화하여 저장
  2. 중복/유사 데이터 난립: 같은 내용이 약간 다른 형태로 수십 번 저장
  3. 스코프 없는 기억: 작업 단위(세션/프로젝트/태스크) 경계가 약함
  4. 시간 개념 부재: 오래된 정보도 동일 가중치로 검색
  5. 압축/요약의 부재: 원문을 그대로 저장해 토큰·스토리지 낭비

해결의 핵심은 간단합니다.

  • 압축: “저장할 가치”가 있는 정보만 더 작게 저장
  • TTL: “유효 기간”이 지난 기억은 자동으로 줄이거나 삭제

2) 벡터DB 압축 전략 5가지

압축은 단일 기법이 아니라 파이프라인으로 보는 게 좋습니다. 아래를 조합하면 효과가 큽니다.

2.1 저장 전 요약(semantic distillation)

관찰/로그 원문을 그대로 벡터화하면 불필요한 토큰이 많습니다. 저장 전 단계에서 요약본 + 핵심 엔티티 + 결정 근거만 남기세요.

  • 원문: 툴 출력 전체, HTML, JSON 덩어리
  • 저장본: 3~7줄 요약 + 키워드/엔티티 + “다음 행동에 필요한 사실”

요약은 “사후 처리”가 아니라 업서트 직전에 강제해야 메모리 폭주를 막습니다.

2.2 중복 제거(near-duplicate)와 유사도 병합

같은 의미의 문장이 여러 번 들어오면 벡터DB는 급격히 비대해집니다.

  • 새 레코드 임베딩을 만든 뒤, 같은 namespace에서 topK=3 정도로 근접 이웃을 검색
  • 코사인 유사도가 0.92 이상이면 “새 레코드 추가” 대신 기존 레코드에 카운트/타임스탬프만 갱신

이렇게 하면 저장량이 줄고 검색 노이즈도 줄어듭니다.

2.3 청크 정책 재설계: 고정 길이보다 “의미 단위”

고정 길이 청크는 문장 경계를 깨고 중복을 만들기 쉽습니다.

  • 로그/문서: 섹션/헤더 단위
  • 대화: 턴 단위(질문+답) 또는 결정 단위
  • 툴 결과: “필요한 필드만 추출” 후 저장

청크가 작을수록 레코드 수가 늘고, 너무 크면 검색 정확도가 떨어집니다. 운영에서는 레코드 수를 통제하는 쪽이 중요하므로, “작게 많이”보다 “의미 단위로 적당히”가 유리한 경우가 많습니다.

2.4 임베딩 차원 축소(차원 다운) 또는 양자화

벡터 차원이 높을수록 저장 공간과 검색 비용이 증가합니다.

  • 차원 축소: PCA/OPQ 같은 기법(엔진에 따라 내장)
  • 양자화: int8/PQ(Product Quantization)

특히 대규모 운영에서는 정확도 손실을 감수하고 비용을 크게 줄이는 선택지가 됩니다.

2.5 메타데이터 최소화 + 원문은 콜드 스토리지로

벡터DB에는 “검색에 필요한 최소 텍스트”만 넣고, 원문은 별도 저장소(S3, R2, GCS 등)로 분리하세요.

  • 벡터DB: id, summary, tags, task_id, ts, importance
  • 오브젝트 스토리지: 원문 전체(필요 시에만 로드)

이 구조는 검색 속도와 비용을 안정화합니다.

3) TTL로 메모리 수명 관리하기

압축만으로는 부족합니다. 에이전트는 계속 새로운 경험을 만들기 때문에, 결국 “언젠가” 다시 비대해집니다. 그래서 TTL은 필수입니다.

3.1 TTL 설계의 3가지 스코프

  • 세션 TTL: 1~24시간. 단기 컨텍스트
  • 태스크 TTL: 7~30일. 프로젝트 단위 기억
  • 글로벌 TTL: 30~180일. 재사용 가능한 지식(규칙, 환경 설정)

모든 기억을 글로벌로 두면 폭주합니다. 기본은 “세션/태스크”에 가두고, 승격(promote) 조건을 만족하는 것만 글로벌로 올리세요.

3.2 Soft TTL vs Hard TTL

  • Hard TTL: 만료되면 삭제
  • Soft TTL: 만료되면 검색 점수에 페널티(예: score * decay(age)), 일정 기간 후 삭제

운영에서는 Soft TTL이 안전합니다. 오래된 기억을 완전히 잃으면 회귀(regression)가 생길 수 있기 때문입니다.

3.3 TTL과 중요도(importance) 결합

중요도 점수(0~1)를 두고 TTL을 동적으로 조절합니다.

  • 중요도 높음: TTL 연장
  • 중요도 낮음: TTL 단축

중요도는 다음 신호로 계산할 수 있습니다.

  • 사용 빈도(hit_count)
  • 최근 사용 시각(last_accessed_at)
  • “결정에 영향을 준 정도”(에이전트가 스스로 표기)

4) 점수 함수: 최신성·중요도·유사도 혼합

벡터 검색 결과를 그대로 쓰지 말고, 재랭킹 점수에 시간 감쇠를 넣으세요.

예시 점수:

  • final = sim * (0.7 + 0.3*importance) * exp(-age_days / half_life)

이렇게 하면 오래된 정보가 상위에 계속 남아 “현재 판단”을 오염시키는 현상이 줄어듭니다.

5) 파이썬 예제: 업서트 전 압축 + 유사 중복 병합 + TTL

아래 코드는 특정 벡터DB SDK에 종속되지 않도록, VectorStore 인터페이스 형태로 작성했습니다. 핵심은 다음 순서입니다.

  1. 원문을 요약해서 저장 텍스트를 줄임
  2. 업서트 전 근접 이웃을 검색해 중복이면 병합
  3. TTL/메타데이터를 함께 저장
from __future__ import annotations

import time
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Tuple


@dataclass
class VectorRecord:
    id: str
    text: str
    embedding: List[float]
    metadata: Dict[str, Any]


class VectorStore:
    def embed(self, text: str) -> List[float]:
        raise NotImplementedError

    def query(self, embedding: List[float], *, top_k: int, namespace: str) -> List[Tuple[str, float, Dict[str, Any]]]:
        """Return list of (id, similarity, metadata)."""
        raise NotImplementedError

    def upsert(self, record: VectorRecord, *, namespace: str) -> None:
        raise NotImplementedError

    def patch_metadata(self, record_id: str, patch: Dict[str, Any], *, namespace: str) -> None:
        raise NotImplementedError


def summarize_for_memory(raw: str, *, max_chars: int = 800) -> str:
    # 운영에서는 LLM 요약을 쓰되, 실패 시를 대비해 규칙 기반 폴백을 둡니다.
    raw = raw.strip()
    if len(raw) <= max_chars:
        return raw
    return raw[: max_chars - 30] + "\n... (truncated for memory)"


def now_ts() -> int:
    return int(time.time())


def upsert_memory(
    store: VectorStore,
    *,
    namespace: str,
    record_id: str,
    raw_text: str,
    task_id: str,
    ttl_seconds: int,
    importance: float = 0.3,
    dedup_threshold: float = 0.92,
) -> None:
    text = summarize_for_memory(raw_text)
    emb = store.embed(text)

    neighbors = store.query(emb, top_k=3, namespace=namespace)
    if neighbors:
        best_id, best_sim, best_meta = neighbors[0]
        if best_sim >= dedup_threshold and best_meta.get("task_id") == task_id:
            # 중복이면 새 레코드 추가 대신 메타데이터만 갱신
            patch = {
                "hit_count": int(best_meta.get("hit_count", 0)) + 1,
                "last_seen_at": now_ts(),
                "expires_at": now_ts() + ttl_seconds,
                "importance": max(float(best_meta.get("importance", 0.0)), importance),
            }
            store.patch_metadata(best_id, patch, namespace=namespace)
            return

    record = VectorRecord(
        id=record_id,
        text=text,
        embedding=emb,
        metadata={
            "task_id": task_id,
            "created_at": now_ts(),
            "last_seen_at": now_ts(),
            "expires_at": now_ts() + ttl_seconds,
            "importance": importance,
            "hit_count": 1,
        },
    )
    store.upsert(record, namespace=namespace)

포인트는 expires_at을 벡터DB 메타데이터로 들고 가는 것입니다. DB가 TTL 인덱스를 지원하면 그 기능을 쓰고, 지원하지 않으면 주기적 GC 작업에서 expires_at을 조건으로 삭제하면 됩니다.

6) 만료 GC 작업(크론/워커) 예제

TTL을 저장만 해서는 줄어들지 않습니다. 삭제/아카이브 작업이 있어야 합니다.

  • 만료된 레코드는 삭제
  • 중요도가 높거나 히트가 많은 레코드는 “장기 요약”으로 재압축 후 재저장(승격)
from typing import Iterable


class VectorAdmin(VectorStore):
    def scan_expired_ids(self, *, namespace: str, before_ts: int, limit: int) -> List[str]:
        raise NotImplementedError

    def delete(self, ids: List[str], *, namespace: str) -> None:
        raise NotImplementedError


def gc_expired(store: VectorAdmin, *, namespace: str, batch_size: int = 500) -> int:
    total_deleted = 0
    while True:
        expired_ids = store.scan_expired_ids(
            namespace=namespace,
            before_ts=now_ts(),
            limit=batch_size,
        )
        if not expired_ids:
            break
        store.delete(expired_ids, namespace=namespace)
        total_deleted += len(expired_ids)
    return total_deleted

운영에서는 GC 주기를 “1시간마다”처럼 촘촘히 두기보다, 쓰기량에 비례하도록 두는 게 안정적입니다(예: N건 업서트마다 한 번 GC, 또는 저장소 크기 임계 초과 시 GC).

7) 운영 팁: 폭주를 조기에 잡는 관측 지표

메모리 폭주는 늦게 발견할수록 복구가 어렵습니다. 아래 지표를 대시보드로 고정하세요.

  • vector_records_total (namespace/태스크별)
  • vector_db_size_bytes
  • embedding_upsert_latency_ms
  • query_latency_ms + topK
  • prompt_tokens_per_step / context_tokens_per_step
  • memory_hit_rate (검색 결과가 실제로 답변에 쓰였는지)

또한 레이트 리밋과 재시도 정책이 없으면 폭주가 “API 오류 폭주”로 번집니다. OpenAI 계열 API를 쓴다면 OpenAI 429 RateLimitError 재시도·백오프 설계처럼 백오프를 반드시 넣으세요.

8) 흔한 함정 6가지

8.1 TTL을 너무 짧게 잡아 에이전트가 금붕어가 됨

해결: Soft TTL + 중요도 기반 연장, 그리고 “승격” 레이어를 둡니다.

8.2 중복 제거를 안 해서 비용만 늘고 정확도는 떨어짐

해결: 업서트 전 topK 근접 이웃 검사로 병합.

8.3 모든 것을 벡터DB에 넣음(JSON 원문 포함)

해결: 벡터DB에는 요약만, 원문은 콜드 스토리지.

8.4 태스크 경계가 없어 다른 프로젝트 기억이 섞임

해결: namespace 또는 task_id 필터를 강제.

8.5 검색 결과를 그대로 컨텍스트에 붙임

해결: 재랭킹 + 최대 토큰 예산 + “근거 문장만 추출” 단계를 추가.

8.6 장기 실행 워커 자체가 메모리 누수

에이전트 프로세스가 장시간 떠 있으면 DB와 무관하게 누수가 날 수 있습니다. 특히 파이썬에서 캐시, 전역 리스트, 로그 버퍼, 세션 객체가 쌓입니다. Go 기반 도구를 쓴다면 누수 진단 관점으로 Go 고루틴 누수 5분 진단 - pprof·채널닫기 같은 접근(관측-원인 분리-주기적 정리)이 유사하게 적용됩니다.

9) 권장 아키텍처: 3-티어 메모리(Hot/Warm/Cold)

마지막으로, 운영에서 가장 안정적인 형태는 “계층화”입니다.

  • Hot(세션 메모리): 최근 N턴, TTL 짧음, 빠른 액세스
  • Warm(태스크 벡터DB): 요약+중복 제거, TTL 중간, 검색 대상
  • Cold(아카이브): 원문/전체 로그, TTL 길거나 무제한, 필요 시만 로드

이 구조에서 Warm가 비대해지지 않도록 압축 + TTL + GC를 걸면, AutoGPT의 기억은 “많이 저장하는 시스템”이 아니라 “필요한 것만 남기는 시스템”으로 바뀝니다.

마무리

AutoGPT 메모리 폭주는 단순히 저장 공간 문제를 넘어, 검색 노이즈 증가와 토큰 비용 폭증으로 직결됩니다. 해결은 두 축입니다.

  • 압축: 요약, 중복 병합, 청크 재설계, 차원/양자화, 콜드 스토리지 분리
  • TTL: 스코프 분리(세션/태스크/글로벌), Soft TTL, 중요도 기반 연장, 주기적 GC

이 두 가지를 파이프라인으로 묶고, 지표를 대시보드로 고정하면 “장기 실행 에이전트”도 운영 가능한 시스템이 됩니다.