Published on

AutoGPT·BabyAGI 메모리 누수·환각 줄이는 RAG 구축

Authors

AutoGPT·BabyAGI 계열 에이전트는 plan -> act -> observe 루프를 돌며 스스로 목표를 쪼개고 도구를 호출합니다. 문제는 이 구조가 장기 메모리(벡터DB)와 단기 메모리(대화 히스토리) 를 계속 누적시키기 쉽고, 잘못된 검색 결과나 근거 없는 추론이 한 번 섞이면 이후 단계에서 환각이 증폭된다는 점입니다.

이 글은 다음 두 가지를 동시에 해결하는 RAG(Retrieval-Augmented Generation) 구축 관점의 실전 가이드를 목표로 합니다.

  • 메모리 누수처럼 보이는 메모리/스토리지 팽창: 임베딩/문서/에이전트 로그가 끝없이 쌓이거나, 프로세스 RSS가 증가해 OOM으로 이어지는 문제
  • 환각 감소: 검색 품질 저하, 컨텍스트 오염, 잘못된 회상으로 인한 허위 답변

운영 중 OOM이 실제로 발생한다면 먼저 커널 로그로 원인을 좁히는 것도 좋습니다. 관련해서는 리눅스 OOM Killer 로그로 메모리 누수 추적하기를 함께 참고하면 진단 속도가 빨라집니다.

왜 AutoGPT·BabyAGI에서 메모리/환각 문제가 커지나

1) 에이전트 루프가 “기억을 계속 추가”하는 구조

AutoGPT·BabyAGI는 각 step에서 다음을 반복합니다.

  • 관찰(Observation) 및 결과 로그 저장
  • 다음 액션을 위한 요약/추론 생성
  • 장기 메모리에 새로운 청크 추가(혹은 기존 청크 업데이트)

여기서 “저장”이 기본값으로 되어 있으면, 유효하지 않은 중간 산출물도 문서처럼 벡터DB에 들어갑니다. 이후 검색에서 이 찌꺼기가 상위로 올라오면, 에이전트는 그걸 사실로 취급하며 환각이 강화됩니다.

2) 단기 메모리(컨텍스트) 오염

대화 히스토리를 무제한으로 붙이면 컨텍스트가 길어져 비용이 늘고, 더 중요한 문제는 모델이 오래된 가정/오답을 계속 참고하게 됩니다. 특히 “스스로 만든 계획”이 틀렸을 때 이를 정정하지 못하고 계속 밀어붙이는 패턴이 흔합니다.

3) 검색 파이프라인이 부정확하면 환각이 “정당화”된다

RAG는 원래 환각을 줄이려고 쓰지만, 검색이 부정확하면 오히려 반대가 됩니다.

  • 청크가 너무 커서 정밀도가 떨어짐
  • 메타데이터 필터가 없어 다른 프로젝트/테넌트 문서가 섞임
  • top-k가 과도하거나, rerank가 없어 노이즈가 섞임
  • 최신 문서보다 오래된 문서가 먼저 뜸

결국 “근거가 있는 척”하는 답이 나오고, 에이전트는 이를 기반으로 다음 행동을 실행해 피해가 커집니다.

목표 아키텍처: 에이전트 메모리를 RAG로 재설계

핵심은 메모리를 무조건 저장하는 시스템에서, 질문마다 필요한 근거만 검색해 주입하는 시스템으로 바꾸는 것입니다.

권장 컴포넌트

  • 단기 메모리: 최근 N턴만 유지 + 요약 메모리 1개
  • 장기 메모리: 벡터DB(문서/정책/로그/툴 결과) + TTL/버전/스코프
  • 검색: query rewrite -> vector search -> metadata filter -> rerank -> context pack
  • 생성: 근거 인용 강제, 스키마 강제, 불확실성 처리

추가로 비용 최적화 관점에서 캐시도 중요합니다. 반복 질의가 많은 에이전트라면 LangChain RAG 캐시로 LLM 비용 70% 줄이기처럼 캐시 레이어를 함께 고려하세요.

메모리 누수처럼 보이는 “팽창”을 줄이는 설계 포인트

1) 저장 정책: “기억할 가치”가 있는 것만 업서트

에이전트 중간 추론(Thought)이나 임시 계획은 장기 메모리에 넣지 않는 편이 안전합니다.

  • 저장 대상: 정책 문서, 사용자 제공 파일, 툴 호출 결과 중 재사용 가치가 큰 것, 최종 결론
  • 저장 제외: 중간 요약, 반복되는 실패 로그, 모델이 생성한 가정

저장 로직에 quality gate를 둡니다.

  • 길이 제한(예: 2KB 초과는 요약 후 저장)
  • 중복 제거(해시 기반)
  • 출처 필수(출처 없는 텍스트는 저장 금지)

2) TTL, 버전, 스코프를 메타데이터로 강제

벡터DB는 “삭제를 안 하면” 계속 커집니다. 최소한 다음 메타데이터는 넣어야 합니다.

  • tenant_id 또는 project_id
  • source (manual, tool, file, web 등)
  • created_at, expires_at
  • doc_version 또는 updated_at

검색 시에는 반드시 필터링합니다. 특히 멀티테넌트 환경에서 필터 누락은 환각 이전에 보안 사고입니다.

3) 청크 전략: 작은 청크 + 풍부한 메타데이터

일반적으로

  • 청크 크기: 300에서 800 토큰
  • 오버랩: 50에서 150 토큰

정도로 시작해 A/B 테스트로 튜닝합니다. 청크가 너무 크면 검색 정밀도가 떨어지고, 너무 작으면 문맥이 끊겨 재구성이 어려워집니다.

4) 프로세스 메모리(RSS) 누수 대응: 스트리밍, 배치, 연결 관리

RAG가 커지면 애플리케이션 쪽에서도 누수가 발생하기 쉽습니다.

  • 대용량 문서 임베딩은 스트리밍 처리
  • 임베딩 큐를 배치로 묶고, 동시성 제한
  • HTTP 커넥션 풀 및 타임아웃 설정
  • 에이전트 step 로그를 메모리에 계속 쌓지 말고 파일/DB로 스트리밍

환각을 줄이는 RAG 검색 품질 레시피

1) Query rewrite로 “검색 가능한 질문”으로 바꾸기

에이전트의 내부 질문은 종종 추상적입니다.

  • 나쁜 예: “이 문제 해결해줘”
  • 좋은 예: “service A에서 timeout=30s가 발생한 원인 후보와 설정 위치”

LLM으로 rewrite하되, 원 질문의 스코프를 벗어나지 않게 제약을 둡니다.

2) Rerank는 사실상 필수

벡터 유사도 top-k만으로는 노이즈가 많이 섞입니다.

  • 1차: 벡터 검색으로 k=30 정도 후보 확보
  • 2차: cross-encoder rerank로 k=5로 압축

이 단계가 환각 감소에 체감 효과가 큽니다.

3) 컨텍스트 패킹: “근거만” 넣고 나머지는 버리기

검색 결과를 그대로 붙이지 말고, 다음 규칙을 적용합니다.

  • 중복 문장 제거
  • 같은 문서에서 여러 청크가 잡히면 인접 청크 병합
  • 답변에 필요 없는 표/로그는 요약
  • 각 청크에 source_id와 위치 정보 부여

4) 답변 생성에서 근거 인용을 강제

모델이 근거를 사용했는지 확인하려면 출력 스키마에 citations를 강제합니다. 체인 오브 쏘트 유출을 막으면서 구조화하려면 CoT 유출 막는 프롬프트 - JSON 스키마 강제를 같은 맥락에서 적용할 수 있습니다.

구현 예시: Python으로 최소 RAG 루프 만들기

아래 예시는 “에이전트가 질문을 만들고, RAG로 근거를 검색하고, 근거 기반으로만 답하게” 하는 최소 골격입니다. 실제 AutoGPT·BabyAGI에 붙일 때는 tool 호출 결과를 문서로 저장하는 부분을 추가하면 됩니다.

주의: 본문에서 부등호 문자는 MDX 빌드 에러를 유발할 수 있으므로, 코드 블록 안에만 사용합니다.

from dataclasses import dataclass
from typing import List, Dict, Any
import time

@dataclass
class Chunk:
    id: str
    text: str
    meta: Dict[str, Any]
    score: float

class VectorStore:
    def search(self, query: str, k: int, filters: Dict[str, Any]) -> List[Chunk]:
        # TODO: implement with pgvector, pinecone, milvus, chroma, etc.
        raise NotImplementedError

class Reranker:
    def rerank(self, query: str, chunks: List[Chunk], top_k: int) -> List[Chunk]:
        # TODO: implement cross-encoder rerank
        return sorted(chunks, key=lambda c: c.score, reverse=True)[:top_k]


def pack_context(chunks: List[Chunk], max_chars: int = 6000) -> str:
    buf = []
    used = 0
    for c in chunks:
        header = f"[source_id={c.id} doc={c.meta.get('doc_id')} section={c.meta.get('section')}]\n"
        body = c.text.strip() + "\n"
        piece = header + body + "\n"
        if used + len(piece) > max_chars:
            break
        buf.append(piece)
        used += len(piece)
    return "".join(buf)


def should_persist(text: str, meta: Dict[str, Any]) -> bool:
    # quality gate: 출처 없는 텍스트는 저장하지 않음
    if not meta.get("source"):
        return False
    # 너무 긴 텍스트는 요약 후 저장하도록 유도
    if len(text) > 8000:
        return False
    return True


def answer_with_rag(llm, question: str, store: VectorStore, reranker: Reranker, tenant_id: str):
    filters = {
        "tenant_id": tenant_id,
        "expires_at_gte": int(time.time()),
    }

    candidates = store.search(query=question, k=30, filters=filters)
    top = reranker.rerank(question, candidates, top_k=5)
    context = pack_context(top)

    system = (
        "You are a grounded assistant. Use ONLY the provided context. "
        "If the context is insufficient, say you do not know. "
        "Return JSON with fields: answer, citations."
    )

    user = f"Question: {question}\n\nContext:\n{context}"

    # pseudo call
    resp = llm.chat.completions.create(
        model="gpt-4.1-mini",
        messages=[
            {"role": "system", "content": system},
            {"role": "user", "content": user},
        ],
        temperature=0.2,
    )

    return resp

위 코드에서 핵심은 다음입니다.

  • 검색 필터에 tenant_id와 TTL 조건을 넣어 오염된 기억을 차단
  • rerank로 노이즈를 줄여 환각의 재료를 제거
  • 시스템 프롬프트로 “컨텍스트 외 추론 금지”와 “부족하면 모른다”를 강제
  • 출력 JSON 스키마로 후처리와 검증을 쉽게 함

AutoGPT·BabyAGI에 붙일 때의 운영 체크리스트

1) step 예산과 종료 조건을 명확히

에이전트가 끝없이 도는 것이 메모리/비용 폭증의 1순위 원인입니다.

  • 최대 step 수
  • 최대 토큰
  • 같은 실패가 n회 반복되면 중단
  • 목표 달성 조건(정량) 정의

2) 재시도/백오프는 필수

RAG는 임베딩 API, LLM API, 벡터DB 네트워크까지 호출이 많아 429나 타임아웃이 빈번합니다. 재시도 정책이 없으면 에이전트가 실패를 “새 목표”로 오해하거나 로그만 쌓습니다.

실전 구현은 OpenAI 429 RateLimitError 재시도·백오프 구현 또는 설계 관점의 OpenAI API 429 Rate Limit 재시도·백오프 설계를 참고해, 지수 백오프와 지터를 표준으로 두는 것을 권장합니다.

3) 관측 가능성: 메모리와 검색 품질을 같이 본다

환각은 모델 문제가 아니라 데이터/검색 문제인 경우가 많습니다. 다음 지표를 같이 수집하세요.

  • 벡터DB 크기 증가율, 문서 수, TTL 만료율
  • top-k 평균 유사도, rerank 점수 분포
  • 답변당 인용 수, 인용 없는 답변 비율
  • “모른다” 응답 비율(너무 낮으면 환각 가능성)

4) 데이터 정리 작업을 배치로 돌린다

벡터DB/메타DB가 Postgres라면, 삭제/업데이트가 많을 때 테이블이 비대해질 수 있습니다. 이 경우 VACUUM 정책이 중요합니다. 원리와 진단은 PostgreSQL VACUUM 안됨? bloat·wraparound 7분 해결도 함께 보면 좋습니다.

결론: 에이전트 메모리는 “저장”보다 “검색 품질”이 핵심

AutoGPT·BabyAGI의 메모리 문제는 종종 진짜 메모리 누수라기보다, “기억을 무한히 축적하는 설계”와 “검색 품질 부족으로 인한 환각 증폭”이 결합된 결과입니다. 해결의 중심은 다음 3가지입니다.

  • 장기 메모리 저장에 quality gate, TTL, 스코프 필터를 넣어 팽창을 제어
  • vector search + rerank + context pack으로 검색 품질을 올려 환각 재료를 제거
  • 출력 스키마/인용 강제로 근거 기반 응답을 시스템적으로 강제

이 3가지를 갖춘 RAG를 에이전트 루프의 기본 메모리로 삼으면, “똑똑하지만 위험한 자동화”가 “근거 중심의 안정적인 자동화”로 바뀝니다.