Published on

AutoGPT 메모리 폭주, TTL·요약·RAG로 잡기

Authors

AutoGPT 같은 에이전트 프레임워크를 장시간 돌리다 보면 어느 순간부터 프롬프트가 비대해지고, 벡터DB가 끝없이 커지며, 결국 지연·비용·품질이 동시에 무너지는 구간을 만나게 됩니다. 흔히 “메모리 폭주”라고 부르지만, 실제로는 다음 3가지가 동시에 발생합니다.

  • 저장(Write) 폭주: 매 스텝/매 메시지/매 툴 결과를 전부 임베딩해 벡터DB에 적재
  • 검색(Read) 오염: 오래된/중복된/저품질 메모리가 검색 결과를 오염시켜 RAG 품질 하락
  • 컨텍스트(Compose) 비대화: 검색된 메모리를 그대로 프롬프트에 붙여 토큰이 폭증

이 글에서는 이를 TTL(수명 정책), 요약 기반 메모리(Compression), **RAG 검색 설계(Filtering·Re-ranking)**로 해결하는 실전 패턴을 정리합니다.

왜 AutoGPT 메모리가 폭주하는가

1) “기억”의 단위가 너무 작다

대부분의 구현이 메시지 1개 = 메모리 1개로 저장합니다. 그러면 하루만 돌려도 수천~수만 개의 조각이 생기고, 검색 시 유사도 상위 K에 우연히 비슷한 잡음이 섞이기 쉬워집니다.

2) 벡터DB는 기본적으로 “삭제”가 어렵다

많은 벡터DB/라이브러리는 시간 경과에 따른 자동 만료가 기본값이 아니거나, 삭제가 가능해도 운영적으로 누락되기 쉽습니다. 결과적으로 “영구 기억”이 되어 데이터가 쌓입니다.

3) 검색이 곧 프롬프트 비용이다

RAG는 검색 결과 텍스트를 프롬프트에 포함시키는 순간 비용이 발생합니다. 메모리가 쌓일수록 검색 결과도 길어지고, 토큰 비용과 지연이 선형 이상으로 증가합니다.

4) 장기 태스크에서 오류가 누적된다

에이전트는 잘못된 가정/실수도 메모리로 저장합니다. 시간이 지날수록 틀린 기억이 재검색되어 더 큰 오류를 만들 수 있습니다.

운영 관점에서 보면 이 문제는 애플리케이션 레벨의 메모리뿐 아니라, 실제로는 클러스터 메모리 압박으로도 이어집니다. 쿠버네티스 환경이라면 파드가 Insufficient memory로 Pending 되거나 OOMKill로 흔들리기도 합니다. 관련 점검은 EKS Pod가 Pending(Insufficient memory)일 때 점검법도 함께 참고하면 좋습니다.

해결 전략 1: 벡터DB TTL로 “기억의 유통기한”을 만든다

TTL의 핵심은 간단합니다.

  • 단기 기억(working memory): 며칠~몇 주 단위로 자동 만료
  • 장기 기억(long-term memory): 검증된 요약/결론/지식만 별도 저장

즉, “일단 다 저장”이 아니라 “일단 저장하되 자동 폐기”를 기본값으로 둡니다.

TTL 설계 체크리스트

  • 메모리 타입별 TTL 분리: 대화 로그, 툴 출력, 웹 스크랩, 실패 로그 등은 TTL을 다르게
  • 세션/태스크 스코프: 태스크가 끝나면 해당 스코프의 working memory를 강제 만료
  • 삭제가 아니라 아카이브: 규정/감사 요구가 있으면 원문은 오브젝트 스토리지로 옮기고 벡터 인덱스만 제거

예시: 메타데이터에 만료 시각 넣고 주기적으로 삭제

아래는 벡터DB가 TTL을 네이티브로 지원하지 않는 경우를 가정한 패턴입니다.

from datetime import datetime, timedelta, timezone

def build_memory_doc(text: str, kind: str, task_id: str, ttl_hours: int):
    now = datetime.now(timezone.utc)
    expires_at = now + timedelta(hours=ttl_hours)

    return {
        "text": text,
        "metadata": {
            "kind": kind,
            "task_id": task_id,
            "created_at": now.isoformat(),
            "expires_at": expires_at.isoformat(),
        }
    }

# 주기적 GC 작업(크론/워커)에서 만료 문서 삭제

def gc_expired(vector_store, now=None):
    now = now or datetime.now(timezone.utc)
    # 실제 구현은 DB별 필터 문법에 맞게 변경
    expired = vector_store.query(
        filter={"expires_at": {"$lt": now.isoformat()}}
    )
    for doc in expired:
        vector_store.delete(doc["id"])

포인트

  • TTL은 “비용 상한”을 만드는 가장 강력한 장치입니다.
  • 다만 TTL만으로는 품질 문제가 남습니다. 오래된 기억이 사라져도, 남아있는 기억이 여전히 “조각”이면 검색 품질이 흔들립니다. 그래서 요약이 필요합니다.

해결 전략 2: 요약 메모리로 저장 단위를 “압축”한다

요약은 단순히 토큰을 줄이는 문제가 아니라, 검색 가능한 지식 단위로 재구성하는 작업입니다.

어떤 것을 요약해야 하나

  • 반복되는 대화: 결론과 결정사항만 남기기
  • 툴 출력: 핵심 수치/에러 원인/재현 조건만 남기기
  • 웹 문서: 원문을 저장하지 말고, 질문에 답이 되는 사실만 추출

요약 메모리의 권장 스키마

  • facts: 검증된 사실
  • decisions: 의사결정과 근거
  • todos: 남은 작업
  • risks: 위험/가정/불확실성
  • sources: 근거 링크나 파일 해시

이렇게 구조화하면, 검색 시에도 “무엇을 가져올지”가 선명해져 프롬프트가 짧아집니다.

예시: 일정량 쌓이면 롤업 요약을 만들고 원문은 TTL로 흘려보내기

def should_rollup(messages, max_messages=30, max_chars=20000):
    total_chars = sum(len(m["content"]) for m in messages)
    return len(messages) >= max_messages or total_chars >= max_chars

ROLLUP_PROMPT = """
너는 에이전트 메모리 관리자다.
아래 대화/로그를 요약해 구조화된 메모리로 재작성하라.

출력 JSON 키:
- facts: 배열
- decisions: 배열
- todos: 배열
- risks: 배열

원문을 그대로 복사하지 말고, 중복을 제거하라.
"""

def rollup_summarize(llm, messages):
    text = "\n".join(f"- {m['role']}: {m['content']}" for m in messages)
    result = llm.generate(ROLLUP_PROMPT + "\n\n" + text)
    return result  # JSON 문자열이라고 가정

운영 팁:

  • 롤업은 매 턴 하지 말고 임계치 기반으로 실행하세요.
  • 롤업 결과는 장기 기억 저장소(별도 컬렉션)에 넣고, 원문은 TTL로 자연 소멸시키는 구성이 안정적입니다.

해결 전략 3: RAG 검색을 “필터링·리랭킹·예산”으로 통제한다

메모리 폭주의 마지막 고리는 검색입니다. 저장과 요약을 잘해도, 검색이 무분별하면 프롬프트가 다시 비대해집니다.

1) 메타데이터 필터링을 기본값으로

유사도 검색 전에 다음을 필터로 거르세요.

  • task_id 또는 project_id
  • kind (예: decision, fact, tool_error)
  • expires_at 유효성
  • confidence (검증된 메모리만)

2) Top-K가 아니라 “토큰 예산”으로 자르기

K=10 같은 고정값은 문서 길이에 따라 비용이 폭주합니다. 대신 “최대 N 토큰/문자” 예산으로 잘라야 합니다.

def pack_context(docs, max_chars=6000):
    packed = []
    total = 0
    for d in docs:
        chunk = d["text"].strip()
        if total + len(chunk) > max_chars:
            break
        packed.append(chunk)
        total += len(chunk)
    return "\n\n".join(packed)

3) 리랭킹으로 “진짜 관련 있는 것”만 남기기

벡터 유사도 상위 결과는 종종 “문장 스타일”이 비슷한 잡음을 올립니다. 가능하면 cross-encoder 리랭커 또는 LLM 기반 스코어링을 얹어 2차 정렬하세요.

  • 1차: 임베딩 검색(빠름)
  • 2차: 리랭킹(정확)
  • 3차: 예산 기반 패킹(비용 상한)

4) 실패 로그는 별도 인덱스로 격리

에이전트는 실패도 많이 기록합니다. 실패 로그가 일반 지식 메모리와 섞이면 “에러 메시지”가 자주 검색되어 프롬프트를 오염시킵니다.

  • kind=error 컬렉션 분리
  • 검색 시 기본 제외, 디버그 모드에서만 포함

이런 “격리” 전략은 분산 시스템에서 장애 원인을 좁힐 때도 유사하게 쓰입니다. 예를 들어 타임아웃이 늘어날 때 원인을 구조적으로 분리하는 접근은 Go gRPC 데드라인 초과 원인과 해결 가이드에서 소개하는 진단 흐름과도 결이 같습니다.

추천 아키텍처: 2-레벨 메모리 + RAG 게이트

현장에서 가장 안정적인 구성은 아래처럼 메모리를 계층화하는 방식입니다.

레벨 1: Working Memory (TTL 필수)

  • 원문 메시지, 툴 출력, 스크랩
  • TTL: 짧게(예: 24시간~7일)
  • 목적: 최근 맥락 유지, 디버깅

레벨 2: Long-term Memory (요약만)

  • 롤업 요약, 결정사항, 검증된 사실
  • TTL: 길게 또는 무기한
  • 목적: 재사용 가능한 지식 축적

RAG 게이트(검색 정책)

  • 기본은 Long-term 위주
  • Working은 “최근 N시간” 또는 “현재 태스크 범위”에서만 제한적으로
  • 컨텍스트 예산으로 강제 컷

운영 관점 체크리스트

비용·지연

  • 임베딩 호출량: “저장 단위”가 커질수록 감소
  • 검색 호출량: 태스크 단계별로 필요한 순간에만
  • 프롬프트 토큰: 예산 기반 패킹으로 상한 설정

품질

  • 요약 품질: 구조화 스키마로 안정화
  • 잘못된 기억: confidence 필드 도입, 검증된 것만 장기 저장

안정성

  • GC 작업의 실패 대비: 삭제 작업은 idempotent하게
  • 장애 시 재처리: 롤업/GC는 재실행해도 결과가 일관되게

CI에서 캐시가 의도대로 동작하지 않아 빌드 시간이 폭증하는 것처럼, “메모리 캐시(벡터DB)”도 정책이 없으면 비용이 새어 나갑니다. 캐시 운영 감각은 GitHub Actions 캐시가 안 먹을 때 터지는 7가지 함정에서 말하는 점검 방식과도 통합니다.

마무리: 폭주를 막는 핵심은 “기본값”이다

AutoGPT 메모리 폭주는 특정 벡터DB나 특정 프레임워크의 문제가 아니라, 설계의 기본값이 “영구 저장 + 무제한 검색 + 무제한 컨텍스트”로 되어 있을 때 필연적으로 발생합니다.

정리하면 우선순위는 다음과 같습니다.

  1. TTL로 저장량 상한을 만든다: working memory는 자동 만료
  2. 요약으로 지식 단위를 키운다: 조각을 줄이고 구조화한다
  3. RAG 게이트로 검색을 통제한다: 필터링·리랭킹·예산 패킹

이 3가지를 적용하면 “오래 돌릴수록 더 똑똑해지는 에이전트”에 가까워지고, 최소한 “오래 돌릴수록 느려지고 비싸지는” 상태는 확실히 벗어날 수 있습니다.