- Published on
AutoGPT 메모리 폭주? 벡터DB+요약압축 튜닝
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
AutoGPT류 에이전트를 며칠만 돌려보면 공통적으로 겪는 문제가 있습니다. 대화 히스토리, 중간 사고(계획), 도구 실행 로그, 웹 스크랩, 파일 내용이 끝없이 누적되면서 컨텍스트 토큰이 폭증하고, 그 결과 비용·지연·오류율이 같이 치솟습니다. 이를 흔히 “메모리 폭주”라고 부르는데, 실제 원인은 대개 다음 두 가지입니다.
- 저장(Write)이 쉬운 구조로만 설계되고, 검색(Read) 제약이 없다
- “기억”을 그대로 붙여넣는 방식이라 요약·압축 계층이 없다
이 글에서는 AutoGPT 메모리 폭주를 벡터DB 설계(검색 제어) 와 요약 압축(토큰 제어) 로 잡는 튜닝을 다룹니다. 또한 에이전트가 도구를 무한히 호출하거나 로그를 끝없이 쌓는 문제까지 함께 완화하는 운영 팁도 포함합니다.
관련해서 에이전트가 루프에 빠져 메모리를 더 빨리 불리는 경우가 많습니다. 아래 글의 “루프 차단” 아이디어를 같이 적용하면 효과가 큽니다.
메모리 폭주의 전형적인 증상과 원인
증상
- 프롬프트에 붙는 히스토리가 길어져 응답이 느려짐
- LLM 호출 비용이 급격히 증가
- “관련 없는 과거 정보”가 섞여 환각/오답 증가
- 벡터 검색 결과가 매번 달라지고, 중요한 사실이 묻힘
- 장기 실행 시 프로세스 RSS 증가(캐시, 로그, 임베딩 배치 등)
원인 (대부분 구조적)
- 모든 것을 동일한 중요도로 저장
- 작업 메모, 장기 메모, 관찰 로그, 웹페이지 전문이 한 컬렉션에 섞임
- 검색 범위 제한이 없음
top_k만 정하고 기간, 작업, 출처, 신뢰도 필터가 없음
- 요약이 “한 번”만 수행되거나 아예 없음
- 요약이 없으면 컨텍스트에 원문을 계속 붙이게 됨
- 요약이 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,filets: 타임스탬프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 + 요약 압축 파이프라인
아래는 구현을 단순화한 파이프라인 예시입니다.
- 사용자 입력 수신
conversation_summary+recent_messages구성- 벡터DB 검색
- 필터:
task_id,kind,source,ts범위 - 후보:
top_k=30 - 리랭킹 또는 MMR로 중복 제거
- 토큰 예산 800 내로 패킹
- LLM 호출
- 결과 저장
- 최근 메시지 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가지만 적용해도 장기 실행 시 비용과 지연이 눈에 띄게 안정화되고, “필요한 기억만 꺼내는” 에이전트에 가까워집니다.