포스트

Zep, A Temporal Knowledge Graph Architecture for Agent Memory

Zep, A Temporal Knowledge Graph Architecture for Agent Memory

Zep: A Temporal Knowledge Graph Architecture for Agent Memory, arXiv, 2025.

getzep/graphiti: github

graphiti

Agent의 memory는 정적인 document를 검색하는 RAG에 머물러 있어요. Zep은 여기서 한 걸음 더 나가요. 대화 기록처럼 계속 바뀌는 데이터와 구조화된 business data를 하나의 temporal knowledge graph로 합치고, 사실이 언제 참이었는지를 함께 관리해요. 이 글에서는 Zep의 핵심 엔진인 Graphiti가 graph를 어떻게 구성하고 검색하는지, 그리고 DMR과 LongMemEval에서 어떤 결과를 냈는지 정리해요.

왜 기존 RAG로는 Agent Memory가 부족할까요

기존 RAG는 corpus가 거의 바뀌지 않는다는 가정 위에서 동작해요. 한 번 넣은 document는 잘 수정되지 않고, 검색은 그 고정된 corpus 위에서 이뤄져요. 사내 문서 QA처럼 지식이 안정적인 상황에는 잘 맞아요.

그런데 Agent가 사용자와 계속 상호작용하면 상황이 달라져요. 대화가 쌓이면서 사실이 갱신되고, 어제 참이던 정보가 오늘은 틀린 정보가 돼요. 예를 들어 “나는 지금 A 회사에 다녀요”라는 발화는 나중에 “이번 주에 B 회사로 옮겼어요”라는 발화로 뒤집혀요. 전체 대화 기록을 context에 그대로 넣는 방법은 conversation이 길어지면 무너지는데, LongMemEval은 conversation이 길어질수록 LLM 성능이 빠르게 떨어진다는 점을 보여줬어요.

Zep은 이 문제를 knowledge graph로 풀어요. 정보를 손실 없이 저장하면서, 사실이 유효했던 기간을 timeline으로 관리해서 변화하는 세계를 표현해요.

Zep은 어떻게 Knowledge Graph를 구성할까요

Graph 시각화

Zep의 memory는 temporally-aware dynamic knowledge graph $G = (N, E, \phi)$로 표현돼요. 여기서 $N$은 node, $E$는 edge, 그리고 $\phi : E \rightarrow N \times N$은 어떤 edge가 어떤 node 쌍을 잇는지 알려주는 incidence function이에요.

이 graph는 세 개의 계층적 subgraph로 이뤄져요.

  • Episode subgraph $G_e$는 message, text, JSON 형태의 원본 데이터를 담는 episodic node와, 그 episode를 추출된 entity에 연결하는 episodic edge $E_e \subseteq \phi^*(N_e \times N_s)$로 구성돼요. Episode는 손실 없는 저장소 역할을 하고, 여기서 semantic entity와 relation을 추출해요.
  • Semantic entity subgraph $G_s$는 episode subgraph 위에 세워져요. Episode에서 추출하고 기존 graph의 entity와 resolve해서 만든 entity node $N_s$와, 그 entity 사이의 관계를 나타내는 semantic edge $E_s \subseteq \phi^*(N_s \times N_s)$로 이뤄져요.
  • Community subgraph $G_c$는 강하게 연결된 entity들의 cluster를 나타내는 community node로 구성돼요. Community는 그 cluster의 상위 요약을 담아요.

원본 episode와 여기서 파생된 semantic entity를 둘 다 저장하는 구조는 인간 기억 모델을 닮았어요. 심리학에서는 개별 사건을 담는 episodic memory와 개념 사이의 의미·연관을 담는 semantic memory를 구분하는데, Zep의 두 subgraph가 이 구분을 그대로 반영해요. Episodic·semantic subgraph 분리는 AriGraph에서, community node를 통한 상위 요약은 GraphRAG에서 아이디어를 가져왔어요. Episode에서 fact, entity, community로 올라가는 계층 구조는 기존 hierarchical RAG를 확장한 형태예요.

Episode는 원본 데이터를 손실 없이 저장해요

Graph 구성은 Episode라는 원본 데이터 단위를 받아들이는 데서 시작해요. Episode는 message, text, JSON 중 하나인데, 이 논문은 conversation memory에 집중하므로 message 타입을 다뤄요. Message는 짧은 텍스트와 그 발화를 만든 actor로 이뤄져요.

각 message에는 언제 보내졌는지를 나타내는 reference timestamp $t_{ref}$가 붙어요. 이 값 덕분에 “다음 주 목요일”, “2주 뒤”, “지난 여름” 같은 상대적·부분적 날짜 표현을 실제 datetime으로 변환할 수 있어요.

Zep은 여기서 bi-temporal model을 도입해요. Timeline $T$는 사건이 실제로 일어난 시간 순서를 나타내고, timeline $T’$는 Zep이 데이터를 ingest한 transactional 순서를 나타내요. $T’$는 전통적인 database audit 용도지만, $T$는 대화 데이터가 시간에 따라 변하는 성질을 모델링하는 추가 차원을 제공해요. 이 두 timeline을 나눠 다루는 접근이 Zep을 이전의 graph 기반 RAG와 구분 짓는 핵심이에요.

Episodic edge $E_e$는 episode를 거기서 추출된 entity node에 연결해요. Episode와 파생된 semantic edge는 양방향 index를 유지해서, semantic 산출물을 원본 episode로 되짚어 인용할 수도 있고 반대로 episode에서 관련 entity와 fact를 빠르게 가져올 수도 있어요. 이 양방향 traversal이 episodic subgraph의 non-lossy 성질을 뒷받침해요.

Entity와 Fact는 어떻게 추출할까요

Entity 추출은 episode 처리의 첫 단계예요. 시스템은 현재 message와 직전 $n$개의 message를 함께 보고 named entity를 인식하는데, 이 논문과 Zep의 기본 구현에서는 $n = 4$를 써요. 두 번의 대화 turn을 맥락으로 제공하는 셈이에요. Message를 다루므로 발화자는 자동으로 entity로 추출돼요. 1차 추출이 끝나면 reflexion에서 영감을 받은 reflection 단계를 거쳐요. LLM에게 방금 뽑은 결과를 다시 보여주고 빠뜨린 entity와 잘못 뽑은 entity를 검토하게 하는데, 이 self-review로 누락된 entity를 더 잡아 추출 범위를 넓히고 지어낸 entity를 걸러 hallucination을 줄여요.

추출된 각 entity의 이름은 1024차원 벡터로 embedding돼요. 이 embedding으로 기존 graph의 entity node와 cosine similarity 검색을 하고, 동시에 entity 이름과 summary에 대한 full-text 검색으로 후보 node를 더 찾아요. 이렇게 모은 후보와 episode 맥락을 LLM entity resolution prompt에 넣어서 중복 entity인지 판단해요. 중복이면 더 완전한 이름과 갱신된 summary를 만들어요. 이렇게 정리한 데이터를 graph DB(Neo4j)에 쓸 때는, LLM이 Cypher query를 매번 생성하게 하지 않고 미리 정의해둔 Cypher query 틀에 값만 채워 넣어요. Cypher는 graph DB의 query 언어인데, LLM이 query를 직접 생성하면 필드 이름이나 구조가 달라지기 쉬워요. Query를 고정해두면 node·edge의 schema가 항상 같은 형식으로 유지되고, query 생성 단계에서 hallucination이 끼어들 여지가 없어요.

Fact는 entity 사이의 관계로 추출돼요. 같은 fact가 서로 다른 entity 쌍 사이에서 여러 번 추출될 수 있는데, Graphiti는 이를 hyper-edge로 구현해서 여러 entity가 얽힌 복잡한 fact를 표현해요. Fact도 embedding을 생성한 뒤 deduplication을 거치는데, 이때 hybrid 검색을 같은 entity 쌍 사이에 존재하는 edge로만 제한해요. 이 제약은 서로 다른 entity 사이의 비슷한 edge가 잘못 합쳐지는 걸 막고, 검색 공간을 줄여서 deduplication 비용도 낮춰요.

Temporal 정보로 오래된 Fact를 무효화해요

Graphiti가 다른 knowledge graph 엔진과 구분되는 지점이 temporal extraction과 edge invalidation이에요.

시스템은 $t_{ref}$를 기준으로 fact의 시간 정보를 추출해요. “Alan Turing은 1912년 6월 23일에 태어났어요” 같은 절대적 timestamp도, “2주 전에 새 직장을 시작했어요” 같은 상대적 timestamp도 실제 datetime으로 표현할 수 있어요. Bi-temporal 모델에 맞춰 네 개의 timestamp를 추적해요.

\[t'_{\text{created}},\ t'_{\text{expired}} \in T' \qquad t_{\text{valid}},\ t_{\text{invalid}} \in T\]

$t’_ {\text{created}}$와 $t’_ {\text{expired}}$는 fact가 시스템에 언제 생기고 무효화됐는지를 나타내고, $t_ {\text{valid}}$와 $t_ {\text{invalid}}$는 그 fact가 실제로 참이었던 기간을 나타내요. 이 값들은 edge에 함께 저장돼요.

새 edge가 들어오면 기존 edge를 무효화할 수 있어요. 시스템은 LLM으로 새 edge를 의미적으로 관련된 기존 edge와 비교해서 모순을 찾아요. 시간적으로 겹치는 모순을 발견하면, 무효화 대상 edge의 $t_{\text{invalid}}$를 새 edge의 $t_{\text{valid}}$로 설정해요. Transactional timeline $T’$을 따라, 충돌 시에는 항상 최신 정보를 우선해요.

이 구조 덕분에 대화가 진행되면서 데이터를 동적으로 추가할 수 있고, 현재의 관계 상태와 관계가 변해온 역사적 기록을 동시에 유지할 수 있어요. 앞의 회사 예시로 돌아가면, “A 회사에 다녀요”라는 fact를 삭제하지 않고 그 fact의 유효 기간을 닫은 채로 두고, “B 회사로 옮겼어요”라는 새 fact를 추가해요.

Community는 상위 개념을 요약해요

Episodic·semantic subgraph를 만든 뒤에는 community detection으로 community subgraph를 구성해요. GraphRAG의 접근을 따르되, Leiden 알고리즘 대신 label propagation을 써요. Label propagation은 동적 확장이 간단해서, 새 데이터가 들어와도 community 표현을 오래 유지할 수 있고 전체 재계산을 미룰 수 있기 때문이에요.

동적 확장은 label propagation의 한 번의 recursive step을 구현해요. 새 entity node $n_i \in N_s$가 추가되면 이웃 node들의 community를 조사하고, 이웃 중 가장 많은 쪽의 community를 새 node에 할당한 뒤 community summary와 graph를 갱신해요. 다만 이렇게 갱신하다 보면 community가 전체 label propagation 결과에서 점점 벗어나기 때문에, 주기적인 refresh는 여전히 필요해요. 그래도 이 방식은 latency와 LLM 추론 비용을 크게 줄이는 실용적 heuristic이에요.

Community node의 summary는 member node들을 map-reduce 방식으로 반복 요약해서 만들어요. 검색을 위해서는 community summary에서 핵심 용어와 주제를 뽑아 community 이름을 만들고, 이 이름을 embedding해서 cosine similarity 검색에 쓸 수 있게 해요. 이 방식은 LightRAG의 high-level key 검색과 닮았어요.

Memory Retrieval은 세 단계로 이루어져요

Zep의 graph search API는 함수 $f : S \rightarrow S$로 볼 수 있어요. 텍스트 query $\alpha \in S$를 받아서, LLM agent가 답을 만드는 데 필요한 context $\beta \in S$를 돌려줘요. 이 과정은 세 단계로 나뉘어요.

\[f(\alpha) = \chi(\rho(\phi(\alpha))) = \beta\]
  • Search $\phi$는 관련 있을 법한 후보 node와 edge를 찾아요. 여러 검색 방법을 쓰지만 전체적으로는 $\phi : S \rightarrow E_s^n \times N_s^n \times N_c^n$으로 표현돼요. Query를 semantic edge, entity node, community node의 3-tuple로 바꿔요.
  • Reranker $\rho$는 검색 결과를 다시 정렬해요.
  • Constructor $\chi$는 정렬된 node와 edge를 텍스트 context로 바꿔요. Semantic edge에서는 fact와 $t_{\text{valid}}$, $t_{\text{invalid}}$를, entity node에서는 name과 summary를, community node에서는 summary를 가져와요.

Search는 세 가지 유사도를 함께 봐요

Zep은 세 가지 검색 함수를 써요. Cosine semantic similarity 검색 $\phi_{cos}$, BM25 full-text 검색 $\phi_{bm25}$, 그리고 BFS(breadth-first search) $\phi_{bfs}$예요. 앞의 두 개는 Neo4j의 Lucene 구현을 활용해요. 검색 대상 필드는 객체 타입마다 달라서, semantic edge는 fact 필드를, entity node는 이름을, community node는 키워드로 구성된 community 이름을 검색해요.

세 방법은 각각 다른 종류의 유사도를 잡아요. Full-text 검색은 단어 수준의 유사도를, cosine similarity는 의미적 유사도를, BFS은 맥락적 유사도를 드러내요. Graph에서 가까운 node와 edge는 비슷한 대화 맥락에 나타난다는 직관이에요. 특히 $\phi_{bfs}$는 node를 파라미터로 받을 수 있어서, 최근 episode를 seed로 삼으면 최근에 언급된 entity와 관계를 context에 끌어올 수 있어요.

세 종류의 검색으로 추출된 후보들은 reranker로 보내져요.

Reranker는 precision을 높여요

검색 단계가 recall을 노린다면, reranker는 가장 관련 있는 결과를 앞으로 올려서 precision을 높여요. Zep은 Reciprocal Rank Fusion(RRF)과 Maximal Marginal Relevance(MMR)를 지원하고, 여기에 graph 기반 reranker를 더해요.

Episode-mentions reranker는 대화 안에서 entity나 fact가 언급된 빈도로 우선순위를 매겨서, 자주 참조되는 정보가 더 잘 검색되게 해요. Node distance reranker는 지정한 centroid node로부터의 graph 거리로 결과를 다시 정렬해서, graph의 특정 영역에 국한된 context를 제공해요. 가장 정교한 방식은 cross-encoder로, cross-attention을 써서 query에 대한 node·edge의 relevance score를 매기지만 계산 비용이 가장 커요. 이들은 매번 전부 켜는 게 아니라 상황에 맞게 고르는 선택지예요. 이 논문의 DMR·LongMemEval 실험에서는 BAAI의 BGE-m3를 reranker로 썼어요.

실험은 두 개의 benchmark로 진행했어요

두 실험 모두 conversation 기록을 Zep knowledge graph에 넣은 뒤, 3장의 기법으로 가장 관련 있는 edge(fact)와 entity node를 검색해서 context string으로 만들어요.

모델 선택은 다음과 같아요. Reranking과 embedding에는 BGE-m3를 쓰고, graph 구성에는 gpt-4o-mini를 써요. 답변 생성에는 gpt-4o-mini와 gpt-4o를 쓰고, MemGPT의 DMR 결과와 직접 비교하기 위해 gpt-4-turbo로도 DMR을 돌렸어요.

DMR에서는 Zep이 근소하게 앞섰어요

Deep Memory Retrieval(DMR)은 MemGPT 팀이 만든 평가로, Multi-Session Chat 데이터의 500개 대화 subset을 써요. 각 대화는 5개 session, session마다 최대 12개 message로 이뤄지고, memory 평가용 question/answer 쌍이 하나씩 붙어요.

Memory Model Score
Recursive Summarization gpt-4-turbo 35.3%
Conversation Summaries gpt-4-turbo 78.6%
MemGPT gpt-4-turbo 93.4%
Full-conversation gpt-4-turbo 94.4%
Zep gpt-4-turbo 94.8%
Conversation Summaries gpt-4o-mini 88.0%
Full-conversation gpt-4o-mini 98.0%
Zep gpt-4o-mini 98.2%

MemGPT는 gpt-4-turbo로 93.4%를 기록했어요. Zep은 gpt-4-turbo로 94.8%, gpt-4o-mini로 98.2%를 기록해서 MemGPT와 full-conversation baseline을 근소하게 넘었어요. 다만 이 결과는 조심해서 봐야 해요. 각 대화가 message 60개뿐이라 요즘 LLM의 context window에 쉽게 들어가고, 그래서 full-context 방법도 이미 매우 높은 점수를 내요.

DMR benchmark의 한계는 무엇일까요

저자들은 DMR의 설계 자체에 약점이 있다고 봐요. 평가가 single-turn fact-retrieval 질문에만 의존해서 복잡한 memory 이해를 측정하지 못해요. 질문 표현이 모호한 경우도 많아서, “긴장을 풀 때 즐겨 마시는 음료”나 “특이한 취미”처럼 대화에서 그렇게 규정되지 않은 개념을 묻기도 해요. 무엇보다 이 데이터셋은 실제 enterprise 사용 사례를 잘 대표하지 못해요. 단순한 full-context 방법이 최신 LLM으로 이미 높은 점수를 낸다는 사실이 benchmark의 부적절함을 드러내요.

LongMemEval에서 정확도와 latency를 모두 개선했어요

LongMemEval은 이런 한계를 보완하는 benchmark예요. 실제 business 상황에 가까운, 더 길고 일관된 대화를 제공하는데 평균 약 115,000 token 길이예요. 질문 유형도 single-session-user, single-session-assistant, single-session-preference, multi-session, knowledge-update, temporal-reasoning 여섯 가지로 다양해요.

실험은 2024년 12월부터 2025년 1월 사이에, Boston의 소비자용 노트북에서 AWS us-west-2에 호스팅된 Zep 서비스에 접속하는 방식으로 진행했어요. 이 분산 구조 때문에 Zep 평가에는 network latency가 더해졌는데, baseline 평가에는 이 지연이 없었어요. 참고로 현재 MemGPT 프레임워크는 기존 message 기록을 직접 ingest하는 기능이 없어서, archival history에 대화를 추가하는 우회 방법을 시도했지만 이 benchmark에서 유효한 응답을 얻지 못했어요.

Memory Model Score Latency Avg Context Tokens
Full-context gpt-4o-mini 55.4% 31.3 s 115k
Zep gpt-4o-mini 63.8% 3.20 s 1.6k
Full-context gpt-4o 60.2% 28.9 s 115k
Zep gpt-4o 71.2% 2.58 s 1.6k

Zep은 gpt-4o-mini로 15.2%, gpt-4o로 18.5%의 정확도 향상을 보였어요. 동시에 context를 115k token에서 1.6k token으로 줄이면서 latency를 약 90% 낮췄어요. 정확도와 비용·지연을 함께 개선했다는 점이 핵심이에요.

질문 유형별로 보면 강점과 약점이 갈려요

질문 유형별로 나눠 보면 개선의 양상이 드러나요.

Question Type Model Full-context Zep Delta
single-session-preference gpt-4o-mini 30.0% 53.3% 77.7% ↑
single-session-assistant gpt-4o-mini 81.8% 75.0% 9.06% ↓
temporal-reasoning gpt-4o-mini 36.5% 54.1% 48.2% ↑
multi-session gpt-4o-mini 40.6% 47.4% 16.7% ↑
knowledge-update gpt-4o-mini 76.9% 74.4% 3.36% ↓
single-session-user gpt-4o-mini 81.4% 92.9% 14.1% ↑
single-session-preference gpt-4o 20.0% 56.7% 184% ↑
single-session-assistant gpt-4o 94.6% 80.4% 17.7% ↓
temporal-reasoning gpt-4o 45.1% 62.4% 38.4% ↑
multi-session gpt-4o 44.3% 57.9% 30.7% ↑
knowledge-update gpt-4o 78.2% 83.3% 6.52% ↑
single-session-user gpt-4o 81.4% 92.9% 14.1% ↑

가장 큰 향상은 single-session-preference, multi-session, temporal-reasoning처럼 복잡한 유형에서 나타나요. Cross-session으로 정보를 종합하거나 시간 추론이 필요한 과제에서 Zep의 구조가 힘을 발휘한다는 뜻이에요. gpt-4o에서는 knowledge-update까지 개선돼서, 더 강한 모델이 Zep의 temporal 데이터를 더 잘 활용한다는 점을 보여줘요. 반대로 덜 강한 모델은 temporal 데이터 이해에 개선 여지가 있어요.

예외는 single-session-assistant예요. gpt-4o에서 17.7%, gpt-4o-mini에서 9.06% 하락했어요. Assistant 자신의 이전 발화를 되짚는 유형인데, 이 부분은 추가 연구와 엔지니어링이 필요한 지점으로 남아 있어요.

정리

Zep은 knowledge graph 위에 episodic·semantic memory와 entity·community 요약을 함께 얹은 memory layer예요. 핵심은 세 가지로 볼 수 있어요.

첫째, 원본 episode를 손실 없이 저장하면서 거기서 semantic entity와 fact를 파생시키는 이중 저장 구조예요. 인간 기억의 episodic·semantic 구분을 graph로 옮겼어요.

둘째, bi-temporal 모델이에요. 사건이 실제로 참이었던 timeline $T$와 데이터가 ingest된 timeline $T’$을 분리하고, 네 개의 timestamp로 fact의 유효 기간을 관리해요. 모순되는 새 fact가 들어오면 기존 fact를 삭제하지 않고 무효화해서, 현재 상태와 변화 이력을 동시에 유지해요.

셋째, cosine similarity·BM25·BFS과 여러 reranker를 조합한 retrieval이에요. 이 덕분에 115k token context를 1.6k token으로 줄이면서도 정확도를 높였어요.

저자들은 남은 과제도 분명히 해요. Graphiti prompt에 맞춘 fine-tuned 추출 모델, domain 온톨로지의 도입, business data와 대화 기록을 함께 종합하는 능력을 평가할 새 benchmark가 필요해요. 특히 마지막 지점은 Zep이 겨냥하는 enterprise 시나리오의 핵심인데 아직 적절한 benchmark가 없다는 점을 스스로 지적해요.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.