- Published on
AutoGPT 장기메모리 - Qdrant+RAG로 환각 줄이기
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
AutoGPT 류의 에이전트는 “생각하고 실행한다”는 장점과 함께, 시간이 조금만 지나도 맥락을 잃고 그럴듯한 답을 만들어내는 환각 문제가 자주 드러납니다. 특히 장기 태스크에서 과거 결정, 사용자 선호, 이미 확인한 사실을 잊어버리면 동일한 검증을 반복하거나, 더 나쁘게는 이전과 모순되는 결론을 내립니다.
이 글에서는 Qdrant 벡터 DB를 장기메모리 저장소로 두고, RAG(Retrieval-Augmented Generation)로 근거를 주입해 AutoGPT의 환각을 줄이는 실전 설계를 다룹니다. 핵심은 “모델이 기억하도록 기대”하는 게 아니라, 기억을 검색해 프롬프트에 주입하고, 출처를 강제하는 것입니다.
참고로 운영 단계에서 에이전트가 크래시나 재시작 루프에 빠지면 메모리 저장이 꼬이거나 중복 기록이 생깁니다. 서비스 관점의 재시작 진단은 systemd 서비스가 계속 재시작될 때 진단 체크리스트 같은 글도 함께 보면 도움이 됩니다.
왜 AutoGPT는 장기 태스크에서 환각이 늘어날까
에이전트가 환각을 만드는 대표 원인은 크게 4가지입니다.
- 컨텍스트 윈도우 한계: 긴 대화나 장기 계획은 토큰 제한 때문에 잘립니다.
- 요약의 손실: 중간 요약을 잘못하면, 잘못된 사실이 “요약본”으로 굳어집니다.
- 도구 실행 결과의 휘발성: 검색, 크롤링, DB 조회 결과가 다음 턴에 전달되지 않으면 모델은 빈칸을 채우려 합니다.
- 근거 없는 생성 허용: “모르면 모른다” 대신 “그럴듯한 답”을 허용하는 프롬프트/아키텍처.
그래서 장기메모리는 단순히 “대화 로그 저장”이 아니라, 재사용 가능한 사실과 결정을 구조화하고, 필요할 때 검색해 주입하는 형태여야 합니다.
목표 아키텍처: Qdrant 장기메모리 + RAG
구성 요소는 다음과 같습니다.
- Memory Writer: 이벤트를 메모리로 기록(임베딩 생성, 메타데이터 부착, Qdrant upsert)
- Memory Retriever: 현재 질의/상황을 임베딩하고 Qdrant에서 top-k 검색
- RAG Prompt Builder: 검색 결과를 “근거 블록”으로 프롬프트에 삽입
- Answer Guardrails: 근거가 없으면 보수적으로 답하도록 강제(출처 요구, 불확실성 표기)
데이터 흐름은 아래처럼 단순화할 수 있습니다.
사용자 입력 또는 에이전트 내부 이벤트 발생
Retriever가 관련 메모리 검색
검색된 메모리를 컨텍스트에 주입해 LLM 호출
실행 결과 및 중요한 사실을 Writer가 다시 저장
여기서 Qdrant를 선택하는 이유는 다음이 큽니다.
- 벡터 검색 성능과 운영 난이도 균형
- payload 필터링으로 테넌트, 태스크, 타입별 검색 제어
- HNSW 기반 근사 최근접 탐색으로 실시간성 확보
메모리 스키마 설계: “무엇을 저장할 것인가”가 80%
장기메모리에서 가장 흔한 실패는 “아무거나 다 저장”입니다. 그러면 검색 품질이 떨어지고, 모델이 관련 없는 근거를 끌어와 오히려 환각을 강화합니다.
추천 스키마는 메모리 타입을 분리하는 것입니다.
fact: 검증된 사실(출처 포함)decision: 의사결정과 근거(왜 그렇게 했는지)preference: 사용자 선호(톤, 금지사항, 형식)plan: 현재 계획과 상태(완료/대기)tool_result: 도구 실행 결과 요약(원문 링크 또는 해시)
각 메모리는 payload에 최소한 다음을 넣습니다.
tenant_id,user_id,agent_idtask_id또는thread_idtype(위 분류)source(URL, 파일 경로, 시스템 이벤트 등)confidence(0에서 1)created_attext(임베딩 대상 원문 또는 요약)
중요: 파일 경로나 플레이스홀더를 쓸 때 </> 같은 부등호가 섞이면 MDX에서 문제를 일으킬 수 있으니, 본문에서는 반드시 인라인 코드로 감싸거나 엔티티로 치환해야 합니다.
Qdrant 컬렉션 생성 예시
아래는 Python에서 Qdrant 컬렉션을 만들고 payload 인덱스를 잡는 예시입니다.
from qdrant_client import QdrantClient
from qdrant_client.http import models
client = QdrantClient(url="http://localhost:6333")
COLLECTION = "autogpt_memory"
VECTOR_SIZE = 3072 # 예: text-embedding-3-large
client.recreate_collection(
collection_name=COLLECTION,
vectors_config=models.VectorParams(
size=VECTOR_SIZE,
distance=models.Distance.COSINE,
),
)
# payload 인덱스(필터링 성능 향상)
client.create_payload_index(
collection_name=COLLECTION,
field_name="tenant_id",
field_schema=models.PayloadSchemaType.KEYWORD,
)
client.create_payload_index(
collection_name=COLLECTION,
field_name="type",
field_schema=models.PayloadSchemaType.KEYWORD,
)
client.create_payload_index(
collection_name=COLLECTION,
field_name="created_at",
field_schema=models.PayloadSchemaType.INTEGER,
)
운영에서는 recreate_collection 대신 마이그레이션 전략을 두고, 임베딩 모델 변경 시 컬렉션 버전(autogpt_memory_v2)을 분리하는 편이 안전합니다.
메모리 기록(Writer): 중복과 오염을 막는 규칙
Writer는 “기록하면 좋을 것 같은 것”이 아니라, 다시 꺼내 쓸 가치가 있는 것만 넣어야 합니다.
추천 규칙:
fact는 반드시source를 요구(없으면confidence를 낮게)- 동일 사실의 중복 저장 방지:
dedupe_key를 만들어 upsert - 도구 결과 원문 전체를 넣지 말고, 요약과 참조만 저장
- 일정 시간이 지나면 decay: 오래된
plan은 검색 가중치를 낮춤
예시 코드입니다.
import time
import uuid
from qdrant_client.http import models
def upsert_memory(client, collection, vector, payload, point_id=None):
if point_id is None:
point_id = str(uuid.uuid4())
client.upsert(
collection_name=collection,
points=[
models.PointStruct(
id=point_id,
vector=vector,
payload=payload,
)
],
)
return point_id
payload = {
"tenant_id": "t1",
"user_id": "u1",
"agent_id": "a1",
"task_id": "task-2026-02-25",
"type": "decision",
"text": "로그 수집은 OpenTelemetry로 통일하고, 애플리케이션 로그는 JSON 구조로 남긴다.",
"source": "meeting-notes-2026-02-25",
"confidence": 0.9,
"created_at": int(time.time()),
}
# vector는 임베딩 결과(리스트 of float)라고 가정
# point_id는 dedupe_key로 고정해도 좋다
point_id = upsert_memory(client, COLLECTION, vector=[0.0]*VECTOR_SIZE, payload=payload)
print(point_id)
실제로는 임베딩 벡터를 text에서 생성해야 하고, dedupe_key는 sha256(text + source + type) 같은 방식이 실용적입니다.
메모리 검색(Retriever): 필터링이 곧 품질
RAG 품질은 검색 품질에 좌우됩니다. 그리고 검색 품질은 “임베딩 모델”보다 “필터링과 쿼리 설계”에서 더 많이 개선됩니다.
- 테넌트/유저/태스크 스코프를 무조건 제한
type을 상황에 따라 제한- 최신성 가중치 적용(최근 메모리를 우선)
예시:
from qdrant_client.http import models
def search_memory(client, collection, query_vector, tenant_id, user_id, task_id, types, limit=8):
flt = models.Filter(
must=[
models.FieldCondition(
key="tenant_id",
match=models.MatchValue(value=tenant_id),
),
models.FieldCondition(
key="user_id",
match=models.MatchValue(value=user_id),
),
models.FieldCondition(
key="task_id",
match=models.MatchValue(value=task_id),
),
models.FieldCondition(
key="type",
match=models.MatchAny(any=types),
),
]
)
res = client.search(
collection_name=collection,
query_vector=query_vector,
query_filter=flt,
limit=limit,
with_payload=True,
)
return res
여기서 흔한 실수는 task_id를 빼고 “전 태스크 메모리”를 섞어버리는 것입니다. 그러면 다른 프로젝트의 사실이 섞여 환각이 폭발합니다. 반대로 “전역 선호(preference)”는 task_id 없이 유저 스코프로만 저장하고, 검색 시에는 type별로 스코프를 달리 주는 방식이 좋습니다.
RAG 프롬프트 구성: “근거 블록”을 별도로 만들기
검색 결과를 그냥 대화에 붙이면 모델이 중요도를 구분하지 못합니다. 근거 블록을 명시적으로 분리하고, 답변 정책을 함께 넣어야 합니다.
권장 템플릿(개념 예시):
- 시스템: “근거가 없으면 모른다고 말하라”
- 개발자: “아래
EVIDENCE에 있는 내용만 사실로 단정하라” - 사용자: 원 질문
- 추가:
EVIDENCE섹션에 메모리 목록(각 항목에 source 포함)
예시(문자열 구성 코드):
def build_prompt(user_question, memories):
evidence_lines = []
for i, m in enumerate(memories, start=1):
p = m.payload
evidence_lines.append(
f"[{i}] type={p.get('type')} conf={p.get('confidence')} source={p.get('source')}\n{p.get('text')}"
)
evidence = "\n\n".join(evidence_lines) if evidence_lines else "(no evidence)"
system = (
"You are an agent that answers with evidence. "
"If evidence is insufficient, say you don't know and ask for clarification."
)
developer = (
"Rules:\n"
"- Treat only EVIDENCE as reliable facts.\n"
"- When making claims, cite evidence item numbers like [1], [2].\n"
"- Do not invent sources."
)
user = (
f"Question:\n{user_question}\n\n"
f"EVIDENCE:\n{evidence}"
)
return system, developer, user
이 방식의 장점은 평가가 쉬워진다는 점입니다. 답변에 [n] 인용이 없으면 실패로 간주할 수 있고, 환각을 자동으로 탐지하는 규칙도 세울 수 있습니다.
환각을 더 줄이는 운영 팁 7가지
1) 메모리의 신뢰도(confidence)를 검색에 반영
Qdrant 검색 결과 점수와 별개로, payload의 confidence를 후처리로 반영해 정렬을 조정하세요. 예를 들어 final_score = similarity * (0.5 + 0.5 * confidence) 같은 방식이 단순하지만 효과가 있습니다.
2) “사실”과 “추론”을 분리 저장
도구 결과나 문서에서 나온 문장만 fact로 저장하고, 에이전트의 해석은 decision 또는 note로 분리하세요. 나중에 검색했을 때 모델이 추론을 사실로 오인하는 일이 줄어듭니다.
3) 메모리 쓰기 전 요약을 LLM에 맡길 때는 스키마를 강제
자유형 요약은 오염의 근원입니다. JSON 스키마로 요약을 강제하고, 필드 누락 시 저장하지 않는 정책이 좋습니다.
4) top-k를 무조건 늘리지 말기
근거가 많아지면 오히려 충돌이 생깁니다. 보통 k=5에서 시작해, 실패 케이스에서만 늘리는 접근이 안정적입니다.
5) 시간 필터링과 decay
오래된 plan이나 tool_result는 현재 상태와 충돌할 수 있습니다. created_at 범위를 제한하거나, 최신성 가중치를 두세요.
6) 멀티테넌시 격리
tenant_id 필터는 필수입니다. 운영에서 가장 위험한 사고는 “다른 고객 메모리”가 섞이는 것입니다.
7) 관측 가능성(Observability)
RAG 시스템은 디버깅이 가능해야 합니다. 최소한 다음 로그를 남기세요.
- 검색 쿼리의 요약(원문 그대로가 부담이면 해시)
- 적용된 필터
- 반환된 메모리 ID와 source
- 최종 프롬프트에 포함된 evidence 항목 수
이런 파이프라인은 결국 배포/운영 자동화와도 맞물립니다. 모노레포에서 에이전트와 메모리 서비스를 함께 관리한다면 GitHub Actions 재사용 워크플로우로 모노레포 빌드 50% 단축 같은 패턴으로 CI 시간을 줄이는 것도 추천합니다.
간단한 엔드투엔드 예시: 질문에 근거를 붙여 답하기
아래는 “질문 임베딩 생성” 부분을 함수로 가정하고, Qdrant 검색 결과를 evidence로 넣어 LLM을 호출하는 흐름의 예시입니다.
# pseudocode: embedding_fn, llm_chat_fn은 사용 환경에 맞게 구현
def answer_with_rag(question, tenant_id, user_id, task_id):
qvec = embedding_fn(question)
memories = search_memory(
client=client,
collection=COLLECTION,
query_vector=qvec,
tenant_id=tenant_id,
user_id=user_id,
task_id=task_id,
types=["fact", "decision", "preference"],
limit=6,
)
system, developer, user = build_prompt(question, memories)
resp = llm_chat_fn([
{"role": "system", "content": system},
{"role": "developer", "content": developer},
{"role": "user", "content": user},
])
return resp
이때 평가 기준을 “정답률”만 두지 말고, 다음을 함께 보세요.
- 인용 누락률: 근거 없이 단정하는 비율
- 근거 일치율: 인용한 evidence가 실제로 답변을 지지하는지
- 재현성: 같은 질문에 같은 evidence가 검색되는지
AutoGPT에 붙일 때의 통합 포인트
AutoGPT 계열 구현체는 보통 다음 지점에 훅을 걸면 됩니다.
before_llm_call: 현재 목표/서브태스크를 키로 메모리 검색after_tool_call: 도구 실행 결과를 요약해tool_result로 저장after_llm_call: 확정된 의사결정만decision으로 저장task_end: 최종 결과와 핵심 사실을fact로 정리 저장
중요한 건 “LLM이 말했으니 저장”이 아니라, 검증된 것만 저장하는 정책입니다. 그렇지 않으면 장기메모리가 환각의 저장소가 됩니다.
마무리
Qdrant 장기메모리와 RAG를 AutoGPT에 결합하면, 모델이 기억을 “생성”하는 대신 “검색”하게 만들 수 있습니다. 환각을 줄이는 핵심은 벡터 DB 자체가 아니라 다음 3가지입니다.
- 저장 단계에서의 스키마와 오염 방지
- 검색 단계에서의 강한 필터링과 스코프 설계
- 생성 단계에서의 근거 인용 강제와 불확실성 처리
이 3가지를 갖추면, 에이전트는 더 느려지지 않으면서도 훨씬 일관되고 재현 가능한 답변을 내기 시작합니다.