- Published on
AutoGPT 메모리 폭주? 벡터DB로 비용 절감
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
AutoGPT 류 에이전트를 운영하다 보면 초반에는 잘 돌아가다가, 일정 시간이 지나면 응답이 느려지고 비용이 급격히 증가하는 구간을 만나기 쉽습니다. 겉으로는 “메모리 폭주”처럼 보이지만, 실제로는 컨텍스트(프롬프트) 누적과 무분별한 회상(recall) 때문에 토큰이 불어나고, 그 결과 지연과 비용이 함께 커지는 패턴이 대부분입니다.
이 글에서는 AutoGPT의 메모리 구조를 운영 관점에서 해부하고, 벡터DB를 장기기억으로 쓰되 ‘무조건 많이 넣고 많이 꺼내는’ 방식이 아니라 비용/지연을 통제하는 설계로 바꾸는 방법을 정리합니다.
왜 AutoGPT는 시간이 갈수록 비싸지고 느려질까
1) 단기기억이 사실상 무한히 커진다
많은 구현에서 단기기억은 “최근 대화/행동 로그를 그대로 프롬프트에 붙이는 방식”입니다. 에이전트가 50턴, 200턴을 넘어가면, 매 호출마다 포함되는 텍스트가 선형으로 증가합니다.
- 입력 토큰 증가
=>모델 비용 증가 - 입력 토큰 증가
=>추론 지연 증가 - 모델이 중요한 정보를 찾기 어려워져 품질도 흔들림
2) ‘기억’이 정보가 아니라 로그가 된다
에이전트가 남기는 로그에는 다음이 섞여 있습니다.
- 실제로 중요한 사실(사용자 선호, 목표, 제약)
- 일시적인 중간 산출물
- 실패한 시도와 에러 메시지
- 반복되는 상태 메시지
이걸 그대로 누적하면 정작 중요한 정보 밀도는 낮아지고, 토큰만 늘어납니다.
3) 회상 단계가 토큰을 더 키운다
벡터 검색으로 관련 메모리를 top_k 개 가져와도, 그 메모리 자체가 길면 프롬프트는 다시 커집니다. 즉, “벡터DB를 붙였는데도 비용이 줄지 않는다”는 케이스는 대부분 아래 조합입니다.
- 메모리 조각이 너무 김
top_k가 과도함- 검색 결과를 그대로 프롬프트에 덕지덕지 붙임
해결 전략: 장기기억은 벡터DB, 단기기억은 압축
핵심은 간단합니다.
- 단기기억은 요약/압축해서 작게 유지
- 장기기억은 벡터DB에 넣되 짧고 원자적인 조각으로 저장
- 회상은
top_k가 아니라 예산 기반(토큰/문장 수)으로 제한 - 회상 결과는 그대로 붙이지 말고 근거만 추출하거나 재요약
이렇게 하면 “오래 돌수록 비싸지는” 곡선을 완만하게 만들 수 있습니다.
메모리 설계: 무엇을 저장하고, 무엇을 버릴까
저장 단위는 ‘로그’가 아니라 ‘사실’
벡터DB에 넣는 단위는 다음처럼 설계하는 것이 운영에 유리합니다.
- 사실: 사용자 선호, 시스템 제약, 외부 API 사용 규칙
- 결정: 중요한 의사결정과 그 근거(짧게)
- 결과: 최종 산출물의 핵심(링크, ID, 결론)
반대로 아래는 대개 저장 가치가 낮습니다.
- 스택 트레이스 전문
- 동일한 재시도 로그
- 모델의 장황한 자기설명
메모리 스키마 예시
메모리 조각을 저장할 때는 검색 품질과 비용 통제를 위해 메타데이터를 적극적으로 씁니다.
type:fact/decision/result/errorimportance:1..5source:user/tool/agentttl_days: 만료 정책token_len: 길이(사전 계산)
이 메타데이터는 나중에 “검색 결과를 얼마나 넣을지”를 결정하는 필터로도 쓰입니다.
벡터DB 선택과 인덱스 튜닝 포인트
운영에서 자주 겪는 문제는 “검색이 느려서 회상 단계가 병목”이 되는 것입니다. 이때는 벡터DB의 인덱스와 파라미터가 지연을 좌우합니다.
- HNSW 계열은
efSearch,M값에 따라 recall 과 p99 지연이 크게 달라짐 - 무작정 recall 을 올리면 회상 결과가 늘고, 결국 프롬프트 토큰이 다시 늘어 비용이 증가
Milvus 를 쓴다면 HNSW 튜닝으로 p99 를 낮추는 접근이 특히 효과적입니다. 자세한 파라미터 감각은 아래 글이 도움이 됩니다.
비용 절감의 핵심: top_k 가 아니라 ‘토큰 예산’
많은 구현이 회상 단계에서 top_k=10 같은 상수를 씁니다. 하지만 메모리 조각 길이가 매번 다르면, 실제로 프롬프트에 추가되는 토큰은 통제되지 않습니다.
권장 방식은 아래처럼 토큰 예산 기반으로 회상 결과를 자르는 것입니다.
- 회상 예산: 예를 들어
800 tokens - 검색 결과를 점수 순으로 정렬
- 누적 토큰이 예산을 넘기기 직전까지 포함
Python 의사코드
아래 예시는 검색 결과를 “예산 내에서만” 프롬프트에 넣는 패턴입니다.
from dataclasses import dataclass
@dataclass
class MemoryHit:
text: str
score: float
token_len: int
importance: int
def select_hits_with_budget(hits: list[MemoryHit], token_budget: int) -> list[MemoryHit]:
# 점수 우선, 중요도 보정
hits = sorted(hits, key=lambda h: (h.score, h.importance), reverse=True)
selected = []
used = 0
for h in hits:
if h.token_len > token_budget:
continue
if used + h.token_len > token_budget:
break
selected.append(h)
used += h.token_len
return selected
이 한 가지 변경만으로도 “긴 메모리 조각 몇 개가 프롬프트를 터뜨리는” 문제를 상당히 줄일 수 있습니다.
회상 결과를 그대로 붙이지 말고 ‘근거만 남기기’
회상 히트를 그대로 프롬프트에 넣으면, 결국 다시 장황해집니다. 운영에서 효과가 좋았던 패턴은 다음 중 하나입니다.
- 근거 추출: 회상 텍스트에서 필요한 문장만 뽑아 1~3줄로 축약
- 재요약 저장: 원문은 벡터DB에 유지하되, 프롬프트에는 “요약본”만 넣고 원문은 필요할 때만 참조
요약 파이프라인 예시
- 원문 메모리 저장
- 별도 필드로
summary생성(짧게) - 검색 시에는
summary를 우선 사용 - 정말 필요할 때만 원문을 fetch
이 구조는 “검색 recall 은 유지하면서도 프롬프트 토큰은 제한”하는 데 유리합니다.
메모리 폭주가 진짜 OOM 으로 이어질 때
AutoGPT를 컨테이너나 노드에서 돌리면, 토큰 비용뿐 아니라 프로세스 메모리도 실제로 증가할 수 있습니다.
- 대화 로그를 메모리에 계속 쌓음
- 임베딩 배치 작업을 큐에 쌓아두고 flush 를 안 함
- 벡터DB 클라이언트가 결과를 과도하게 캐시
이때는 애플리케이션 레벨 설계만큼이나, 시스템 레벨에서 OOM 원인을 빠르게 좁히는 것이 중요합니다.
운영 체크리스트: 비용과 품질을 같이 잡는 방법
1) 관측 지표를 먼저 고정
최소한 아래는 꼭 수집하세요.
- 요청당 입력 토큰, 출력 토큰
- 회상 단계에서 추가된 토큰(검색 결과 합)
- 검색 p50/p95/p99 지연
top_k요청 분포(혹은 예산 사용량)- 메모리 조각 평균 길이, p95 길이
2) 메모리 조각 길이를 강제
저장 시점에 길이를 제한하면, 회상 비용이 예측 가능해집니다.
- 예: 메모리 원문은 1,000자 제한
- 요약은 300자 제한
- 초과분은 잘라 저장하거나 별도 아카이브로 분리
3) TTL 과 중요도 기반 삭제
모든 기억이 영구 보관될 필요는 없습니다.
error타입은ttl_days=7fact타입은ttl_days=180importance가 낮은 것은 더 빨리 만료
4) 회상은 단계별로
한 번에 많이 가져오지 말고, 실패했을 때만 확장하는 방식이 비용에 유리합니다.
- 1차: 예산
300 tokens - 답이 부족하면 2차: 예산
600 tokens - 그래도 부족하면 도구 호출이나 원문 fetch
예시 아키텍처: AutoGPT 메모리 파이프라인
구성 요소를 단순화하면 아래 흐름입니다.
- 단기기억: 최근 대화
N턴 + 요약 1개 - 장기기억 저장: 이벤트 발생 시
fact/decision/result로 정규화 - 임베딩 생성: 비동기 배치 처리
- 벡터 검색: 쿼리 임베딩으로 후보 검색
- 예산 기반 선택: 토큰 예산 내에서 히트 선택
- 프롬프트 구성: 선택 히트는
summary만 포함
프롬프트 구성 의사코드
아래는 “단기 요약 + 예산 기반 장기기억”을 합치는 예시입니다.
def build_prompt(system_rules: str, short_summary: str, recent_msgs: list[str], memory_summaries: list[str]) -> str:
sections = []
sections.append("[SYSTEM RULES]\n" + system_rules)
sections.append("[SHORT SUMMARY]\n" + short_summary)
sections.append("[RECENT]\n" + "\n".join(recent_msgs[-6:]))
if memory_summaries:
sections.append("[RECALLED MEMORY]\n" + "\n".join(f"- {s}" for s in memory_summaries))
return "\n\n".join(sections)
포인트는 recent_msgs 를 무한히 늘리지 않고, 장기기억도 원문이 아니라 summary 위주로 넣는 것입니다.
마무리: 벡터DB는 ‘기억을 늘리는 도구’가 아니라 ‘기억을 압축하는 도구’
AutoGPT의 비용 폭주는 대개 “기억이 많아서”가 아니라 “기억을 텍스트로 그대로 들고 다녀서” 발생합니다. 벡터DB를 붙이는 것만으로는 해결되지 않고, 아래 3가지를 함께 해야 비용 곡선이 꺾입니다.
- 저장 단위를 로그가 아닌 사실로 쪼개기
- 회상은
top_k가 아니라 토큰 예산으로 제한하기 - 회상 결과는 원문이 아니라 요약/근거로 주입하기
이 구조로 바꾸면 에이전트가 오래 돌아도 프롬프트가 안정적으로 유지되고, 검색 지연과 토큰 비용을 함께 제어할 수 있습니다.