Published on

AutoGPT 메모리 폭주 해결 - Qdrant 튜닝 가이드

Authors

AutoGPT를 돌리다 보면 어느 순간부터 RAM이 치솟고, 디스크도 같이 불어나며, 결국 컨테이너가 OOMKilled로 죽는 패턴을 자주 봅니다. 표면적으로는 “LLM이 답을 길게 해서”처럼 보이지만, 실제로는 메모리(장기기억) 저장 전략 + 벡터DB(Qdrant) 설정 + 인덱싱/세그먼트 정책이 합쳐져 폭주하는 경우가 많습니다.

이 글은 AutoGPT류 에이전트가 Qdrant에 임베딩을 계속 쌓을 때 발생하는 메모리 폭주를 재현 가능한 체크리스트로 진단하고, Qdrant를 중심으로 튜닝하는 방법을 정리합니다.

폭주의 전형적 원인: “기억을 무한히 쌓는 구조”

AutoGPT 계열의 메모리 파이프라인은 대체로 다음 흐름입니다.

  1. 프롬프트/툴 결과/중간 사고(Chain-of-thought 유사 로그 포함)를 텍스트로 저장
  2. 임베딩 생성
  3. Qdrant에 upsert
  4. 다음 스텝에서 search로 유사 컨텍스트를 끌어옴

문제는 1~3이 제한 없이 반복될 때입니다. 특히 아래 조건이 겹치면 폭주가 빨라집니다.

  • 중간 로그(실패/재시도/툴 출력)가 길고 자주 저장됨
  • 동일한 사실/문서가 중복 저장됨(중복 제거 부재)
  • payload에 원문 전체를 저장하고, 반환 결과에서도 payload를 전부 가져옴
  • 컬렉션이 on_disk_payload=false로 커진 payload를 RAM에 오래 잡음
  • HNSW 인덱스가 너무 이른 시점에 빌드되어 메모리 사용량이 급증

먼저 확인할 지표: Qdrant가 “메모리”를 어디에 쓰는가

Qdrant에서 메모리 사용량은 크게 아래로 나뉩니다.

  • 벡터 저장소: 벡터 자체(예: 1536 float32)와 세그먼트 메타
  • HNSW 인덱스: 그래프 구조가 메모리를 많이 씀
  • payload 인덱스: 필터링을 위한 인덱스(필요할 때만)
  • payload 캐시: payload를 디스크에서 읽어올 때 캐시

따라서 “RAM이 늘었다”는 증상만으로는 원인을 특정하기 어렵고, 아래 순서로 접근하는 게 좋습니다.

  1. 포인트 수 증가 속도: 초당/분당 몇 건 upsert 되는지
  2. 벡터 차원과 dtype: 1536 float32면 포인트 100만에서 벡터만 대략 1536*4 바이트 단위로 커짐
  3. payload 크기: 원문을 통째로 넣으면 벡터보다 payload가 더 커질 수도 있음
  4. 인덱싱 타이밍: 인덱스가 빌드되는 순간 메모리 스파이크가 발생

운영 중 디스크가 줄지 않는 이슈가 함께 보이면, 로그/스냅샷/삭제 파일 핸들 점유도 같이 의심해야 합니다. 이런 케이스는 logrotate 후 디스크가 안 줄 때 - 삭제 파일 점유 해결도 함께 참고하면 좋습니다.

핵심 처방 1: 컬렉션 스키마를 “메모리 친화적”으로 다시 만든다

AutoGPT 메모리 컬렉션을 만들 때, 기본값으로 두면 payload가 RAM에 남거나 인덱스가 과하게 잡히는 경우가 있습니다. 아래는 튜닝 포인트입니다.

1) payload는 디스크로: on_disk_payload=true

payload에 원문 텍스트를 넣는다면, RAM에 올려두는 순간 폭주의 주범이 됩니다. 가능하면 payload는 디스크에 두고, 필요할 때만 읽게 하세요.

curl -X PUT "http://localhost:6333/collections/autogpt_memory" \
  -H 'Content-Type: application/json' \
  -d '{
    "vectors": {"size": 1536, "distance": "Cosine"},
    "on_disk_payload": true
  }'

이미 컬렉션이 있다면, 운영에서는 마이그레이션이 필요할 수 있습니다. 신규 컬렉션을 만들고 배치로 옮기는 편이 안전합니다.

2) 벡터를 디스크로: on_disk=true (대규모일 때)

포인트 수가 수십만~수백만을 넘어가면 벡터 자체도 부담입니다. Qdrant는 벡터를 디스크에 두는 옵션을 제공합니다.

curl -X PATCH "http://localhost:6333/collections/autogpt_memory" \
  -H 'Content-Type: application/json' \
  -d '{
    "optimizers_config": {
      "default_segment_number": 2
    },
    "vectors": {
      "on_disk": true
    }
  }'

디스크 기반은 latency가 증가할 수 있으니, “장기기억” 영역에만 적용하고 “작업 메모리”는 별도 컬렉션으로 RAM 기반을 유지하는 식의 분리가 실무에서 효과적입니다.

3) HNSW 파라미터를 줄여 메모리 상한을 잡는다

HNSW는 mef_construct가 메모리와 빌드 비용에 큰 영향을 줍니다.

  • m: 그래프 연결 수. 크면 recall은 좋아지지만 메모리 증가
  • ef_construct: 인덱스 빌드 품질. 크면 빌드 시간/메모리 증가

AutoGPT 메모리는 “완벽한 검색”보다 “대충 관련 있는 기억”이 중요할 때가 많아서, 과한 HNSW는 낭비가 됩니다.

curl -X PATCH "http://localhost:6333/collections/autogpt_memory" \
  -H 'Content-Type: application/json' \
  -d '{
    "hnsw_config": {
      "m": 8,
      "ef_construct": 64,
      "full_scan_threshold": 20000
    }
  }'

full_scan_threshold를 적절히 두면, 데이터가 작을 때는 인덱스 없이 스캔하고 커지면 인덱스를 쓰는 식으로 전환되어 초기 메모리 스파이크를 줄이는 데 도움이 됩니다.

핵심 처방 2: “무한 적재”를 막는 데이터 전략(중복 제거 + TTL)

Qdrant 튜닝만으로도 개선되지만, AutoGPT 메모리 폭주의 본질은 데이터가 계속 쌓이는 것입니다. 다음 두 가지가 가장 효과가 큽니다.

1) 중복 제거: 해시 기반 id를 강제한다

동일 문장을 매번 새 포인트로 넣지 말고, 내용 기반 해시를 id로 사용하면 upsert가 덮어쓰게 만들 수 있습니다.

import hashlib
from qdrant_client import QdrantClient
from qdrant_client.http.models import PointStruct

client = QdrantClient(url="http://localhost:6333")


def content_id(text: str) -> str:
    return hashlib.sha256(text.encode("utf-8")).hexdigest()


def upsert_memory(collection: str, text: str, vector: list[float], meta: dict):
    pid = content_id(text)
    client.upsert(
        collection_name=collection,
        points=[
            PointStruct(
                id=pid,
                vector=vector,
                payload={
                    "text": text,
                    **meta,
                },
            )
        ],
    )

이때 인코딩 문제가 섞이면 해시가 달라져 중복 제거가 깨질 수 있습니다. 크롤링/파일 입력을 섞어 쓰는 파이프라인이라면 Python UnicodeDecodeError - utf-8 실전 해결법처럼 입력을 utf-8로 정규화하는 습관이 중요합니다.

2) TTL(수명) 설계: “기억”에도 유통기한을 둔다

AutoGPT 메모리는 보통 다음 두 레이어로 나누면 안정적입니다.

  • 작업 메모리: 최근 N시간~N일 (짧은 TTL)
  • 장기 메모리: 요약/정제된 사실만 (긴 TTL 또는 수동 유지)

Qdrant 자체 TTL 기능은 버전/구성에 따라 접근 방식이 달라질 수 있어, 실무에서는 payload에 created_at을 넣고 주기적으로 삭제하는 배치가 가장 단순합니다.

import time
from qdrant_client import QdrantClient
from qdrant_client.http import models

client = QdrantClient(url="http://localhost:6333")


def delete_older_than(collection: str, max_age_seconds: int):
    threshold = int(time.time()) - max_age_seconds

    client.delete(
        collection_name=collection,
        points_selector=models.FilterSelector(
            filter=models.Filter(
                must=[
                    models.FieldCondition(
                        key="created_at",
                        range=models.Range(lte=threshold),
                    )
                ]
            )
        ),
    )

이 배치를 크론으로 하루 1회만 돌려도 폭주를 크게 줄일 수 있습니다.

핵심 처방 3: payload를 “검색용”과 “표시용”으로 분리한다

AutoGPT는 검색 결과에서 payload를 그대로 프롬프트에 붙이는 경우가 많습니다. 이때 payload가 크면

  • Qdrant에서 읽어오는 비용 증가
  • 네트워크 전송량 증가
  • LLM 컨텍스트 낭비

가 동시에 발생합니다.

권장 패턴은 다음입니다.

  • Qdrant payload에는 요약문/키워드/문서 ID만 저장
  • 원문은 별도 스토리지(PostgreSQL, S3, 파일)로 저장
  • 필요할 때만 원문을 가져오고, 프롬프트에는 요약을 우선 사용

예시 payload 설계:

{
  "doc_id": "kb-2026-02-25-001",
  "summary": "장기기억: Qdrant on_disk_payload로 RAM 폭주를 줄인다.",
  "tags": ["qdrant", "autogpt", "memory"],
  "created_at": 1700000000
}

그리고 search 시에는 with_payload를 최소화합니다(클라이언트 옵션으로 필요한 필드만 가져오기).

핵심 처방 4: 필터 인덱스는 필요한 것만 만든다

payload 필드에 대해 무분별하게 인덱스를 만들면 메모리/디스크가 늘고, 세그먼트 최적화 비용도 증가합니다.

AutoGPT 메모리에서 인덱싱 가치가 큰 필드는 보통 아래 정도입니다.

  • created_at (TTL 삭제/기간 필터)
  • tags 또는 namespace (프로젝트/에이전트 분리)
  • doc_type (요약 vs 원문 vs 로그 구분)

필드 인덱스 생성 예시:

curl -X PUT "http://localhost:6333/collections/autogpt_memory/index" \
  -H 'Content-Type: application/json' \
  -d '{
    "field_name": "created_at",
    "field_schema": "integer"
  }'

핵심 처방 5: 세그먼트/옵티마이저 튜닝으로 “스파이크”를 줄인다

Qdrant는 백그라운드에서 세그먼트 머지/최적화를 수행합니다. 이 과정에서 CPU/IO/RAM 스파이크가 발생할 수 있고, AutoGPT처럼 지속적으로 쓰기(upsert)가 들어오면 스파이크가 더 잦아집니다.

접근 방법:

  • 너무 잦은 최적화를 피하도록 옵티마이저 임계값 조정
  • 스냅샷/백업 주기를 조절해 IO 경합을 줄임
  • 컨테이너 리소스 제한을 현실적으로 설정하고, OOM이 나면 Qdrant가 아닌 “에이전트”를 먼저 죽이도록 분리 배치

운영 환경에서 이런 리소스 경합이 DB 락/대기처럼 보일 때도 있습니다. 데이터 저장소를 PostgreSQL로 같이 쓰는 구조라면 PostgreSQL 락 대기 폭증? deadlock 진단·해결처럼 병목을 함께 점검하세요.

실전 구성 예시: 작업 메모리/장기 메모리 2컬렉션

가장 안정적인 패턴은 Qdrant 컬렉션을 2개로 쪼개는 것입니다.

  • autogpt_working_memory
    • 최근 대화/툴 결과 요약
    • TTL 짧음(예: 24~72시간)
    • 성능 우선(필요하면 RAM)
  • autogpt_long_memory
    • 검증된 사실/요약만 저장
    • TTL 길거나 수동 큐레이션
    • on_disk_payload=true, 필요 시 벡터도 디스크

검색은 보통 다음 순서가 좋습니다.

  1. 작업 메모리에서 top_k 작게(예: 5~10)
  2. 부족하면 장기 메모리에서 추가(예: 5)
  3. 합친 뒤 중복 제거 후 프롬프트에 삽입

이렇게 하면 장기 메모리의 규모가 커져도 작업 성능이 급격히 떨어지지 않습니다.

체크리스트: 튜닝 전후로 꼭 비교할 것

  • 포인트 증가량: 분당 upsert 수, 중복 제거 후 감소했는지
  • 평균 payload 크기: 원문을 빼고 요약으로 줄었는지
  • Qdrant RAM: on_disk_payload 적용 후 안정화되는지
  • 인덱스 빌드 스파이크: HNSW 파라미터 조정 후 피크가 줄었는지
  • 검색 품질: recall이 약간 떨어져도 에이전트 성능이 유지되는지(대부분 유지됨)

마무리

AutoGPT 메모리 폭주는 “Qdrant가 느리다/무겁다”의 문제가 아니라, 기억을 무제한 적재하는 제품 구조가 먼저이고, 그 다음이 Qdrant의 payload/인덱스/세그먼트 정책입니다.

정리하면 우선순위는 다음이 가장 효과적입니다.

  1. 중복 제거(내용 해시 id)로 쓰기량 자체를 줄이기
  2. TTL로 상한을 만들기
  3. on_disk_payload=true로 RAM 폭주 차단
  4. HNSW 파라미터를 보수적으로 조정
  5. 작업/장기 메모리 컬렉션 분리

이 5가지만 적용해도 AutoGPT를 장시간 돌릴 때 “처음엔 되다가 며칠 뒤 죽는” 패턴을 대부분 끊어낼 수 있습니다.