- Published on
AutoGPT 장기메모리 - Neo4j 그래프RAG 구축
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
에이전트(예: AutoGPT 계열)가 “대화 맥락”을 넘어 장기적으로 성장하려면, 단순 벡터DB만으로는 부족한 순간이 자주 옵니다. 벡터 검색은 유사도 기반 회상에는 강하지만, 관계(누가-무엇을-언제-왜) 를 지속적으로 축적하고 추론하려면 그래프가 훨씬 자연스럽습니다.
이 글에서는 Neo4j를 장기메모리 저장소로 두고, GraphRAG(그래프 기반 Retrieval-Augmented Generation) 로 회상 품질을 끌어올리는 구축 방식을 다룹니다. 목표는 다음입니다.
- 에이전트가 관찰한 사실/이벤트/결정/선호를 그래프로 축적
- 질의 시 벡터 유사도 + 그래프 탐색을 결합해 컨텍스트를 구성
- 운영 환경에서 인제스트/검색 지연, 재시도, 일관성 문제를 줄이는 패턴 적용
또한 외부 API 호출이 많은 RAG 파이프라인에서 흔한 재시도/중복요금 이슈는 이 글과 함께 보면 좋습니다: OpenAI 429·5xx 재시도, Idempotency 키로 중복 결제 막기
왜 “장기메모리 = 그래프 + RAG”인가
벡터 메모리의 한계
벡터DB는 “비슷한 문장”을 찾는 데 최적화되어 있습니다. 하지만 장기기억에서 자주 필요한 질문은 다음처럼 관계형입니다.
- “지난달에 A 고객이 불만을 제기한 원인은 무엇이었지?”
- “내가 선호한다고 말한 기술 스택과 그 근거는?”
- “이 결정을 내릴 때 참조한 문서/사람/이벤트의 연결 구조는?”
이런 질의는 단순 Top-K 유사도만으로는 중요한 연결고리가 누락되기 쉽습니다.
그래프가 주는 장점
Neo4j 같은 그래프DB는 다음을 잘합니다.
- 엔티티(사람/조직/프로젝트/개념)와 관계(참조/소속/선호/원인)의 명시적 저장
- k-hop 탐색, 경로 기반 랭킹, 커뮤니티/중심성 분석
- “기억의 근거”를 경로로 설명 가능(설명가능성)
GraphRAG의 핵심 아이디어
GraphRAG는 보통 아래 중 1개 또는 혼합 형태로 구현합니다.
- 그래프 우선: 질문에서 엔티티를 뽑고, 그래프 탐색으로 관련 서브그래프를 뽑아 LLM 컨텍스트로 넣기
- 벡터 우선: 벡터 검색으로 후보 노드/문서를 찾고, 그 주변 k-hop 서브그래프를 확장해 컨텍스트로 넣기
- 하이브리드 랭킹: 벡터 점수 + 그래프 점수(경로 길이, 관계 타입 가중치, 시간 감쇠 등)를 합쳐 최종 컨텍스트 구성
AutoGPT 장기메모리에서는 2) 또는 3) 패턴이 특히 실용적입니다. “일단 비슷한 기억 조각을 찾고, 그 주변 관계를 확장”하면 회상 누락이 줄어듭니다.
Neo4j 장기메모리 스키마 설계
스키마는 정답이 없지만, 장기메모리에서는 사실/이벤트/선호/결정을 분리하면 유지보수가 쉬워집니다.
추천 노드/관계 모델
:Agent에이전트 인스턴스(또는 사용자별 에이전트):User실제 사용자:Entity일반 엔티티(사람/조직/제품/기술/문서 등):Memory기억 조각(대화에서 추출된 사실/요약/관찰):Event시간 축을 갖는 사건(회의, 장애, 배포, 의사결정):Preference선호(예: 언어, 스타일, 금기)
관계 예시
(:Agent)-[:OBSERVED]->(:Memory)(:Memory)-[:MENTIONS]->(:Entity)(:Event)-[:INVOLVES]->(:Entity)(:User)-[:PREFERS]->(:Preference)(:Memory)-[:ABOUT]->(:Event)(:Memory)-[:DERIVED_FROM]->(:Memory)(요약/정제 파이프라인에서 계보 관리)
시간/신뢰도/출처 메타데이터
장기메모리는 “맞다/틀리다”보다 “언제/어디서/얼마나 확실한가”가 중요합니다.
createdAt(UTC ISO)source(chat, tool, doc, web 등)confidence(0~1)ttl또는expiresAt(휘발성 기억)importance(에이전트가 판단한 중요도)
제약조건과 인덱스
Neo4j는 인덱스/제약이 성능을 좌우합니다.
// 고유 키(예: 엔티티는 외부 키 또는 정규화된 이름 기반)
CREATE CONSTRAINT entity_key IF NOT EXISTS
FOR (e:Entity) REQUIRE e.key IS UNIQUE;
CREATE CONSTRAINT memory_id IF NOT EXISTS
FOR (m:Memory) REQUIRE m.id IS UNIQUE;
CREATE INDEX memory_created IF NOT EXISTS
FOR (m:Memory) ON (m.createdAt);
CREATE INDEX entity_name IF NOT EXISTS
FOR (e:Entity) ON (e.name);
벡터 검색을 Neo4j에 직접 넣을지(버전/플러그인/구성에 따라 다름), 외부 벡터DB를 병행할지는 팀의 운영 역량에 따라 결정하세요. 운영 단순화를 원하면 “Neo4j + 벡터 인덱스” 단일화가 매력적이고, 대규모/고성능이 필요하면 전용 벡터DB 병행도 흔합니다.
기억 인제스트 파이프라인: 대화에서 그래프까지
인제스트는 대개 4단계입니다.
- 원문 저장(감사/재처리용)
- 요약/사실 추출(LLM)
- 엔티티 추출 및 정규화(LLM + 룰)
- 그래프 반영(업서트)
1) 메모리 레코드 업서트
MERGE (m:Memory {id: $id})
SET m.text = $text,
m.summary = $summary,
m.createdAt = $createdAt,
m.source = $source,
m.confidence = $confidence,
m.importance = $importance
WITH m
MATCH (a:Agent {id: $agentId})
MERGE (a)-[:OBSERVED]->(m);
2) 엔티티 업서트 및 연결
엔티티는 key를 어떻게 잡느냐가 매우 중요합니다. 최소한 다음을 권합니다.
key = lower(trim(name))같은 단순 키는 충돌이 잦음- 가능하면
type + canonicalName + namespace조합
UNWIND $entities AS ent
MERGE (e:Entity {key: ent.key})
SET e.name = ent.name,
e.type = ent.type,
e.updatedAt = $now
WITH e, ent
MATCH (m:Memory {id: $memoryId})
MERGE (m)-[r:MENTIONS]->(e)
SET r.weight = coalesce(ent.weight, 1.0);
3) 이벤트/결정 모델링
“결정”은 장기메모리에서 가치가 큽니다. 예를 들어 :Event에 decision 필드를 두거나, :Decision 노드를 따로 둬도 됩니다.
MERGE (ev:Event {id: $eventId})
SET ev.title = $title,
ev.occurredAt = $occurredAt,
ev.kind = $kind
WITH ev
MATCH (m:Memory {id: $memoryId})
MERGE (m)-[:ABOUT]->(ev);
GraphRAG 검색 전략: “관련 기억”을 서브그래프로 뽑기
검색은 보통 다음 순서로 설계합니다.
- 질문을 임베딩해 후보
:Memory또는:Entity를 Top-K로 찾기 - 후보 주변 k-hop 확장(관계 타입 가중치 적용)
- 시간 감쇠/중요도/신뢰도 기반 재랭킹
- 최종 컨텍스트(요약 + 근거 경로) 구성
예시: 후보 메모리에서 2-hop 확장
아래는 “후보 메모리 집합”이 이미 정해졌다고 가정한 Cypher 예시입니다.
// $seedMemoryIds: 벡터 검색 등으로 얻은 후보 메모리 ID 리스트
MATCH (m:Memory)
WHERE m.id IN $seedMemoryIds
WITH collect(m) AS seeds
UNWIND seeds AS s
MATCH p=(s)-[:MENTIONS|ABOUT|DERIVED_FROM*1..2]->(x)
WITH s, x, p
RETURN s.id AS seedId,
labels(x) AS nodeLabels,
x{.*} AS nodeProps,
length(p) AS hops
ORDER BY hops ASC
LIMIT 200;
그래프 점수(가중치) 설계 팁
- 관계 타입별 가중치:
ABOUT는 강하게,MENTIONS는 약하게 - 시간 감쇠: 오래된 기억은 점수 감소(하지만
importance가 높으면 완화) - 신뢰도:
confidence낮은 기억은 컨텍스트에 넣되 “불확실” 태그를 붙이기
점수는 결국 LLM 프롬프트 품질로 귀결되니, “점수로 완벽히 정답을 고른다”보다 컨텍스트 후보군을 잘 구성하는 데 집중하는 편이 성공 확률이 높습니다.
LLM 프롬프트 구성: 기억을 “근거와 함께” 전달
GraphRAG의 장점은 “왜 이 기억을 꺼냈는지”를 경로로 설명할 수 있다는 점입니다. 컨텍스트에는 다음을 포함시키면 좋습니다.
- 요약된 기억(각
:Memory.summary) - 관련 엔티티 목록
- 핵심 경로 2~5개(짧게)
- 시간/출처/신뢰도
예: 컨텍스트 템플릿(개념)
[Memory]
- summary: ...
- createdAt: ...
- confidence: 0.72
- entities: A, B, C
- evidencePath: Agent -OBSERVED- Memory -ABOUT- Event -INVOLVES- Entity
이렇게 넣으면 모델이 환각으로 “그럴듯한 연결”을 만들기보다, 그래프에서 가져온 구조를 바탕으로 답변하게 됩니다.
AutoGPT류 에이전트에 붙이는 방법(아키텍처)
구성 요소를 최소 단위로 나누면 다음과 같습니다.
MemoryWriter: 대화/툴 결과를 받아 메모리 추출 및 그래프 반영MemoryRetriever: 질문을 받아 GraphRAG 검색으로 컨텍스트 생성Policy: 어떤 상황에서 장기메모리를 쓰고, 어떤 기억을 승격/폐기할지 결정
Python 예시: Neo4j 드라이버로 업서트
아래 코드는 골격 예시입니다(실전에서는 재시도, 트랜잭션, 타임아웃을 꼭 추가).
from neo4j import GraphDatabase
from datetime import datetime, timezone
driver = GraphDatabase.driver(
"neo4j://localhost:7687",
auth=("neo4j", "password"),
)
def upsert_memory(agent_id: str, memory: dict):
cypher = """
MERGE (m:Memory {id: $id})
SET m.text = $text,
m.summary = $summary,
m.createdAt = $createdAt,
m.source = $source,
m.confidence = $confidence,
m.importance = $importance
WITH m
MATCH (a:Agent {id: $agentId})
MERGE (a)-[:OBSERVED]->(m)
"""
params = {
"id": memory["id"],
"text": memory["text"],
"summary": memory.get("summary"),
"createdAt": memory.get("createdAt")
or datetime.now(timezone.utc).isoformat(),
"source": memory.get("source", "chat"),
"confidence": float(memory.get("confidence", 0.7)),
"importance": float(memory.get("importance", 0.5)),
"agentId": agent_id,
}
with driver.session() as session:
session.execute_write(lambda tx: tx.run(cypher, **params).consume())
외부 LLM 호출이 포함된 파이프라인은 429/5xx가 생각보다 자주 발생합니다. 이때 “요약 생성 성공 여부”와 “그래프 반영 성공 여부”가 엇갈리면 중복 메모리가 생깁니다. 반드시 멱등성을 설계하세요. 관련 운영 팁은 OpenAI 429·5xx 재시도, Idempotency 키로 중복 결제 막기에서 재시도/중복 방지 패턴을 참고할 수 있습니다.
운영에서 자주 터지는 문제와 해결책
1) 기억 폭증: 그래프가 비대해짐
- 모든 대화를 전부
:Memory로 남기면 그래프가 금방 커집니다. - 해결: “원문”은 저렴한 오브젝트 스토리지에 두고, Neo4j에는 요약/구조화된 사실만 저장하거나, 중요도 기반 승격 정책을 둡니다.
권장 정책
importance낮고 오래된 메모리는 관계만 남기고 텍스트를 축약DERIVED_FROM로 요약 계보를 유지해 추적 가능하게
2) 엔티티 중복(동명이인, 표기 흔들림)
AWSvsAmazon Web Services같은 문제는 필연입니다.- 해결: 정규화 파이프라인을 두고,
:EntityAlias노드 또는alias속성을 운영합니다.
3) 검색 지연과 컨텍스트 과다
- k-hop 확장을 무작정 키우면 컨텍스트가 비대해집니다.
- 해결: 관계 타입 화이트리스트, hop 제한, 시간 창 제한, 상위 N개 경로만 선택.
4) 캐시/일관성: 최신 기억이 안 잡힘
에이전트 시스템은 캐시 계층이 많습니다(애플리케이션 캐시, RAG 캐시, RSC 캐시 등). 최신 대화가 회상되지 않으면 “검색” 문제가 아니라 “캐시” 문제일 수 있습니다. Next.js 기반 대시보드/툴링을 붙였다면 Next.js 14 RSC 캐시로 데이터가 안 갱신될 때도 함께 점검해보세요.
최소 기능 제품(MVP) 체크리스트
-
:Memory업서트 +:Entity연결까지 자동화 - 질문 임베딩 기반 Top-K 후보 선정(외부 벡터DB 또는 Neo4j 벡터 인덱스)
- 후보 주변 1~2 hop 확장 + 간단한 재랭킹
- 컨텍스트에
createdAt,source,confidence포함 - 멱등 키로 인제스트 중복 방지(요약/그래프 반영 각각)
- 엔티티 정규화(최소한
key충돌 방지)
마무리
AutoGPT 장기메모리를 “그럴듯한 로그 저장”에서 “쓸모 있는 기억 시스템”으로 끌어올리려면, 기억을 관계 구조로 저장하고, 검색 시 서브그래프를 근거로 컨텍스트를 구성하는 GraphRAG 접근이 효과적입니다.
핵심은 기술 스택보다도 운영 원칙입니다.
- 기억은 요약/사실/관계로 쪼개서 저장
- 검색은 벡터 유사도와 그래프 탐색을 결합
- 시간/신뢰도/출처를 메타데이터로 관리
- 재시도/멱등성으로 중복 메모리와 비용 폭탄을 방지
이 패턴을 기반으로, 도메인에 맞는 엔티티/관계 타입만 잘 다듬으면 “사용자와 함께 성장하는” 장기기억 에이전트를 충분히 구현할 수 있습니다.