Published on

AutoGPT 메모리 폭주? 벡터DB+요약압축 튜닝

Authors

AutoGPT류 에이전트를 며칠만 돌려보면 공통적으로 겪는 문제가 있습니다. 대화 히스토리, 중간 사고(계획), 도구 실행 로그, 웹 스크랩, 파일 내용이 끝없이 누적되면서 컨텍스트 토큰이 폭증하고, 그 결과 비용·지연·오류율이 같이 치솟습니다. 이를 흔히 “메모리 폭주”라고 부르는데, 실제 원인은 대개 다음 두 가지입니다.

  • 저장(Write)이 쉬운 구조로만 설계되고, 검색(Read) 제약이 없다
  • “기억”을 그대로 붙여넣는 방식이라 요약·압축 계층이 없다

이 글에서는 AutoGPT 메모리 폭주를 벡터DB 설계(검색 제어)요약 압축(토큰 제어) 로 잡는 튜닝을 다룹니다. 또한 에이전트가 도구를 무한히 호출하거나 로그를 끝없이 쌓는 문제까지 함께 완화하는 운영 팁도 포함합니다.

관련해서 에이전트가 루프에 빠져 메모리를 더 빨리 불리는 경우가 많습니다. 아래 글의 “루프 차단” 아이디어를 같이 적용하면 효과가 큽니다.


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

증상

  • 프롬프트에 붙는 히스토리가 길어져 응답이 느려짐
  • LLM 호출 비용이 급격히 증가
  • “관련 없는 과거 정보”가 섞여 환각/오답 증가
  • 벡터 검색 결과가 매번 달라지고, 중요한 사실이 묻힘
  • 장기 실행 시 프로세스 RSS 증가(캐시, 로그, 임베딩 배치 등)

원인 (대부분 구조적)

  1. 모든 것을 동일한 중요도로 저장
  • 작업 메모, 장기 메모, 관찰 로그, 웹페이지 전문이 한 컬렉션에 섞임
  1. 검색 범위 제한이 없음
  • top_k만 정하고 기간, 작업, 출처, 신뢰도 필터가 없음
  1. 요약이 “한 번”만 수행되거나 아예 없음
  • 요약이 없으면 컨텍스트에 원문을 계속 붙이게 됨
  • 요약이 1회성이라면 시간이 지나 다시 비대해짐
  1. 에이전트 루프
  • 같은 쿼리로 검색하고 같은 도구를 호출하며 로그를 누적

목표: “저장”이 아니라 “회수 가능한 기억” 만들기

튜닝 목표는 단순히 저장량을 줄이는 게 아니라, 다음 3가지를 동시에 만족하는 것입니다.

  • 회수 정확도: 필요한 정보만 잘 꺼내기
  • 토큰 예산: 프롬프트에 붙는 메모리를 일정하게 유지
  • 운영 비용: 임베딩/저장/검색 비용과 지연을 예측 가능하게

이를 위해 “메모리”를 한 덩어리로 보지 말고, 계층으로 나눕니다.

  • Working Memory: 현재 태스크에 필요한 짧은 상태(수십~수백 토큰)
  • Episodic Memory: 사건 단위 기록(요약 + 키 포인트)
  • Semantic Memory: 사실/지식 단위(정규화된 노트)
  • Artifacts: 파일, 코드, 웹문서 원문(원문은 별도 저장소)

벡터DB 튜닝 1: 컬렉션을 분리하고 스키마를 강제하기

모든 것을 한 컬렉션에 넣으면 검색 노이즈가 폭증합니다. 최소한 아래처럼 분리하세요.

  • mem_episodic: “무슨 일이 있었나” 중심
  • mem_semantic: “무엇이 사실인가” 중심
  • mem_tools: 도구 실행 결과(필요 시)
  • docs_raw_index: 원문 문서 청크(검색용)

그리고 메타데이터를 강제합니다.

  • task_id: 어떤 목표/프로젝트의 기억인지
  • source: chat, web, tool, file
  • ts: 타임스탬프
  • importance: 0~1 (휴리스틱 또는 모델 평가)
  • ttl_days: 보존 기간
  • hash: 중복 방지

예시: Pydantic으로 메모리 레코드 강제

from pydantic import BaseModel, Field
from typing import Literal, Optional
import time

class MemoryRecord(BaseModel):
    id: str
    task_id: str
    kind: Literal["episodic", "semantic", "tool", "doc"]
    text: str
    source: Literal["chat", "web", "tool", "file"]
    ts: float = Field(default_factory=lambda: time.time())
    importance: float = 0.3
    ttl_days: int = 30
    hash: Optional[str] = None

이렇게 해두면 “어떤 메모리가 왜 검색되는지”를 관찰 가능하게 만들 수 있고, 필터링도 쉬워집니다.


벡터DB 튜닝 2: 검색은 top_k가 아니라 “필터 + 예산”이다

메모리 폭주를 부르는 흔한 구현은 다음입니다.

  • 최근 N개 메시지 + 벡터 검색 top_k=10을 그대로 컨텍스트에 붙이기

이 방식은 시간이 지날수록 컨텍스트가 늘어납니다. 해결책은 “토큰 예산 기반”으로 회수량을 제한하는 것입니다.

원칙

  • top_k는 넉넉히 가져오되, 컨텍스트에 넣는 건 토큰 예산으로 컷
  • 시간/태스크/출처 필터를 먼저 적용
  • MMR(Maximal Marginal Relevance) 로 중복을 줄임

예시: 토큰 예산 기반 메모리 패킹

아래 코드는 “관련도 순으로 후보를 가져온 뒤, 800 토큰 예산 안에서만 메모리를 넣는” 단순 패턴입니다.

import tiktoken

enc = tiktoken.get_encoding("cl100k_base")

def count_tokens(s: str) -> int:
    return len(enc.encode(s))

def pack_memories(mem_texts, budget_tokens: int = 800):
    packed = []
    used = 0
    for t in mem_texts:
        n = count_tokens(t)
        if used + n > budget_tokens:
            break
        packed.append(t)
        used += n
    return "\n\n".join(packed), used

포인트는 “검색 결과 개수”가 아니라 “프롬프트에 넣는 총 토큰”을 상수로 만드는 것입니다.


벡터DB 튜닝 3: 청크 전략을 바꾸면 비용과 정확도가 같이 변한다

원문 문서를 벡터화할 때 청크가 너무 크면 검색이 둔해지고, 너무 작으면 저장량과 검색 노이즈가 늘어납니다.

권장 출발점(일반 텍스트 기준):

  • 청크 크기: 300~600 토큰
  • 오버랩: 40~80 토큰
  • 문서 타입별로 다르게: 코드/로그는 더 작게, 논문/가이드는 더 크게

또한 “원문 전체를 메모리로 취급”하지 말고, 원문은 docs_raw_index에 두고 에이전트 메모리에는 요약만 넣는 구조가 안정적입니다.


요약 압축 1: “점진 요약(rolling summary)”로 대화 히스토리 고정

대화가 길어질수록 히스토리를 그대로 붙이면 폭주합니다. 해결책은 “최근 대화 몇 개 + 누적 요약” 패턴입니다.

  • recent_messages: 최근 6~12턴만 유지
  • conversation_summary: 그 이전은 누적 요약으로 압축

예시: 누적 요약 업데이트 프롬프트

요약은 “예쁘게”가 아니라 “재사용 가능한 상태”여야 합니다.

시스템: 너는 에이전트의 메모리 압축기다.
규칙:
- 사실/결정/미해결 TODO/제약/사용자 선호만 남겨라.
- 중복은 제거하고, 모호하면 "불확실"로 표기해라.
- 250~350 토큰 사이로 유지해라.

입력:
[기존 요약]
{old_summary}

[새 대화 구간]
{new_messages}

출력:
업데이트된 요약만 출력해라.

이 요약은 벡터DB에도 저장해두고, 프롬프트에는 “항상” 들어가게 하면 장기 실행 안정성이 크게 좋아집니다.


요약 압축 2: “에피소드 요약”과 “사실 노트”를 분리

요약을 하나로만 만들면 두 가지가 섞입니다.

  • 사건 기록(언제 무엇을 했나)
  • 사실/지식(무엇이 참인가)

이를 분리하면 검색 품질이 좋아집니다.

  • 에피소드 요약: 작업 진행 상황, 결정, 근거 링크
  • 사실 노트: 재사용 가능한 규칙/설정/정책/키 값

예시: 에피소드 요약 JSON 스키마

MDX에서 부등호 노출을 피하기 위해 스키마는 코드 블록으로만 제공합니다.

{
  "task_id": "project-x",
  "episode": {
    "title": "벡터DB 필터링 도입",
    "decisions": ["mem_semantic 컬렉션 분리"],
    "todos": ["importance 스코어링 추가"],
    "constraints": ["메모리 컨텍스트 예산 800 tokens"],
    "evidence": ["internal-doc:mem-design-v1"],
    "timestamp": 1730000000
  }
}

요약 압축 3: 중요도 기반 TTL과 “승격(promote)” 파이프라인

모든 메모리를 영구 보존하면 결국 다시 폭주합니다. 운영 관점에서 가장 효과적인 방법은 TTL + 승격입니다.

  • 기본은 짧은 TTL(예: 7~30일)
  • 중요도가 높은 항목만 장기 보존 컬렉션으로 승격

중요도는 휴리스틱으로도 충분히 시작할 수 있습니다.

  • 사용자가 “반드시 기억”, “다음부터”, “항상” 같은 표현 사용
  • API 키/설정/정책/제약 등 재사용성이 높은 내용
  • 동일 항목이 여러 번 참조됨(조회 카운트)

예시: 승격 규칙(간단)

def should_promote(text: str, importance: float, access_count: int) -> bool:
    keywords = ["항상", "반드시", "기억", "규칙", "제약", "설정"]
    if any(k in text for k in keywords):
        return True
    if importance >= 0.75:
        return True
    if access_count >= 3:
        return True
    return False

검색 품질을 지키는 핵심: “요약을 벡터화”하고 “원문은 링크로”

요약 압축을 하면 정보가 손실될 수 있습니다. 이를 보완하는 패턴은 다음입니다.

  • 요약(짧은 텍스트)은 벡터화해서 mem_episodic 또는 mem_semantic에 저장
  • 원문은 별도 저장소(S3, DB, 파일)로 두고, 요약에 artifact_id 같은 참조를 남김

에이전트는 평소에는 요약만 컨텍스트에 넣고, 필요할 때만 원문을 가져옵니다. 이때도 원문 전체를 넣지 말고 “관련 청크만” 넣습니다.


메모리 폭주를 더 키우는 2가지: 무한 루프와 프롬프트 누출

1) 도구 호출 무한 루프

검색 결과가 마음에 들지 않으면 에이전트가 같은 쿼리를 반복하고 로그가 계속 쌓입니다. 아래 두 가지를 강제하면 폭주를 크게 줄일 수 있습니다.

  • 동일 query_hash로 연속 N회 실패 시 중단
  • tool_budget(호출 횟수/시간/토큰) 상한

루프 차단은 아래 글을 함께 참고하세요.

2) 불필요한 사고(계획) 텍스트 저장

에이전트의 중간 사고를 그대로 저장하면 토큰도 늘고, 보안상 민감할 수 있습니다. “결론/결정/근거”만 저장하고 내부 추론은 최소화하세요. 프롬프트 방어 관점은 아래 글이 도움이 됩니다.


운영 체크리스트: 튜닝 전후를 수치로 비교하기

메모리 튜닝은 감으로 하면 실패합니다. 아래 지표를 최소로 로깅하세요.

  • 프롬프트 총 토큰(입력/출력)
  • 메모리 회수 토큰(메모리 섹션만 따로)
  • 벡터 검색 latency, 후보 개수, 필터 적용 후 개수
  • 메모리 중복률(동일 hash 비율)
  • 요약 길이(토큰)와 업데이트 주기
  • 도구 호출 횟수, 실패율, 동일 쿼리 반복 횟수

비용/리소스 최적화라는 점에서, GPU 메모리 최적화 글이지만 “OOM을 예산/상한으로 제어한다”는 사고방식은 유사합니다.


실전 구성 예시: 벡터DB + 요약 압축 파이프라인

아래는 구현을 단순화한 파이프라인 예시입니다.

  1. 사용자 입력 수신
  2. conversation_summary + recent_messages 구성
  3. 벡터DB 검색
  • 필터: task_id, kind, source, ts 범위
  • 후보: top_k=30
  • 리랭킹 또는 MMR로 중복 제거
  • 토큰 예산 800 내로 패킹
  1. LLM 호출
  2. 결과 저장
  • 최근 메시지 append
  • 일정 턴마다 conversation_summary 롤링 업데이트
  • 에피소드 요약 생성 후 mem_episodic 저장
  • 재사용 가능한 사실은 mem_semantic에 별도 저장
  • 원문/로그는 artifact로 저장하고 참조만 남김

예시: 메모리 섹션을 프롬프트에 주입하는 템플릿

부등호를 피하기 위해 템플릿은 일반 텍스트로만 구성합니다.

[CONVERSATION SUMMARY]
{conversation_summary}

[RELEVANT MEMORIES]
{packed_memories}

[RECENT MESSAGES]
{recent_messages}

[USER]
{user_input}

이 구조의 장점은 메모리 섹션이 항상 “예산 내”로 유지된다는 점입니다.


흔한 실패 패턴과 해결

실패 1: 요약이 너무 공격적이라 디테일이 사라짐

  • 해결: 요약을 2단으로
    • 1단: 300 토큰 내 상태 요약
    • 2단: 결정/키값/정책만 모은 “사실 노트”
  • 해결: 요약에 artifact_id를 남겨 원문 복구 경로 확보

실패 2: 벡터 검색이 과거 잡음을 계속 가져옴

  • 해결: ts 기반 decay(최근 가중치)
  • 해결: kind 별로 검색 쿼리를 다르게
    • 사실 질문이면 mem_semantic 우선
    • 진행 상황이면 mem_episodic 우선

실패 3: 중복 메모리가 폭증

  • 해결: 저장 전 정규화 + hash로 중복 제거
  • 해결: 유사도 기반 near-duplicate 제거(추가 비용 감안)

결론: 메모리 폭주는 “검색과 요약의 부재”가 만든다

AutoGPT 메모리 폭주는 모델이 멍청해서가 아니라, 시스템이 “기억을 쌓기만 하고 회수 전략이 없는” 상태에서 자연스럽게 발생합니다. 해결의 핵심은 다음 3줄로 요약됩니다.

  • 벡터DB는 컬렉션/메타데이터/필터로 검색 범위를 설계한다
  • 컨텍스트는 top_k가 아니라 토큰 예산으로 상한을 둔다
  • 대화와 로그는 점진 요약 + 에피소드/사실 분리로 압축한다

이 3가지만 적용해도 장기 실행 시 비용과 지연이 눈에 띄게 안정화되고, “필요한 기억만 꺼내는” 에이전트에 가까워집니다.