- Published on
AutoGPT 메모리 누적·환각 줄이는 RAG 설계
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
AutoGPT 스타일 에이전트는 장기 메모리(벡터DB) 를 붙이는 순간 성능이 좋아지는 듯 보이지만, 시간이 지날수록 메모리가 누적되고 검색 결과가 오염되면서 환각이 늘어나는 패턴을 자주 겪습니다. 원인은 간단합니다.
- 메모리 저장 단위가 너무 작거나(잡담, 중복 로그까지 저장) 너무 크고(한 덩어리로 뭉쳐 검색 정밀도 하락)
- 검색 쿼리가 부정확하거나(현재 목표와 무관한 키워드)
- 검색된 컨텍스트를 검증 없이 프롬프트에 주입하고
- 오래된 정보와 최신 정보가 섞여도 구분하지 않기 때문입니다.
이 글은 AutoGPT의 메모리 누적을 줄이면서 환각도 함께 낮추는 RAG 설계를, “벡터 검색 + 프롬프트” 수준이 아니라 파이프라인/데이터 모델/게이팅/평가까지 포함해 정리합니다. 메모리 폭주 자체를 다룬 글은 아래 글이 더 직접적이니 함께 보시면 좋습니다.
또한 추론이 꼬일 때 정확도를 끌어올리는 방법으로 self-consistency를 함께 쓰면 RAG의 “검색 노이즈”에 대한 내성이 좋아집니다.
문제 정의: AutoGPT에서 메모리가 왜 누적되고 환각이 늘까
AutoGPT류 에이전트의 루프는 대체로 다음을 반복합니다.
- 목표를 세운다
- 도구를 호출한다(웹, 코드 실행, DB, 파일)
- 결과를 관찰한다
- 다음 행동을 결정한다
- 유용해 보이는 정보를 메모리에 저장한다
여기서 “유용해 보이는 정보”를 저장하는 기준이 모호하면, 메모리에는 다음이 쌓입니다.
- 중복된 사실(같은 페이지를 여러 번 스크랩)
- 근거 없는 추정(모델이 생성한 중간 결론)
- 일시적인 상태(실패한 시도 로그)
- 오래된 정책/가격/스펙(시간 민감 정보)
이런 메모리가 벡터 검색으로 다시 주입되면, 모델은 그럴듯한 과거의 오염된 컨텍스트를 근거처럼 사용하며 환각이 강화됩니다. 즉, 환각은 “모델이 나빠서”가 아니라 메모리 데이터 품질과 검색/주입 설계가 나빠서 커지는 경우가 많습니다.
목표: 메모리와 RAG를 분리해 설계하기
핵심은 “메모리”를 하나로 보지 않고, 최소 3계층으로 분리하는 것입니다.
- 작업 메모리: 현재 루프에서 필요한 짧은 컨텍스트(최근 N턴)
- 요약 메모리: 세션/에피소드 요약(압축된 사실)
- 지식 메모리(RAG): 근거가 있는 문서/관찰/스냅샷을 검색해 주입
여기서 환각을 줄이는 RAG는 다음 원칙을 가집니다.
- 저장 단계에서 오염을 막는다(수집/정제/중복제거)
- 검색 단계에서 “현재 의도”를 반영한다(쿼리 재작성, 필터링)
- 주입 단계에서 근거를 강제한다(출처, 인용, 스코어 기반 게이팅)
- 생성 단계에서 불확실성을 제어한다(답변 거부/추가 탐색)
데이터 모델: 문서 단위와 메모리 단위를 다르게
AutoGPT에서 흔한 실수는 “대화 한 턴”을 그대로 벡터DB에 넣는 것입니다. 대신, RAG 인덱스는 문서 중심으로 설계하고, 대화/행동 로그는 별도 저장소에 두는 편이 안정적입니다.
아래는 실무에서 자주 쓰는 최소 스키마 예시입니다.
// TypeScript 예시: RAG 문서 레코드 스키마
export type RagDoc = {
id: string;
source: {
type: "web" | "file" | "tool" | "db";
uri: string; // URL, file path, tool name etc.
collectedAt: string; // ISO datetime
};
content: {
text: string;
chunkId: number;
chunkCount: number;
};
metadata: {
title?: string;
tags: string[];
taskId?: string;
episodeId?: string;
author?: string;
language?: string;
timeSensitivity?: "low" | "medium" | "high";
ttlSeconds?: number;
};
signals: {
// 검색/게이팅에 활용
qualityScore: number; // 0..1
factualityHint?: "observed" | "quoted" | "generated";
dedupHash?: string;
};
};
포인트는 factualityHint 입니다.
observed: 도구 실행 결과, 로그, DB 조회 등 “관찰”quoted: 웹 문서에서 발췌, 출처가 있는 인용generated: 모델이 만든 요약/추정(가급적 RAG 인덱스에 넣지 않거나 낮은 가중치)
환각이 늘어나는 시스템은 generated 가 observed 처럼 취급되는 경우가 많습니다.
수집 단계: 저장 게이트를 먼저 만들기
RAG가 망가지는 가장 큰 이유는 “검색이 못해서”가 아니라 “쓸데없는 걸 너무 많이 저장해서”입니다. 따라서 저장 시점에 게이트를 둡니다.
저장 게이트 체크리스트
- 중복인가(동일 URL, 동일 본문, 유사도)
- 시간 민감 정보인가(가격, 정책, 버전)
- 현재 목표와 관련 있는가(태그/태스크 연결)
- 근거가 있는가(출처 URI 존재, 도구 결과)
- 길이가 적절한가(너무 짧은 문장은 노이즈)
아래는 간단한 파이썬 의사코드입니다.
from dataclasses import dataclass
from datetime import datetime, timedelta
import hashlib
@dataclass
class Candidate:
uri: str
text: str
source_type: str # "web" | "tool" | ...
collected_at: datetime
time_sensitivity: str # "low" | "medium" | "high"
def dedup_hash(text: str) -> str:
normalized = " ".join(text.split()).lower()
return hashlib.sha256(normalized.encode("utf-8")).hexdigest()
def should_store(c: Candidate) -> bool:
if len(c.text.strip()) < 200:
return False
if c.source_type == "web" and not c.uri.startswith("http"):
return False
# 시간 민감도가 높으면 TTL을 짧게 가져가거나 별도 컬렉션으로 분리
# 여기서는 저장은 하되 후속 단계에서 ttlSeconds를 반드시 부여한다고 가정
return True
실제로는 dedup_hash 를 DB에 조회해 중복을 차단하고, qualityScore 를 휴리스틱 또는 작은 모델로 산정합니다.
청킹 전략: “의미 단위 + 인용 가능성” 중심
AutoGPT RAG에서 청킹은 단순히 토큰 길이로 자르는 문제가 아닙니다. 에이전트는 다음 행동을 결정해야 하므로, 청크는 “의사결정에 바로 쓸 수 있는 단위”가 되어야 합니다.
권장 전략:
- 문서 구조 기반(헤딩, 리스트, 표) 우선
- 최대 길이 제한(예: 400~800 토큰)
- 오버랩은 최소화(중복 저장은 검색 오염)
- 각 청크에
title,section,uri,collectedAt를 반드시 포함
예시로, 마크다운/HTML을 섹션 단위로 분해 후 길이를 맞추는 방식이 실무에서 안정적입니다.
인덱싱: 벡터만 저장하지 말고 “필터 키”를 같이 저장
환각을 줄이려면 검색을 “유사도 순”으로만 하지 말고, 메타데이터 필터를 적극 사용해야 합니다.
taskId또는episodeId로 범위를 좁히기collectedAt기준 최신 우선(특히 timeSensitivity가 높을 때)factualityHint가generated인 문서는 기본 제외
벡터DB가 필터를 지원한다면, 검색 함수는 대략 이런 형태가 됩니다.
// 의사코드: 검색 시 메타데이터 필터를 동반
async function retrieve(query: string, opts: {
taskId?: string;
excludeGenerated?: boolean;
maxAgeDays?: number;
k: number;
}) {
const filters: any = {};
if (opts.taskId) filters["metadata.taskId"] = opts.taskId;
if (opts.excludeGenerated) filters["signals.factualityHint"] = { $ne: "generated" };
if (opts.maxAgeDays) {
const cutoff = new Date(Date.now() - opts.maxAgeDays * 86400_000).toISOString();
filters["source.collectedAt"] = { $gte: cutoff };
}
return vectorDb.search({
query,
k: opts.k,
filters,
});
}
이 필터링이 없으면, 오래된 요약이나 모델이 만든 추정이 계속 상위에 뜨며 “기억이 강해지는 환각”이 발생합니다.
쿼리 설계: 에이전트의 의도를 쿼리로 재작성하기
AutoGPT의 현재 상태는 보통 다음을 포함합니다.
- 목표(goal)
- 현재 하위 작업(subtask)
- 이미 시도한 것(observations)
- 필요한 출력 형식(output spec)
그런데 사용자가 처음 준 목표 문장만으로 검색하면, 검색 결과가 넓고 애매해집니다. 따라서 쿼리 재작성 단계를 둡니다.
- 목표를 키워드화
- 금지어/제외 조건 포함(이미 시도한 도구 결과 제외)
- 필요한 근거 타입을 명시(정책 문서, API 레퍼런스 등)
간단한 프롬프트 템플릿 예시:
You are a query rewriter for RAG.
Given:
- Goal: {goal}
- Current subtask: {subtask}
- Constraints: {constraints}
Rewrite a concise search query.
Return only the query.
재작성된 쿼리는 보통 원문보다 짧고 정확해져서, 상위 k 를 줄여도 충분한 근거를 가져오게 됩니다.
검색 전략: 하이브리드 + 재랭킹으로 “그럴듯함”을 줄이기
벡터 검색만 쓰면 의미 유사도는 잡지만, 고유명사/버전/에러코드 같은 키워드 일치를 놓칠 수 있습니다. 반대로 BM25만 쓰면 문맥 유사도가 약합니다.
따라서 권장 조합은 다음입니다.
- 1차 후보: 하이브리드(벡터 + BM25)
- 2차 정렬: 크로스 인코더 또는 LLM 기반 재랭킹
- 최종 주입: 상위 3~6개만, 길이 예산 내로
재랭킹 시 “현재 질문에 답을 직접 포함하는가”를 기준으로 점수를 매기면 환각이 줄어듭니다.
컨텍스트 주입: 출처/시간/스코어를 프롬프트에 구조적으로 제공
RAG 컨텍스트를 그냥 본문으로 붙이면 모델은 “이게 사실인지”를 판단하기 어렵습니다. 아래처럼 구조화해 주입하세요.
- 각 청크에
source uri collectedAtrelevance score- 가능하면
quote형태(원문 발췌임을 명시)
예시 템플릿:
You must answer using ONLY the provided sources.
If sources are insufficient, say "insufficient evidence" and request the missing info.
[SOURCE 1]
uri: {uri}
collectedAt: {collectedAt}
score: {score}
quote:
{chunkText}
[SOURCE 2]...
이렇게 하면 모델이 자기 지식을 섞어 쓰는 비율이 줄고, 부족하면 “추가 탐색”으로 루프를 돌리게 만들 수 있습니다.
환각 방지 게이팅: 답변 전에 근거 충족 조건을 검사
AutoGPT에서 특히 효과적인 패턴은 “생성 전 게이트”입니다.
- 질문을 답하기 위해 필요한 근거 타입을 정의
- 검색 결과가 그 근거를 충족하는지 검사
- 충족 못하면 답변 대신 추가 수집/검색 액션을 선택
의사코드:
def evidence_gate(question: str, retrieved_docs: list[dict]) -> bool:
# 간단한 규칙 기반 예시
if len(retrieved_docs) == 0:
return False
top = retrieved_docs[0]
if top.get("score", 0) < 0.35:
return False
# generated 문서는 근거로 취급하지 않음
if top.get("factualityHint") == "generated":
return False
return True
def decide_next_action(question, retrieved_docs):
if evidence_gate(question, retrieved_docs):
return "answer"
return "search_more" # 또는 "use_tool" / "ask_user"
이 게이트 하나로 “근거 없는 자신감”이 크게 줄어듭니다.
메모리 누적 억제: RAG 인덱스에도 수명과 승격 정책을
RAG 문서가 계속 쌓이면 검색 품질이 떨어집니다. 따라서 다음 정책을 도입합니다.
- TTL: 시간 민감도가 높은 문서는 자동 만료
- 승격(promotion): 여러 번 참조되고 검증된 문서만 장기 보관
- 강등(demotion): 오래 참조되지 않고 품질 점수가 낮은 문서는 아카이브
예를 들어, qualityScore 와 referenceCount 로 승격을 결정합니다.
-- 의사 SQL: 참조가 누적된 고품질 청크만 장기 컬렉션으로 승격
INSERT INTO rag_docs_longterm
SELECT *
FROM rag_docs_staging
WHERE quality_score >= 0.75
AND reference_count >= 3
AND factuality_hint IN ('observed', 'quoted');
이렇게 하면 “한 번 스크랩한 쓰레기 지식”이 영구적으로 시스템을 오염시키지 않습니다.
평가: 환각을 수치로 다루는 최소 지표
RAG 설계를 바꿨다면 반드시 측정해야 합니다. 추천하는 최소 지표는 다음입니다.
- 컨텍스트 정밀도: 주입된 청크 중 실제로 답변에 사용된 비율
- 근거 충족률: 답변 문장별로 출처가 매핑되는 비율
- 불충분 응답률: 근거 부족 시 거부 또는 추가 탐색으로 전환한 비율
- 시간 민감 오류율: 최신 정보가 필요한 질문에서 오래된 문서 인용 비율
LLM-as-a-judge를 쓰더라도, 판정 프롬프트에 “출처 문장 인용 여부”를 강제하면 흔들림이 줄어듭니다.
실전 적용 체크리스트
- 저장: 중복 제거, 최소 길이, 출처 강제,
generated분리 - 청킹: 섹션 기반, 오버랩 최소, 인용 가능한 단위
- 인덱싱: 필터 키를 설계하고 검색에 반드시 사용
- 검색: 하이브리드 + 재랭킹, 상위 소수만 주입
- 주입: 출처/시간/스코어를 구조화
- 게이트: 근거 부족이면 답변 대신 탐색
- 수명: TTL + 승격/강등으로 오염 방지
마무리
AutoGPT의 메모리 누적과 환각은 “모델 파라미터”보다 “지식이 축적되는 방식”에서 더 자주 발생합니다. RAG를 붙이는 순간부터는 검색 품질이 곧 에이전트의 사고 품질이 되기 때문에, 저장 게이트와 메타데이터 필터, 근거 게이팅, TTL/승격 정책까지 포함한 설계가 필요합니다.
위 구조를 적용하면 보통 다음과 같은 변화가 나타납니다.
- 같은 목표를 더 적은 스텝으로 완료(불필요한 재탐색 감소)
- 답변이 짧아져도 근거가 명확(출처 기반)
- 오래된 메모리로 인한 자기모순 감소
다음 단계로는, RAG 검색 결과가 애매할 때 self-consistency나 다중 쿼리 확장을 결합해 “검색 불확실성”을 낮추는 접근을 권합니다.