From 060f6d9f46da6da8d03fb23af2444304daa7b61f Mon Sep 17 00:00:00 2001 From: bits-bytes-nn Date: Sun, 7 Jun 2026 22:47:37 +0900 Subject: [PATCH 1/2] Add paper review: Zep: A Temporal Knowledge Graph Architecture for Agent Memory --- ...dge-graph-architecture-for-agent-memory.md | 661 ++++++++++++++++++ 1 file changed, 661 insertions(+) create mode 100644 _posts/2025-01-20-zep-a-temporal-knowledge-graph-architecture-for-agent-memory.md diff --git a/_posts/2025-01-20-zep-a-temporal-knowledge-graph-architecture-for-agent-memory.md b/_posts/2025-01-20-zep-a-temporal-knowledge-graph-architecture-for-agent-memory.md new file mode 100644 index 0000000..e3568f7 --- /dev/null +++ b/_posts/2025-01-20-zep-a-temporal-knowledge-graph-architecture-for-agent-memory.md @@ -0,0 +1,661 @@ +--- +layout: post +title: "Zep: A Temporal Knowledge Graph Architecture for Agent Memory" +date: 2025-01-20 16:52:48 +author: "Preston Rasmussen et al." +categories: ["Paper Reviews", "Retrieval-Augmented-Generation-(incl.-knowledge-graphs-&-ontologies)"] +tags: ["Temporally-Aware-Knowledge-Graph-Engine", "Bi-Temporal-Knowledge-Graph-Modeling", "Hierarchical-Knowledge-Graph-Construction", "Dynamic-Edge-Invalidation-for-Temporal-Reasoning", "Episodic-and-Semantic-Memory-Subgraphs", "Community-Detection-with-Label-Propagation", "Hybrid-Search-with-Breadth-First-Graph-Traversal", "Non-Lossy-Knowledge-Graph-Updates", "Multi-Hop-Entity-and-Relationship-Extraction", "Graph-Based-Memory-Retrieval-for-LLM-Agents"] +cover: /assets/images/default.jpg +use_math: true +--- +### TL;DR +#### 이 연구를 시작하게 된 배경과 동기는 무엇입니까? + +대규모 언어 모델(LLM) 기반 대화형 에이전트는 산업과 연구 커뮤니티에서 광범위하게 활용되고 있지만, 근본적인 한계를 마주하고 있습니다. LLM의 **컨텍스트 윈도우 크기 제약**, 긴 맥락에 대한 **효과적인 정보 활용의 어려움**, 그리고 **사전 학습 이후의 새로운 정보나 도메인 특화 지식에 대한 무지**가 그것입니다. 이러한 제약을 극복하기 위해 검색 증강 생성(RAG) 기법이 등장했으나, 기존 RAG 접근법은 주로 정적인 코퍼스를 다루도록 설계되어 있어 끊임없이 진화하는 대화 데이터와 비즈니스 정보를 효과적으로 처리하지 못합니다. 에이전트가 일상의 다양한 문제를 자율적으로 해결하는 보편적 존재가 되려면, 사용자 상호작용에서 생성되는 동적 데이터와 도메인 특화 콘텐츠로 이루어진 **광범위하고 진화하는 메모리**에 접근할 수 있어야 합니다. 이 연구는 LLM 에이전트에게 시간 인식적이고 동적인 메모리 계층을 제공함으로써 이 근본적 문제를 해결하고자 합니다. + +#### 이 연구에서 제시하는 새로운 해결 방법은 무엇입니까? + +본 논문은 **Graphiti**라는 동적이고 시간 인식적인 지식 그래프 엔진으로 구동되는 메모리 계층 서비스 **Zep**을 제안합니다. Zep의 핵심 혁신은 **비손실적 갱신(non-lossy update)** 방식으로 지식 그래프를 관리하면서 사실과 관계의 유효 기간을 명시적으로 추적한다는 점입니다. 기존 정보를 폐기하지 않고 시간 범위를 함께 관리함으로써, 시스템은 "현재 참인 것"뿐 아니라 "과거 한때 참이었던 것"까지 모두 보존하는 시간 인식 메모리를 구현합니다. 이를 위해 Zep은 **이중 시간 모델(bi-temporal model)**을 도입하여 사건이 실제로 발생한 연대순(사건 타임라인)과 시스템이 데이터를 수집한 거래순(거래 타임라인)을 구분합니다. 또한 지식 그래프는 **세 개의 위계적 부분그래프**로 구성되는데, 원시 에피소드 데이터를 저장하는 에피소드 부분그래프, 추출된 개체와 사실을 담는 의미 개체 부분그래프, 그리고 강하게 연결된 개체들의 군집을 나타내는 커뮤니티 부분그래프가 그것입니다. 이러한 다층 구조는 인간의 일화 기억과 의미 기억이라는 심리학적 모델을 반영하면서도, 기존 그래프 기반 RAG 접근법보다 훨씬 풍부한 정보 표현을 가능하게 합니다. + +#### 제안된 방법은 어떻게 구현되었습니까? + +Zep의 구현은 메시지, 텍스트, JSON 형태의 원시 데이터를 수집하는 것에서 시작됩니다. 각 에피소드에는 **참조 타임스탬프**가 부착되어 상대적 시간 표현("2주 후", "지난여름")을 정확한 날짜로 변환하는 데 활용됩니다. 개체 추출 단계에서는 직전 4개 메시지를 맥락으로 활용하여 명시적·암묵적 개체를 식별하고, **반성(reflection)** 기법으로 환각을 최소화합니다. 추출된 개체는 1024차원 벡터로 임베딩되어 코사인 유사도 검색과 전문 검색을 통해 기존 개체와의 중복을 판별하는 **개체 해소** 과정을 거칩니다. 사실 추출에서는 개체 쌍 사이의 관계를 식별하며, 중복 제거 시 동일 개체 쌍으로 검색을 한정하여 계산 복잡도를 낮춥니다. 가장 독창적인 부분은 **시간 정보 추출과 엣지 무효화**입니다. 새로운 엣지가 추가될 때 시스템은 LLM을 사용하여 기존 엣지와의 모순을 식별하고, 시간적으로 겹치는 모순이 발견되면 기존 엣지의 무효화 시점을 새 엣지의 유효 시작점으로 설정합니다. 커뮤니티 탐지는 **레이블 전파 알고리즘**을 사용하여 강하게 연결된 개체들을 군집화하며, 새 데이터 유입 시 전체 재계산 대신 **단일 재귀 단계**의 휴리스틱으로 점진적 갱신을 수행합니다. 메모리 검색 파이프라인은 **검색(Search)**, **재순위화(Reranker)**, **구성(Constructor)** 세 단계로 구성되며, 코사인 유사도, BM25 전문 검색, 너비 우선 탐색이라는 세 가지 검색 방식을 결합하여 높은 재현율을 확보한 후, 상호 순위 융합, 최대 한계 관련성, 교차 인코더 등 다양한 재순위화 전략으로 정밀도를 높입니다. + +#### 이 연구의 결과가 가지는 의미는 무엇입니까? + +Zep은 두 개의 주요 벤치마크에서 강력한 성능을 입증했습니다. **Deep Memory Retrieval(DMR)** 과제에서는 gpt-4-turbo 기준 94.8%의 정확도로 MemGPT의 93.4%를 근소하게 앞섰으며, 더 복잡한 **LongMemEval** 벤치마크에서는 gpt-4o-mini 대비 정확도를 15.2%포인트, gpt-4o 대비 18.5%포인트 향상시켰습니다. 더욱 인상적인 것은 **응답 지연 시간을 약 90% 단축**했다는 점입니다. 평균 맥락 토큰을 115,000개에서 1,600개로 극적으로 압축하면서도 정확도를 높였으므로, 지식 그래프 검색이 방대한 대화에서 정말 필요한 정보만을 정밀하게 추출함을 의미합니다. 질문 유형별 분석에서 Zep은 **single-session-preference, multi-session, temporal-reasoning** 같은 복잡한 질문에서 가장 큰 개선을 보였으며, 특히 시간적 추론이 필요한 과제에서 두드러진 강점을 드러냈습니다. 다만 single-session-assistant 유형에서는 성능이 하락했는데, 이는 어시스턴트 측 콘텐츠가 그래프 추출 과정에서 충분히 보존되지 못했을 가능성을 시사합니다. 논문은 Zep이 정확도, 비용, 지연 시간을 균형 있게 고려하는 실용적 생산 시스템이며, 향후 미세 조정된 추출 모델, 도메인 특화 온톨로지 통합, 더 나은 메모리 벤치마크 개발 등을 통해 한층 발전할 수 있는 여지가 충분함을 강조합니다. +- - - +# Zep: 에이전트 메모리를 위한 시간 인식 지식 그래프 아키텍처 + +## 서론 + +### LLM 에이전트와 메모리의 필요성 + +트랜스포머 기반 대규모 언어 모델(LLM)은 최근 산업계와 연구 커뮤니티 양쪽에서 막대한 관심을 받아 왔으며, 그 대표적인 활용 분야 중 하나가 바로 대화형 에이전트(chat-based agent)입니다. 그러나 이러한 에이전트의 능력에는 본질적인 한계가 존재합니다. 논문은 그 한계를 세 가지로 정리합니다. 첫째는 LLM의 **컨텍스트 윈도우(context window)** 크기 제약으로, 한 번에 처리할 수 있는 토큰의 양이 물리적으로 제한되어 있다는 점입니다. 둘째는 **효과적인 컨텍스트 활용(effective context utilization)**의 한계로, 설령 긴 컨텍스트를 입력하더라도 모델이 그 안의 정보를 모두 제대로 활용하지 못하는 문제입니다. 셋째는 **사전 학습(pre-training) 과정에서 획득한 지식의 한계**로, 학습 시점 이후의 정보나 특정 도메인에 국한된 지식은 모델이 알지 못합니다. + +이러한 제약 때문에 LLM에는 추가적인 외부 맥락(context)이 필요합니다. 모델이 학습하지 못한 도메인 외(out-of-domain, OOD) 지식을 공급하고, 사실이 아닌 내용을 그럴듯하게 지어내는 **환각(hallucination)** 현상을 줄이기 위해서입니다. 즉, 모델 내부의 고정된 지식만으로는 충분하지 않으며, 외부에서 적절한 정보를 끌어와 보강해 주는 메커니즘이 필수적이라는 것입니다. + +### RAG의 등장과 한계 + +이 문제에 대한 핵심적인 해법으로 부상한 것이 **검색 증강 생성(Retrieval-Augmented Generation, RAG)**입니다. RAG는 지난 50여 년간 발전해 온 **정보 검색(Information Retrieval, IR)** 기법을 활용하여 LLM에 필요한 도메인 지식을 공급합니다. 여기서 짚어둘 배경 지식은, IR이 검색 엔진이나 문서 색인 시스템 등에서 오랜 기간 축적되어 온 성숙한 분야라는 점입니다. RAG는 이러한 검증된 검색 기술을 LLM과 결합함으로써, 모델이 응답을 생성하기 전에 관련 문서를 먼저 찾아 입력에 포함시키는 방식으로 작동합니다. + +그러나 현재의 RAG 접근법은 주로 광범위한 도메인 지식과 **대체로 정적인(static) 코퍼스**를 다루는 데 집중되어 있습니다. 다시 말해, 코퍼스에 추가된 문서의 내용이 거의 변하지 않는다는 가정 위에서 설계되어 있다는 것입니다. 이 점이 바로 논문이 지적하는 핵심 한계입니다. 에이전트가 일상 속에서 사소한 일부터 매우 복잡한 문제까지 자율적으로 해결하는 보편적 존재가 되려면, 사용자와의 상호작용에서 끊임없이 생성되는 데이터, 그리고 이와 관련된 비즈니스 데이터 및 세계의 데이터로 이루어진 **지속적으로 진화하는 거대한 코퍼스**에 접근할 수 있어야 합니다. + +논문은 에이전트에게 이처럼 광범위하고 동적인 "메모리(memory)"를 부여하는 것이야말로 이러한 비전을 실현하기 위한 핵심 구성 요소라고 보며, 현재의 정적 코퍼스 중심 RAG 접근법은 이러한 미래에 적합하지 않다고 주장합니다. 전체 대화 기록, 비즈니스 데이터셋, 그 밖의 도메인 특화 콘텐츠 전부를 LLM의 컨텍스트 윈도우 안에 효과적으로 담을 수 없기 때문에, 에이전트 메모리를 위한 새로운 접근법이 필요하다는 것입니다. + +### Zep과 Graphiti의 제안 + +LLM 기반 에이전트에 메모리를 추가한다는 발상 자체가 완전히 새로운 것은 아닙니다. 이 개념은 MemGPT 연구에서 이미 선구적으로 탐구된 바 있습니다. 또한 최근에는 전통적인 IR 기법의 약점을 보완하기 위해 **지식 그래프(Knowledge Graph, KG)**가 RAG 아키텍처에 도입되고 있습니다. 여기서 지식 그래프가 왜 기존 검색 방식의 한계를 보완하는지 직관적으로 이해할 필요가 있습니다. 전통적인 문서 검색은 텍스트 조각(chunk) 단위로 유사도를 계산하여 관련 문서를 찾지만, 개체(entity) 사이의 관계나 시간에 따른 변화를 명시적으로 표현하지는 못합니다. 반면 지식 그래프는 개체를 노드로, 그들 사이의 사실과 관계를 엣지로 표현하기 때문에, 여러 정보 조각에 흩어져 있는 사실을 구조적으로 연결하고 통합적으로 추론하는 데 유리합니다. + +이러한 흐름 속에서 논문은 동적이고 시간 인식적인(temporally-aware) 지식 그래프 엔진인 **Graphiti**로 구동되는 메모리 계층 서비스 **Zep**을 제안합니다. Zep은 비정형 메시지 데이터와 정형 비즈니스 데이터를 모두 수집하여 종합합니다. Graphiti KG 엔진은 새로운 정보가 들어올 때마다 지식 그래프를 **비손실적(non-lossy)** 방식으로 갱신하며, 기존 정보를 폐기하지 않고 사실과 관계의 유효 기간(periods of validity)을 함께 관리하면서 사실들의 타임라인을 유지합니다. 이러한 접근 덕분에 지식 그래프는 복잡하고 끊임없이 변화하는 세계를 표현할 수 있게 됩니다. (비손실적 갱신과 유효 기간 관리의 구체적인 메커니즘은 이후 지식 그래프 구축을 다루는 부분에서 상세히 살펴봅니다.) + +Zep은 연구용 프로토타입이 아니라 실제 운영되는 **생산 시스템(production system)**이라는 점이 중요한 설계 배경입니다. 이 때문에 논문은 메모리 검색 메커니즘의 **정확도(accuracy), 지연 시간(latency), 확장성(scalability)**에 특히 비중을 두어 설계했음을 강조합니다. 즉, 단순히 답을 잘 찾는 것뿐 아니라, 빠르게 그리고 대규모로 안정적으로 작동하는 것이 실제 배포 환경에서는 필수적이기 때문입니다. + +Zep의 메모리 검색 메커니즘의 효용성은 두 가지 기존 벤치마크를 통해 평가됩니다. 하나는 MemGPT가 자신들의 주요 평가 지표로 확립한 **Deep Memory Retrieval(DMR)** 과제이고, 다른 하나는 더 복잡한 시간적 추론(temporal reasoning) 과제를 통해 기업용 실사용 사례를 더 잘 반영하는 **LongMemEval** 벤치마크입니다. 논문에 따르면 Zep은 DMR에서 MemGPT를 근소하게 앞서는 성능(94.8% 대 93.4%)을 보이며, 더 까다로운 LongMemEval에서는 정확도를 최대 18.5%까지 향상시키는 동시에 응답 지연 시간을 약 90% 줄이는 결과를 달성합니다. 특히 이러한 강점은 세션 간 정보 종합(cross-session information synthesis)이나 장기적 맥락 유지(long-term context maintenance)와 같이 기업 환경에서 결정적으로 중요한 과제에서 두드러지게 나타나며, 이는 Zep이 실제 응용 환경에 배포되기에 효과적임을 보여줍니다. 각 벤치마크의 구체적인 실험 설정과 수치 분석은 이후 실험을 다루는 부분에서 자세히 다룹니다. +## 지식 그래프 구축 + +Zep의 메모리는 시간 인식적(temporally-aware) 동적 지식 그래프 위에서 동작합니다. 앞서 서론에서 Graphiti가 비손실적으로 그래프를 갱신하며 사실의 유효 기간을 관리한다고 소개했는데, 이 부분에서는 그러한 메모리가 어떤 수학적 구조 위에 얹혀 있는지를 형식적으로 정의합니다. + +### 그래프의 형식적 정의와 세 가지 계층 + +Zep의 지식 그래프는 다음과 같이 세 요소의 순서쌍으로 정의됩니다. + +$$ \mathcal{G} = (\mathcal{N}, \mathcal{E}, \phi) $$ + +여기서 \\(\mathcal{N}\\)은 노드(node)들의 집합, \\(\mathcal{E}\\)는 엣지(edge)들의 집합을 의미합니다. 세 번째 요소인 \\(\phi\\)는 **형식적 결합 함수(incidence function)**로서, 각 엣지를 두 노드의 순서쌍에 대응시키는 역할을 합니다. + +$$ \phi : \mathcal{E} \to \mathcal{N} \times \mathcal{N} $$ + +직관적으로 설명하면, 결합 함수 \\(\phi\\)는 "이 엣지는 어떤 노드에서 출발해서 어떤 노드로 도착하는가?"라는 질문에 답을 주는 장치입니다. 예를 들어 "앨리스 — 근무한다 → 회사 X"라는 관계가 하나의 엣지라면, \\(\phi\\)는 그 엣지를 (앨리스, 회사 X)라는 노드 쌍에 매핑합니다. 그래프 이론에서 노드와 엣지의 집합만으로는 어느 엣지가 어느 노드들을 잇는지가 불명확하기 때문에, 이렇게 결합 함수를 명시적으로 도입해 그래프의 위상(topology)을 엄밀하게 규정하는 것입니다. + +이 그래프는 세 개의 위계적(hierarchical) 부분그래프(subgraph) 계층으로 구성됩니다. 가장 아래에 **에피소드 부분그래프(episode subgraph)** \\(\mathcal{G}_e\\), 그 위에 **의미 개체 부분그래프(semantic entity subgraph)** \\(\mathcal{G}_s\\), 그리고 가장 높은 층에 **커뮤니티 부분그래프(community subgraph)** \\(\mathcal{G}_c\\)가 자리합니다. 이 세 층은 각각 원시 데이터, 추출된 개체와 사실, 그리고 개체들의 군집이라는 점진적으로 추상화되는 정보 단위를 담당합니다. + +### 에피소드 부분그래프 + +가장 아래 층인 에피소드 부분그래프 \\(\mathcal{G}_e\\)에서, 에피소드 노드(episodic node) \\(n_i \in \mathcal{N}_e\\)는 메시지, 텍스트, 혹은 JSON 형태의 **원시 입력 데이터**를 그대로 담습니다. 여기서 핵심은 에피소드가 **비손실적 데이터 저장소(non-lossy data store)** 역할을 한다는 점입니다. 즉 입력된 원본 정보를 손상시키거나 요약해 버리지 않고 원형 그대로 보존하며, 이후 의미 개체와 관계를 추출해 내는 원천(source)이 됩니다. 이는 마치 회의록 원문을 그대로 보관하면서, 거기서 별도로 핵심 인물과 결정 사항을 정리한 메모를 따로 만드는 것과 비슷합니다. 원문이 남아 있으므로 언제든 정리본을 원본과 대조하거나 인용 출처를 확인할 수 있습니다. + +에피소드 엣지(episodic edge)는 다음과 같이 정의됩니다. + +$$ e\_i \in \mathcal{E}\_e \subseteq \phi^{*}(\mathcal{N}\_e \times \mathcal{N}\_s) $$ + +이 표기에서 \\(\phi^{*}\\)의 위첨자 별표(\\(*\\))는 앞서 정의한 결합 함수 \\(\phi\\)를, 개별 원소가 아니라 **두 노드 집합의 곱집합(product space) 전체 위로 확장(lift)한 유도 사상**을 의미한다고 이해할 수 있습니다. 다시 말해 \\(\phi^{*}(\mathcal{N}_e \times \mathcal{N}_s)\\)는 에피소드 노드 집합 \\(\mathcal{N}_e\\)와 의미 개체 노드 집합 \\(\mathcal{N}_s\\) 사이에 결합 함수로 형성될 수 있는 모든 가능한 엣지의 모집합을 가리키며, 실제 에피소드 엣지 집합 \\(\mathcal{E}_e\\)는 그 부분집합(\\(\subseteq\\))으로 존재합니다. 결국 에피소드 엣지는 하나의 에피소드를, 그 안에서 언급된 의미 개체에 연결하는 역할을 합니다. + +### 의미 개체 부분그래프 + +두 번째 층인 의미 개체 부분그래프 \\(\mathcal{G}_s\\)는 에피소드 부분그래프 위에 쌓아 올려집니다. 개체 노드(entity node) \\(n_i \in \mathcal{N}_s\\)는 에피소드로부터 추출되어, 기존 그래프에 이미 존재하는 개체들과 **해소(resolution)**를 거친 개체를 나타냅니다. 여기서 해소란 새로 추출한 "앨리스"가 이미 그래프에 있던 "앨리스"와 동일 인물인지 판별하여 중복을 통합하는 과정을 가리키며, 이 구체적 메커니즘은 이후 개체 추출과 해소를 다루는 부분에서 상세히 설명됩니다. + +개체 엣지(entity edge), 즉 **의미 엣지(semantic edge)**는 다음과 같이 정의됩니다. + +$$ e\_i \in \mathcal{E}\_s \subseteq \phi^{*}(\mathcal{N}\_s \times \mathcal{N}\_s) $$ + +에피소드 엣지가 서로 다른 두 종류의 노드 집합(에피소드와 개체)을 이었던 것과 달리, 의미 엣지는 동일한 개체 노드 집합 \\(\mathcal{N}_s\\) 내부의 두 개체를 잇습니다. 이 엣지들은 에피소드로부터 추출된 개체 간의 관계, 즉 "사실(fact)"을 표현합니다. + +### 커뮤니티 부분그래프 + +가장 높은 층인 커뮤니티 부분그래프 \\(\mathcal{G}_c\\)에서, 커뮤니티 노드(community node) \\(n_i \in \mathcal{N}_c\\)는 **강하게 연결된 개체들의 군집(cluster)**을 나타냅니다. 각 커뮤니티 노드는 해당 군집에 대한 고수준 요약(high-level summarization)을 담고 있어, 의미 개체 부분그래프 \\(\mathcal{G}_s\\)의 구조를 더 포괄적이고 상호 연결된 시각에서 조망할 수 있게 합니다. 비유하자면, 개별 개체들이 도시의 건물이라면 커뮤니티는 그 건물들을 묶은 "동네"에 해당하며, 동네 단위로 전체 지형을 파악하면 세부 건물을 일일이 보지 않고도 도메인 전반에 대한 거시적 이해를 얻을 수 있습니다. + +커뮤니티 엣지(community edge)는 다음과 같이 정의됩니다. + +$$ e\_i \in \mathcal{E}\_c \subseteq \phi^{*}(\mathcal{N}\_c \times \mathcal{N}\_s) $$ + +즉 커뮤니티 엣지는 각 커뮤니티 노드를 그 구성원인 개체 노드들과 연결합니다. 커뮤니티 탐지에 사용되는 레이블 전파(label propagation) 알고리즘과 동적 갱신 방식은 이후 커뮤니티를 다루는 부분에서 자세히 살펴봅니다. + +### 인간 기억 모델과의 대응 + +이처럼 원시 에피소드 데이터와 거기서 파생된 의미 개체 정보를 **이중으로 저장(dual storage)**하는 설계는 인간 기억에 관한 심리학적 모델을 반영합니다. 논문이 인용하는 이 심리학 모델은 기억을 두 종류로 구분합니다. 하나는 개별적이고 구체적인 사건을 나타내는 **일화 기억(episodic memory)**이고, 다른 하나는 개념들 사이의 연관과 그 의미를 담는 **의미 기억(semantic memory)**입니다. Zep의 에피소드 부분그래프가 전자에, 의미 개체 부분그래프가 후자에 대응하는 구조입니다. 이러한 설계 덕분에 Zep을 사용하는 LLM 에이전트는 인간 기억 체계에 대한 이해와 더 잘 부합하는, 한층 정교하고 섬세한 메모리 구조를 형성할 수 있게 됩니다. + +지식 그래프는 이러한 기억 구조를 표현하는 효과적인 매개체이며, 일화와 의미를 별도의 부분그래프로 구분한 이 구현은 AriGraph에서 제안된 유사한 접근법에서 착안한 것입니다. 또한 고수준 구조와 도메인 개념을 표현하기 위해 커뮤니티 노드를 활용한 것은 GraphRAG의 작업을 토대로 한 것으로, 도메인에 대한 더 포괄적이고 전역적인 이해를 가능하게 합니다. 그 결과 에피소드 → 사실 → 개체 → 커뮤니티로 이어지는 위계적 조직은 기존의 계층적 RAG 전략들을 확장하며, 이는 단일하거나 이중 층위에 머물던 기존 제안들보다 한층 풍부한 다층 구조를 형성합니다. + +지식 그래프의 전체 구조와 세 계층을 살펴보았으니, 이제 그 구축이 실제로 어디에서 시작되는지를 따라가 볼 차례입니다. + +### 에피소드: 그래프 구축의 출발점 + +Zep의 그래프 구축은 **에피소드(Episode)**라 불리는 원시 데이터 단위를 수집(ingestion)하는 데서 시작됩니다. 앞서 언급했듯 에피소드는 메시지(message), 텍스트(text), JSON이라는 세 가지 핵심 유형 중 하나일 수 있습니다. 각 유형은 그래프 구축 과정에서 서로 다른 처리를 요구하지만, 이 논문은 실험이 대화 메모리(conversation memory)를 중심으로 진행되기 때문에 **메시지 유형**에 초점을 맞춥니다. + +이 맥락에서 메시지는 비교적 짧은 텍스트(여러 메시지가 하나의 LLM 컨텍스트 윈도우 안에 들어갈 수 있을 정도의 길이)와, 그 발화를 생성한 행위자(actor) 정보로 구성됩니다. 그리고 각 메시지에는 그것이 전송된 시점을 나타내는 **참조 타임스탬프(reference timestamp)** \\(t_{\text{ref}}\\)가 부착됩니다. + +이 시간 정보가 왜 중요한지는 구체적인 예를 보면 분명해집니다. 사람의 대화에는 "다음 주 목요일", "2주 후", "지난여름"처럼 절대적이지 않고 **상대적이거나 부분적인 날짜 표현**이 흔히 등장합니다. 이런 표현은 그 자체로는 의미가 모호하지만, 메시지가 언제 전송되었는지를 알려주는 기준점 \\(t_{\text{ref}}\\)가 있으면 정확한 실제 날짜로 변환할 수 있습니다. 예컨대 메시지의 \\(t_{\text{ref}}\\)가 6월 1일이라면 "2주 후"는 6월 15일경으로 특정됩니다. Zep은 바로 이 참조 타임스탬프를 활용해 메시지 내용에 언급된 상대적·부분적 날짜를 정확히 식별하고 추출합니다. + +### 이중 시간 모델 + +Zep의 가장 독창적인 설계 중 하나는 **이중 시간 모델(bi-temporal model)**입니다. 여기에는 두 개의 서로 다른 타임라인이 등장합니다. 첫 번째 타임라인 \\(T\\)는 사건들이 실제로 발생한 **연대순(chronological ordering)**을 나타내고, 두 번째 타임라인 \\(T^{\prime}\\)은 Zep이 데이터를 실제로 수집·기록한 **거래순(transactional order)**을 나타냅니다. + +이 둘을 구분하는 이유를 직관적으로 이해해 보겠습니다. 어떤 사건이 실제로 일어난 시점과, 그 사건이 시스템에 입력된 시점은 서로 다를 수 있습니다. 예를 들어 사용자가 "지난여름에 이사했어요"라고 오늘 말했다면, 사건 발생 시점(지난여름)은 \\(T\\) 타임라인에, 그 정보가 Zep에 기록된 시점(오늘)은 \\(T^{\prime}\\) 타임라인에 놓입니다. \\(T^{\prime}\\) 타임라인은 전통적인 데이터베이스 감사(auditing), 즉 "언제 무엇이 기록되었는가"를 추적하는 일반적 목적에 부합합니다. 반면 \\(T\\) 타임라인은 대화 데이터와 메모리의 **동적 성격**을 모델링하는 추가적 차원을 제공합니다. 이러한 이중 시간 접근법은 LLM 기반 지식 그래프 구축에서 새로운 진전이며, 이전의 그래프 기반 RAG 제안들과 차별화되는 Zep의 고유 능력 상당 부분을 뒷받침합니다. 네 개의 타임스탬프(\\(t'_{\text{created}}\\), \\(t'_{\text{expired}}\\), \\(t_{\text{valid}}\\), \\(t_{\text{invalid}}\\))를 통한 동적 사실 갱신과 엣지 무효화의 구체적 작동 방식은 이후 시간 추출과 엣지 무효화를 다루는 부분에서 상세히 설명됩니다. + +### 양방향 인덱스와 비손실 추적 + +앞서 정의한 에피소드 엣지 \\(\mathcal{E}_e\\)는 에피소드를 그로부터 추출된 개체 노드와 연결합니다. 여기서 한 걸음 더 나아가, 에피소드와 그로부터 파생된 의미 엣지들은 **양방향 인덱스(bidirectional index)**를 유지하여, 엣지와 그 원천 에피소드 사이의 관계를 추적합니다. + +이 설계의 의의는 Graphiti 에피소드 부분그래프의 비손실적 성격을 강화하는 데 있습니다. 양방향 탐색이 가능해지기 때문입니다. 한 방향으로는, 추출된 의미적 산출물(개체나 사실)을 그 원천 에피소드로 거슬러 올라가 추적할 수 있어 **인용(citation)이나 출처 표시(quotation)**가 가능합니다. 반대 방향으로는, 어떤 에피소드로부터 그와 관련된 개체와 사실을 빠르게 조회할 수 있습니다. 앞서 회의록 비유로 설명했듯, 정리본에서 원문으로, 원문에서 정리본으로 양쪽 모두 자유롭게 오갈 수 있는 구조인 셈입니다. 논문은 이러한 연결이 본 논문의 실험에서는 직접 검증되지 않지만 향후 연구에서 탐구될 것이라고 밝히고 있습니다. +## 의미 개체와 사실 + +지식 그래프의 형식적 구조와 에피소드 수집 과정을 살펴보았으니, 이제 그 원시 에피소드로부터 어떻게 실제 의미 정보가 추출되어 그래프에 통합되는지를 따라갈 차례입니다. 이 과정은 크게 개체(entity)의 추출과 해소, 사실(fact)의 추출과 중복 제거, 그리고 시간 정보 추출과 엣지 무효화라는 세 갈래로 나뉩니다. + +### 개체 추출과 해소 + +개체 추출은 에피소드 처리의 첫 단계입니다. 시스템은 새로 들어온 메시지 내용뿐 아니라 직전의 \\(n\\)개 메시지를 함께 처리하여 개체명 인식(named entity recognition)에 필요한 맥락(context)을 확보합니다. 본 논문과 Zep의 일반적 구현에서 이 값은 \\(n=4\\)로 설정되어 있으며, 이는 완결된 두 번의 대화 턴(turn)에 해당하는 맥락을 제공합니다. 왜 직전 메시지들이 필요한지는 직관적으로 분명합니다. 가령 "그 사람도 거기 출신이래"라는 한 문장만 떼어 보면 "그 사람"과 "거기"가 누구·어디를 가리키는지 알 수 없지만, 앞선 대화 턴들을 함께 보면 지시 대상을 정확히 식별할 수 있기 때문입니다. 본 연구가 메시지 처리에 초점을 두고 있으므로, 발화자(speaker)는 자동으로 하나의 개체로 추출됩니다. + +초기 개체 추출이 끝나면, 시스템은 환각(hallucination)을 최소화하고 추출 누락을 줄이기 위해 **반성(reflection)** 기법을 적용합니다. 이는 Reflexion에서 소개된 언어 에이전트 자기 반성(self-reflection) 프레임워크에서 착안한 접근으로, 에이전트가 자신이 생성한 출력을 다시 한번 언어적 피드백을 통해 평가·보정하도록 하는 방식입니다. 즉 한 번의 추출로 결과를 확정하는 것이 아니라, "혹시 놓친 개체는 없는가, 잘못 만들어낸 개체는 없는가"를 LLM에게 재차 점검하게 함으로써 정밀도와 재현율을 동시에 끌어올리는 것입니다. 또한 시스템은 에피소드로부터 각 개체에 대한 **요약(summary)**도 함께 추출하는데, 이 요약은 이후의 개체 해소(entity resolution)와 검색(retrieval)이라는 두 가지 별개의 하위 작업 모두를 원활하게 하는 데 쓰입니다. + +추출 후에는 각 개체의 이름을 **1024차원 벡터 공간**으로 임베딩합니다. 이 임베딩 덕분에 기존 그래프의 개체 노드들과 **코사인 유사도(cosine similarity)** 검색을 통해 유사한 노드를 찾아낼 수 있습니다. 동시에 시스템은 기존 개체의 이름과 요약을 대상으로 별도의 **전문 검색(full-text search)**을 수행하여 추가 후보 노드를 확보합니다. 이렇게 모은 후보 노드들과 에피소드 맥락을 함께 개체 해소 프롬프트에 담아 LLM에 전달합니다. 만약 시스템이 어떤 개체를 기존 개체의 중복으로 판별하면, 갱신된 이름과 요약을 생성하여 통합합니다. 이 단계가 바로 개체 해소에 해당하며, 새로 추출된 "앨리스"가 그래프에 이미 존재하는 "앨리스"와 동일 인물인지 판단하여 중복을 합치는 과정입니다. + +이러한 개체 추출과 해소의 흐름을 개념적 의사코드로 정리하면 다음과 같습니다. 아래 코드는 논문이 서술한 절차를 이해를 돕기 위해 재구성한 개념적 예시로, 실제 구현 코드 그대로가 아니라 단계별 논리를 드러내기 위한 것입니다. + +```python +# 개념적 의사코드: 개체 추출 및 해소 파이프라인 +def extract_and_resolve_entities(current_message, context_messages, graph, t_ref): + # 1단계: 직전 n=4개 메시지를 맥락으로 한 개체명 인식 + context = context_messages[-4:] + raw_entities = llm_extract_entities(current_message, context) + # 발화자는 항상 하나의 개체로 포함됨 + + # 2단계: Reflexion에서 착안한 자기 반성 단계로 환각 최소화·누락 보완 + refined_entities = llm_reflect_and_refine(raw_entities, current_message, context) + + for entity in refined_entities: + # 3단계: 개체 요약 추출(이후 해소·검색에 모두 활용) + entity.summary = llm_summarize_entity(entity, current_message) + + # 4단계: 개체 이름을 1024차원 벡터로 임베딩 + entity_vector = embed(entity.name) # dim = 1024 + + # 5단계: 하이브리드 검색으로 후보 노드 확보 + cosine_candidates = graph.cosine_search(entity_vector) + text_candidates = graph.full_text_search(entity.name, entity.summary) + candidates = merge(cosine_candidates, text_candidates) + + # 6단계: LLM 기반 개체 해소 + resolution = llm_resolve_entity(entity, candidates, context) + if resolution.is_duplicate: + graph.update_node(resolution.existing_id, entity) # 이름·요약 갱신 + else: + graph.create_node(entity) +``` + +개체 추출과 해소가 끝나면, 시스템은 그 데이터를 **미리 정의된 Cypher 쿼리**를 사용해 지식 그래프에 반영합니다. 여기서 한 가지 설계적 결정에 주목할 필요가 있습니다. 논문은 LLM이 직접 데이터베이스 쿼리를 생성하도록 하는 대신, 사전에 고정해 둔 Cypher 쿼리를 사용하는 방식을 택했습니다. 그 이유는 일관된 스키마 형식(schema format)을 보장하고, LLM이 쿼리를 생성하면서 발생할 수 있는 환각 가능성을 줄이기 위해서입니다. 즉 추출과 판단이라는 "유연성이 필요한 부분"에만 LLM을 쓰고, 데이터 삽입이라는 "정확성이 중요한 부분"은 결정론적 쿼리에 맡기는 역할 분담을 한 셈입니다. 그래프 구축에 사용되는 구체적인 프롬프트들은 부록에 정리되어 있습니다. + +### 사실 추출과 중복 제거 + +개체가 그래프의 노드라면, **사실(fact)**은 그 개체들을 잇는 관계, 즉 엣지에 해당합니다. 각 사실은 그 관계의 핵심을 나타내는 **술어(predicate)**를 담고 있습니다. 여기서 중요한 점은, **동일한 사실이 서로 다른 개체 쌍 사이에서 여러 번 추출될 수 있다**는 것입니다. 이를 통해 Graphiti는 **하이퍼엣지(hyper-edge)** 구현을 통해 여러 개체가 얽힌 복잡한 다중 개체 사실(multi-entity fact)을 모델링할 수 있습니다. 기술적 맥락에서 보면, 표준적인 그래프(노드 두 개를 잇는 이진 엣지만 허용하는 구조) 위에서 여러 개체에 걸친 사실을 표현하기 위해 같은 사실을 여러 개체 쌍에 걸쳐 반복 추출하는 방식으로 하이퍼엣지의 효과를 얻는 것으로 이해할 수 있습니다. + +추출된 사실에 대해서도 시스템은 그래프 통합을 준비하기 위해 임베딩을 생성합니다. 이후 엣지 중복 제거(edge deduplication)는 앞서 설명한 개체 해소와 유사한 절차로 진행됩니다. 다만 여기에는 중요한 제약이 하나 추가됩니다. 관련 엣지를 찾는 하이브리드 검색이 **새 엣지가 잇는 것과 동일한 개체 쌍(entity pair) 사이에 존재하는 엣지들로만 한정**된다는 점입니다. + +이 제약은 두 가지 효과를 동시에 냅니다. 첫째, 서로 다른 개체들 사이의 비슷한 엣지가 잘못 결합되는 오류를 방지합니다. 예컨대 "앨리스—근무한다—회사 X"와 "밥—근무한다—회사 Y"는 술어가 같아 의미적으로 유사해 보이지만, 개체 쌍이 다르므로 결코 같은 사실로 합쳐져서는 안 됩니다. 둘째, 검색 공간을 특정 개체 쌍에 관련된 엣지의 부분집합으로 좁힘으로써 중복 제거 과정의 계산 복잡도를 크게 낮춥니다. 전체 그래프의 모든 엣지를 대상으로 비교하는 대신 해당 개체 쌍에 걸린 소수의 엣지만 비교하면 되기 때문에, 그래프가 커질수록 이 절감 효과는 더욱 두드러집니다. + +### 시간 정보 추출과 엣지 무효화 + +Graphiti가 다른 지식 그래프 엔진과 결정적으로 차별화되는 지점은, **시간 정보 추출(temporal extraction)**과 **엣지 무효화(edge invalidation)**를 통해 동적인 정보 갱신을 관리하는 능력입니다. 세상의 사실은 고정되어 있지 않습니다. "앨리스는 회사 X에 다닌다"는 사실은 어느 시점부터 참이었다가 이직과 함께 거짓이 될 수 있습니다. Graphiti는 바로 이런 변화를 폐기 없이 추적하도록 설계되어 있습니다. + +시스템은 앞서 소개한 참조 타임스탬프 \\(t_{\text{ref}}\\)를 활용하여 에피소드 맥락으로부터 사실에 관한 시간 정보를 추출합니다. 이를 통해 "앨런 튜링은 1912년 6월 23일에 태어났다" 같은 **절대 타임스탬프**와, "나는 2주 전에 새 직장을 시작했다" 같은 **상대 타임스탬프**를 모두 정확한 날짜 표현으로 변환하여 추출할 수 있습니다. 후자의 경우, 메시지가 전송된 기준 시점 \\(t_{\text{ref}}\\)가 있어야만 "2주 전"이 실제 어떤 날짜인지 특정할 수 있다는 점이 핵심입니다. + +앞서 소개한 이중 시간 모델(bi-temporal model)에 따라, 시스템은 각 사실에 대해 네 개의 타임스탬프를 추적합니다. 이 네 타임스탬프는 두 개의 서로 다른 타임라인에 나뉘어 속하는데, 이를 정리하면 다음과 같습니다. + +| 타임스탬프 | 속하는 타임라인 | 의미 | +|---|---|---| +| \\(t'_{\text{created}}\\) | 거래 타임라인 \\(T'\\) | 시스템에서 사실이 생성·기록된 시점 | +| \\(t'_{\text{expired}} \in T'\\) | 거래 타임라인 \\(T'\\) | 시스템에서 사실이 무효화된 시점 | +| \\(t_{\text{valid}}\\) | 사건 타임라인 \\(T\\) | 사실이 실제 세계에서 참이 되기 시작한 시점 | +| \\(t_{\text{invalid}} \in T\\) | 사건 타임라인 \\(T\\) | 사실이 실제 세계에서 참이기를 멈춘 시점 | + +여기서 \\(t'_{\text{created}}\\)와 \\(t'_{\text{expired}}\\)는 거래 타임라인 \\(T'\\)에 속하여 "언제 이 사실이 시스템에 들어왔고 언제 시스템 차원에서 무효 처리되었는가"를 감시(monitor)하는 역할을 합니다. 반면 \\(t_{\text{valid}}\\)와 \\(t_{\text{invalid}}\\)는 사건 타임라인 \\(T\\)에 속하여 "그 사실이 현실에서 실제로 참이었던 시간 범위"를 추적합니다. 이 시간 데이터들은 다른 사실 정보와 함께 엣지 위에 저장됩니다. + +이제 새로운 엣지가 추가될 때 어떤 일이 벌어지는지를 살펴보겠습니다. 새 엣지의 도입은 데이터베이스에 이미 존재하는 엣지를 무효화할 수 있습니다. 시스템은 LLM을 사용하여 새 엣지를 의미적으로 관련된 기존 엣지들과 비교하면서 잠재적 모순(contradiction)을 식별합니다. 그리고 **시간적으로 겹치는 모순**이 발견되면, 영향을 받는 기존 엣지를 무효화합니다. 이때 무효화의 구체적 방식이 흥미로운데, 기존 엣지의 \\(t_{\text{invalid}}\\)를 무효화를 일으킨 새 엣지의 \\(t_{\text{valid}}\\) 값으로 설정합니다. 다시 말해, 새로운 사실이 참이 되기 시작한 그 순간을 기존 사실이 거짓이 되는 경계로 삼는 것입니다. + +이 무효화 절차를 개념적 의사코드로 표현하면 다음과 같습니다. 아래 역시 논문의 서술을 이해를 돕기 위해 재구성한 개념적 예시입니다. + +```python +# 개념적 의사코드: 시간 기반 엣지 무효화 +def process_new_edge(new_edge, graph): + # 새 엣지의 사실을 임베딩 + new_edge_vector = embed(new_edge.fact) + + # 동일 개체 쌍으로 검색을 한정하여 관련 기존 엣지 탐색 + related_edges = graph.search_edges_by_entity_pair( + new_edge.source, new_edge.target + ) + + for existing_edge in related_edges: + # LLM으로 모순 여부 판단 + contradiction = llm_check_contradiction(new_edge, existing_edge) + # 시간적으로 겹치는 모순일 때만 무효화 + if contradiction and temporal_overlap(new_edge, existing_edge): + # 기존 엣지의 t_invalid를 새 엣지의 t_valid로 설정 + existing_edge.t_invalid = new_edge.t_valid + graph.update_edge(existing_edge) + + graph.create_edge(new_edge) +``` + +여기서 한 가지 설계 철학을 짚어둘 만합니다. 논문은 거래 타임라인 \\(T'\\)를 따라 Graphiti가 엣지 무효화를 결정할 때 **일관되게 새로운 정보를 우선시한다**고 명시합니다. 즉 같은 \\(T'\\) 안에서 더 나중에 수집된 정보를 더 오래된 정보보다 신뢰하는 것입니다. 분석적으로 덧붙이자면, 이는 결코 유일하게 가능한 선택은 아닙니다. 예컨대 출처의 신뢰도에 따라 우선순위를 정하는 대안적 설계도 상상할 수 있으나, Graphiti는 "최신 수집 정보를 신뢰한다"는 단순하고 일관된 규칙을 채택함으로써 동적 갱신의 예측 가능성을 확보합니다. + +결국 이 종합적인 접근법 덕분에 Graphiti는 대화가 진행됨에 따라 데이터를 동적으로 추가하면서도, 현재의 관계 상태와 그 관계가 시간에 따라 진화해 온 역사적 기록을 **동시에** 유지할 수 있습니다. 사실을 덮어쓰거나 지우지 않고 유효 기간을 갱신하는 방식이기에, 시스템은 "지금 참인 것"뿐 아니라 "과거 한때 참이었던 것"까지 모두 보존하는 비손실적(non-lossy) 시간 인식 메모리를 구현하게 되는 것입니다. +## 커뮤니티 + +에피소드 부분그래프와 의미 개체 부분그래프를 구축하고 나면, Zep은 마지막 층인 **커뮤니티 부분그래프**를 **커뮤니티 탐지(community detection)**를 통해 완성합니다. 앞서 커뮤니티 노드가 강하게 연결된 개체들의 군집을 나타내며 고수준 요약을 담는다고 정의했는데, 이 부분에서는 그러한 군집이 실제로 어떤 알고리즘으로 형성되고, 새로운 데이터가 흘러들어올 때 어떻게 효율적으로 갱신되는지를 구체적으로 살펴봅니다. + +### 레이블 전파 알고리즘의 선택 + +Zep의 커뮤니티 탐지 기법은 GraphRAG에서 서술된 방식을 토대로 하지만, 한 가지 중요한 차이를 둡니다. GraphRAG가 사용하는 **Leiden 알고리즘** 대신 **레이블 전파(label propagation) 알고리즘**을 채택한 것입니다. 이 선택의 동기를 이해하려면 두 알고리즘이 무엇을 우선시하는지를 대조해 볼 필요가 있습니다. + +[Leiden 알고리즘](https://doi.org/10.1038/s41598-019-41695-z)은 Louvain 알고리즘을 개선한 기법으로, 탐지된 커뮤니티가 항상 **연결성(connectivity)을 보장**하도록 설계되었다는 강점을 지닙니다. 참조 연구에 따르면 기존 Louvain 알고리즘은 최대 25%의 커뮤니티가 빈약하게 연결되고 16%는 완전히 단절되는 결함을 보였는데, Leiden은 이를 이론적으로 해결하면서도 더 빠른 실행 속도를 달성합니다. 즉 Leiden은 **분할 품질(partition quality)**의 최적성을 추구하는 알고리즘입니다. + +그러나 Zep은 이러한 품질 보장보다 **동적 확장성(dynamic extensibility)**을 우선시했습니다. 그 이유가 핵심인데, 레이블 전파는 새로운 데이터가 그래프에 들어올 때 전체를 다시 계산하지 않고도 손쉽게 점진적으로 확장할 수 있는 구조이기 때문입니다. 이 덕분에 시스템은 새 데이터가 유입되어도 비교적 오랜 기간 동안 정확한 커뮤니티 표현을 유지할 수 있으며, 비용이 큰 **전체 커뮤니티 재계산(complete community refresh)**의 필요 시점을 뒤로 미룰 수 있습니다. 운영 환경에서 지연 시간과 LLM 추론 비용을 줄이는 것이 결정적으로 중요한 Zep의 설계 철학에 비추어 보면, "최적이지만 정적인" Leiden보다 "다소 차선이지만 동적인" 레이블 전파가 더 합리적인 선택이었던 셈입니다. + +### 레이블 전파의 기본 원리 + +레이블 전파 알고리즘의 작동 방식을 직관적으로 이해해 보겠습니다. 처음에는 모든 노드가 각자 고유한 커뮤니티를 갖습니다. 그다음 각 노드는 자신의 이웃 노드들이 가장 많이 속한 커뮤니티, 즉 **다수결(plurality)로 결정되는 커뮤니티**를 자신의 새 레이블로 채택합니다. 이 과정을 어떤 노드의 레이블도 더 이상 바뀌지 않을 때까지 반복하면, 결국 강하게 연결된 노드들이 같은 레이블로 수렴하면서 자연스럽게 군집이 형성됩니다. 관련 연구에서 정리한 갱신 규칙은 다음과 같이 표현할 수 있습니다. + +$$ y\_i^{(t+1)} = \text{argmax}\_c \sum\_{j \in \mathcal{N}(i)} w\_{ij} \, \mathbb{1}[y\_j^{(t)} = c] $$ + +여기서 \\(y_i^{(t+1)}\\)은 노드 \\(i\\)가 \\(t+1\\) 시점에 새로 채택하는 커뮤니티 레이블, \\(\mathcal{N}(i)\\)는 노드 \\(i\\)의 이웃 집합, \\(w_{ij}\\)는 노드 \\(i\\)와 \\(j\\) 사이 엣지의 가중치, 그리고 \\(\mathbb{1}[\cdot]\\)는 이웃 \\(j\\)가 커뮤니티 \\(c\\)에 속할 때만 1이 되는 지시 함수(indicator function)입니다. 직관적으로 이 식은 "각 이웃이 자신의 커뮤니티에 가중치만큼 투표를 던지고, 가장 많은 표를 받은 커뮤니티를 채택한다"는 가중 다수결 투표를 형식화한 것입니다. + +Graphiti의 실제 구현을 살펴보면 이 원리가 그대로 코드로 구현되어 있음을 확인할 수 있습니다. 특히 엣지의 개수(`edge_count`)가 가중치 \\(w_{ij}\\)의 역할을 한다는 점이 흥미롭습니다. 두 개체 사이에 여러 사실(엣지)이 존재할수록 그 연결의 영향력이 커지는 것입니다. + +```python +def label_propagation(projection: dict[str, list[Neighbor]]) -> list[list[str]]: + # 1. 각 노드를 자기 자신만의 커뮤니티로 초기화 + community_map = {uuid: i for i, uuid in enumerate(projection.keys())} + + while True: + no_change = True + new_community_map: dict[str, int] = {} + + for uuid, neighbors in projection.items(): + curr_community = community_map[uuid] + + # 2. 이웃들의 커뮤니티별로 엣지 개수(가중치)를 누적 + community_candidates: dict[int, int] = defaultdict(int) + for neighbor in neighbors: + community_candidates[community_map[neighbor.node_uuid]] += neighbor.edge_count + community_lst = [ + (count, community) for community, count in community_candidates.items() + ] + + # 3. 가중치 내림차순 정렬 후 최상위 후보 선택 + community_lst.sort(reverse=True) + candidate_rank, community_candidate = community_lst[0] if community_lst else (0, -1) + if community_candidate != -1 and candidate_rank > 1: + new_community = community_candidate + else: + # 동점 처리: 더 큰(번호가 높은) 커뮤니티로 귀속 + new_community = max(community_candidate, curr_community) + + new_community_map[uuid] = new_community + if new_community != curr_community: + no_change = False + + # 4. 어떤 노드도 변하지 않으면 수렴으로 간주하고 종료 + if no_change: + break + community_map = new_community_map + + community_cluster_map: dict[int, list[str]] = defaultdict(list) + for uuid, community in community_map.items(): + community_cluster_map[community].append(uuid) + return list(community_cluster_map.values()) +``` + +이 구현에서 주목할 부분은 동점(tie) 처리 방식입니다. 가장 많은 표를 받은 후보가 명확하지 않을 때는 `max(community_candidate, curr_community)`를 통해 더 큰 커뮤니티 식별자로 귀속시킴으로써 **결정론적(deterministic) 수렴**을 보장합니다. 레이블 전파는 본래 무작위성이 개입할 수 있는 알고리즘인데, 이렇게 동점을 일관된 규칙으로 깨뜨림으로써 같은 입력에 대해 항상 같은 결과가 나오도록 안정화한 것입니다. 또한 이 그래프 투영(`projection`)은 실제로는 개체 노드들 사이의 `RELATES_TO` 관계, 즉 앞서 다룬 의미 엣지의 개수를 집계하여 만들어집니다. 즉 사실로 강하게 얽힌 개체들일수록 같은 커뮤니티로 묶일 가능성이 높아지는 구조입니다. + +### 동적 확장: 단일 재귀 단계의 휴리스틱 + +레이블 전파를 선택한 진짜 이유가 발휘되는 지점이 바로 **동적 확장(dynamic extension)**입니다. 이 동적 확장은 레이블 전파에서 **단 한 번의 재귀 단계(single recursive step)**가 수행하는 로직을 그대로 구현한 것입니다. 시스템이 새로운 개체 노드 \\(n_i \in \mathcal{N}_s\\)를 그래프에 추가할 때, 전체 그래프에 대해 알고리즘을 처음부터 다시 돌리는 대신 그 노드의 **이웃 노드들이 속한 커뮤니티만을 조사**합니다. 그리고 이웃들 가운데 다수가 속한 커뮤니티에 새 노드를 배정한 다음, 그에 맞추어 커뮤니티 요약과 그래프를 갱신합니다. + +이 점진적 갱신 로직 역시 Graphiti 코드에서 명확히 드러납니다. 새 개체가 이미 어떤 커뮤니티에 속해 있는지 먼저 확인하고, 속해 있지 않다면 주변 개체들의 **최빈(mode) 커뮤니티**를 찾아 배정하는 방식입니다. + +```python +async def determine_entity_community( + driver: GraphDriver, entity: EntityNode +) -> tuple[CommunityNode | None, bool]: + # 이미 어떤 커뮤니티의 멤버인지 확인 + records, _, _ = await driver.execute_query( + """ + MATCH (c:Community)-[:HAS_MEMBER]->(n:Entity {uuid: $entity_uuid}) + RETURN + """ + COMMUNITY_NODE_RETURN, + entity_uuid=entity.uuid, + ) + if len(records) > 0: + return get_community_node_from_record(records[0]), False + + # 소속 커뮤니티가 없으면, 인접 개체들의 최빈 커뮤니티를 탐색 + records, _, _ = await driver.execute_query( + """ + MATCH (c:Community)-[:HAS_MEMBER]->(m:Entity)-[:RELATES_TO]-(n:Entity {uuid: $entity_uuid}) + RETURN + """ + COMMUNITY_NODE_RETURN, + entity_uuid=entity.uuid, + ) + communities = [get_community_node_from_record(record) for record in records] + + # 이웃이 속한 커뮤니티 중 가장 빈도가 높은 것을 선택 (다수결) + community_map: dict[str, int] = defaultdict(int) + for community in communities: + community_map[community.uuid] += 1 + + community_uuid = None + max_count = 0 + for uuid, count in community_map.items(): + if count > max_count: + community_uuid = uuid + max_count = count + + if max_count == 0: + return None, False + for community in communities: + if community.uuid == community_uuid: + return community, True + return None, False +``` + +여기서 한 가지 중요한 트레이드오프를 정직하게 짚어둘 필요가 있습니다. 논문은 이 동적 갱신이 데이터 유입 시 효율적인 커뮤니티 확장을 가능하게 하지만, 그 결과로 형성된 커뮤니티가 **완전한 레이블 전파를 처음부터 다시 실행했을 때 얻어질 커뮤니티로부터 점진적으로 벗어난다(diverge)**고 명시합니다. 이를 흔히 커뮤니티 **드리프트(drift)**라 부릅니다. 비유하자면, 동네 경계를 새 건물이 들어설 때마다 "주변 건물이 어느 동네에 속하는가"만 보고 즉석에서 정하다 보면, 시간이 지날수록 전체를 한꺼번에 다시 구획했을 때의 이상적인 경계와 어긋나게 되는 것과 같습니다. 따라서 **주기적인 전체 커뮤니티 재계산은 여전히 필요**합니다. 그럼에도 이 동적 갱신 전략은 지연 시간과 LLM 추론 비용을 크게 절감하는 실용적 휴리스틱(practical heuristic)으로서 그 값어치를 합니다. 즉 "완벽한 정확성을 매번 추구하는 대신, 충분히 좋은 근사를 저렴하게 유지하다가 가끔씩만 정밀하게 보정한다"는 운영상의 현실적 타협인 것입니다. + +### 맵-리듀스 방식의 커뮤니티 요약 + +커뮤니티 노드가 담는 **요약(summary)**은 GraphRAG의 방식을 따라 구성원 노드들에 대한 **반복적 맵-리듀스(map-reduce) 스타일의 요약**을 통해 도출됩니다. 이 과정의 구현은 이진 트리(binary tree) 형태의 점진적 병합으로 이루어진다는 점이 흥미롭습니다. 구성원 개체들의 요약을 둘씩 짝지어 LLM으로 병합 요약을 생성하고, 그 결과를 다시 둘씩 짝지어 병합하는 식으로 요약의 개수가 1이 될 때까지 상향식(bottom-up)으로 줄여나가는 것입니다. + +```python +async def build_community( + llm_client: LLMClient, community_cluster: list[EntityNode] +) -> tuple[CommunityNode, list[CommunityEdge]]: + summaries = [entity.summary for entity in community_cluster] + length = len(summaries) + # 요약이 하나로 줄어들 때까지 쌍단위로 병합 (이진 트리 리듀스) + while length > 1: + odd_one_out: str | None = None + if length % 2 == 1: + odd_one_out = summaries.pop() # 홀수 개일 때 하나를 남겨둠 + length -= 1 + new_summaries = list( + await semaphore_gather( + *[ + summarize_pair(llm_client, (str(left), str(right))) + for left, right in zip( + summaries[: int(length / 2)], summaries[int(length / 2):], strict=False + ) + ] + ) + ) + if odd_one_out is not None: + new_summaries.append(odd_one_out) + summaries = new_summaries + length = len(summaries) + + summary = truncate_at_sentence(summaries[0], MAX_SUMMARY_CHARS) + name = await generate_summary_description(llm_client, summary) # 커뮤니티 이름 생성 + ... +``` + +이러한 이진 트리 리듀스 방식의 장점은 계산 복잡도를 낮추면서도 여러 LLM 호출을 동시에(concurrently) 병렬 처리할 수 있다는 데 있습니다. 실제로 구현은 세마포어(semaphore)로 동시 실행 개수를 최대 10개로 제한하여 병렬성과 자원 사용 사이의 균형을 맞춥니다. 한편 앞서 살펴본 동적 갱신 시에는 전체 트리를 다시 구성하는 대신, 새로 들어온 개체의 요약과 기존 커뮤니티 요약을 한 쌍으로 묶어 `summarize_pair`로 병합하는 가벼운 방식(`update_community`)으로 처리됩니다. 이 역시 전체 재계산을 피하려는 동일한 효율성 철학의 연장선입니다. + +### 검색을 위한 커뮤니티 이름 임베딩 + +마지막으로, Zep의 요약 활용 방식은 GraphRAG와 결정적으로 갈라집니다. 논문은 GraphRAG의 맵-리듀스식 검색(retrieval) 방식과 자신들의 검색 방법론이 **상당히 다르다**고 분명히 밝힙니다. 이 차이를 뒷받침하기 위해, Zep은 커뮤니티 요약으로부터 **핵심 용어와 관련 주제를 담은 커뮤니티 이름(community name)**을 생성합니다. 위 코드에서 `generate_summary_description`이 바로 이 이름을 만들어내는 단계입니다. 그리고 이 이름들을 임베딩하여 저장함으로써, 이후 검색 단계에서 **코사인 유사도(cosine similarity) 검색**의 대상으로 삼을 수 있게 합니다. + +이 설계가 의미하는 바를 직관적으로 풀면 이렇습니다. GraphRAG는 질의가 들어올 때 모든 커뮤니티 요약을 LLM으로 훑어가며 맵-리듀스로 답을 종합하는데, 이는 비용과 지연이 큽니다. 반면 Zep은 커뮤니티마다 핵심어가 압축된 짧은 이름을 미리 임베딩해 두었다가, 질의 벡터와의 코사인 유사도만으로 관련 커뮤니티를 빠르게 골라냅니다. 즉 무거운 LLM 종합 과정을 가벼운 벡터 유사도 검색으로 대체함으로써, 앞서 강조된 Zep의 낮은 지연 시간과 비용 효율성이라는 목표가 검색 단계에서도 일관되게 관철되는 것입니다. 이렇게 임베딩된 커뮤니티 이름이 실제 검색 파이프라인에서 어떻게 활용되는지는 이어지는 메모리 검색 부분에서 상세히 다루어집니다. +## 메모리 검색 + +지식 그래프를 구축하고 그 안에 시간 정보를 비손실적으로 축적하는 과정을 앞서 상세히 살펴보았습니다. 이렇게 체계적으로 추적해 온 데이터는 결국 LLM 에이전트가 질의에 답할 수 있도록 적절한 맥락으로 제공될 때 비로소 활용됩니다. 이 부분에서는 Zep이 그래프에 저장된 정보를 어떻게 꺼내어 LLM이 쓸 수 있는 텍스트 맥락으로 변환하는지를 형식적으로 정의합니다. 논문은 Zep의 메모리 검색 시스템이 강력하고 복잡하며 높은 설정 가능성(highly configurable)을 갖춘 기능이라고 강조하는데, 그 핵심은 그래프 검색 API가 하나의 함수로 추상화된다는 점에 있습니다. + +### 검색 파이프라인의 형식적 정의 + +Zep의 그래프 검색 API는 다음과 같은 함수로 표현됩니다. + +$$ f : S \to S $$ + +여기서 \\(S\\)는 텍스트 문자열(string)들의 공간을 의미합니다. 이 함수 \\(f\\)는 텍스트 문자열 질의 \\(\alpha \in S\\)를 입력으로 받아, 텍스트 문자열 맥락 \\(\beta \in S\\)를 출력으로 반환합니다. 직관적으로 말하면, 사용자가 던진 자연어 질문을 받아서 LLM 에이전트가 정확한 응답을 생성하는 데 필요한, 노드와 엣지로부터 형식화된 데이터를 담은 맥락 텍스트를 만들어 주는 것입니다. 즉 입력도 텍스트, 출력도 텍스트이지만 그 사이에서 지식 그래프 전체가 검색·정제되는 셈입니다. + +이 변환 과정 \\(f(\alpha) \to \beta\\)는 세 개의 구별되는 단계가 차례로 연결되어 이루어집니다. 각 단계를 하나씩 살펴보면 다음과 같습니다. + +첫 번째 단계는 **검색(Search)** 단계로, 함수 \\(\varphi\\)로 표현됩니다. 이 단계는 관련 정보를 담고 있을 가능성이 있는 후보 노드와 엣지를 식별하는 데서 시작합니다. Zep은 여러 가지 서로 다른 검색 방법을 사용하지만, 전체 검색 함수는 다음과 같이 하나로 표현할 수 있습니다. + +$$ \varphi : S \to \mathcal{E}\_s^n \times \mathcal{N}\_s^n \times \mathcal{N}\_c^n $$ + +여기서 \\(\varphi\\)는 질의 문자열을 받아 세 가지 요소로 이루어진 3-튜플(3-tuple)로 변환합니다. 그 세 요소는 각각 의미 엣지(semantic edge)의 리스트 \\(\mathcal{E}_s^n\\), 개체 노드(entity node)의 리스트 \\(\mathcal{N}_s^n\\), 그리고 커뮤니티 노드(community node)의 리스트 \\(\mathcal{N}_c^n\\)입니다. 위첨자 \\(n\\)은 각 유형마다 여러 개의 결과가 리스트 형태로 반환됨을 나타냅니다. 앞서 지식 그래프 구축 부분에서 정의했던 세 가지 그래프 요소—의미 엣지, 개체 노드, 커뮤니티 노드—가 바로 관련 텍스트 정보를 담고 있는 세 가지 그래프 유형이며, 검색 단계는 질의에 대해 이 세 유형 각각에서 후보를 길어 올리는 역할을 합니다. + +두 번째 단계는 **재순위화(Reranker)** 단계로, 함수 \\(\rho\\)로 표현됩니다. 이 단계는 검색 결과를 재정렬하는 역할을 합니다. 재순위화 함수 혹은 모델은 검색 결과의 리스트를 받아 그것을 재정렬한 버전을 산출합니다. + +$$ \rho : \varphi(\alpha), \ldots \to \mathcal{E}\_s^n \times \mathcal{N}\_s^n \times \mathcal{N}\_c^n $$ + +이 시그니처에서 주목할 부분은 입력에 \\(\varphi(\alpha)\\) 뒤에 붙은 줄임표(\\(\ldots\\))입니다. 이는 재순위화 함수가 단순히 검색 결과만 받는 것이 아니라 추가적인 인자를 함께 받을 수 있음을 시사합니다. 실제로 뒤에서 살펴볼 여러 재순위화 방식 중 일부는 중심 노드나 질의 같은 부가 정보를 함께 활용합니다. 출력 공간은 검색 단계의 출력과 동일한 3-튜플 형태이므로, 재순위화는 결과의 "구성"을 바꾸는 것이 아니라 그 "순서"를 바꾸는 변환임을 알 수 있습니다. + +세 번째 단계는 **구성(Constructor)** 단계로, 함수 \\(\chi\\)로 표현됩니다. 이 마지막 단계는 관련 노드와 엣지를 텍스트 맥락으로 변환합니다. + +$$ \chi : \mathcal{E}\_s^n \times \mathcal{N}\_s^n \times \mathcal{N}\_c^n \to S $$ + +구성 함수가 각 그래프 요소로부터 정확히 어떤 필드를 끄집어내는지가 중요한데, 이는 앞서 다룬 이중 시간 모델과 직접 연결됩니다. 각 의미 엣지 \\(e_i \in \mathcal{E}_s\\)에 대해서는 사실(fact)과 함께 \\(t_{\text{valid}}\\), \\(t_{\text{invalid}}\\) 필드를 반환합니다. 즉 단순히 "무엇이 사실인가"뿐 아니라 "그 사실이 언제부터 언제까지 참이었는가"라는 시간 범위를 함께 제공하는 것입니다. 앞서 엣지 무효화 과정에서 관리하던 사건 타임라인 상의 두 타임스탬프가 바로 이 지점에서 LLM에게 전달되어, 에이전트가 시간적 추론을 수행할 수 있게 됩니다. 각 개체 노드 \\(n_i \in \mathcal{N}_s\\)에 대해서는 이름(name)과 요약(summary) 필드를, 그리고 각 커뮤니티 노드 \\(n_i \in \mathcal{N}_c\\)에 대해서는 요약 필드를 반환합니다. + +이 세 구성 요소를 정의하고 나면, 전체 검색 함수 \\(f\\)는 다음과 같이 세 함수의 합성(composition)으로 깔끔하게 표현됩니다. + +$$ f(\alpha) = \chi(\rho(\varphi(\alpha))) = \beta $$ + +이 합성 구조를 단계별로 따라가 보면, 먼저 질의 \\(\alpha\\)가 검색 함수 \\(\varphi\\)에 들어가 후보 결과 3-튜플이 만들어지고, 그 결과가 재순위화 함수 \\(\rho\\)를 거쳐 관련성 높은 순서로 재배열되며, 마지막으로 구성 함수 \\(\chi\\)가 이를 텍스트 맥락 \\(\beta\\)로 변환합니다. 함수의 합성으로 검색 파이프라인을 정의한 이 방식은 각 단계를 독립적으로 교체하거나 설정할 수 있게 해 주며, 논문이 강조한 "높은 설정 가능성"이 형식적으로 어떻게 뒷받침되는지를 보여 줍니다. + +### 맥락 문자열 템플릿의 구조 + +구성 함수 \\(\chi\\)가 산출하는 최종 출력 \\(\beta\\)가 실제로 어떤 모습인지는 논문이 제시한 샘플 맥락 문자열 템플릿에서 확인할 수 있습니다. 이는 논문이 직접 제공하는 유일한 구체적 산출물이므로, 그 구조를 면밀히 들여다볼 가치가 있습니다. + +``` +FACTS and ENTITIES represent relevant context to the current conversation. +These are the most relevant facts and their valid date ranges. If the fact +is about an event, the event takes place during this time. +format: FACT (Date range: from - to) + +{facts} + +These are the most relevant entities +ENTITY_NAME: entity summary + +{entities} + +``` + +이 템플릿의 설계에서 몇 가지 의도를 읽어낼 수 있습니다. 우선 사실(FACTS)과 개체(ENTITIES)가 XML과 유사한 태그(``, ``)로 명확히 구획되어 있습니다. 이렇게 구조화된 구분자를 사용하면 LLM이 어느 부분이 사실 정보이고 어느 부분이 개체 정보인지를 혼동 없이 파싱할 수 있습니다. 특히 사실에 대해서는 `FACT (Date range: from - to)`라는 날짜 범위 주석 형식이 명시되어 있는데, 이는 앞서 구성 함수가 각 엣지에 대해 \\(t_{\text{valid}}\\)와 \\(t_{\text{invalid}}\\)를 함께 반환한다고 한 점과 정확히 대응합니다. 즉 사건 타임라인 상의 유효 구간이 "from - to" 형태로 LLM에게 명시적으로 전달되며, 사실이 어떤 사건에 관한 것이라면 그 사건이 해당 기간 동안 일어났음을 알려 줍니다. 이러한 날짜 범위 정보야말로 Zep이 시간적 추론 과제에서 강점을 보이는 근본적 토대가 됩니다. 개체에 대해서는 `ENTITY_NAME: entity summary` 형식으로 이름과 요약이 짝지어 제시되는데, 이는 구성 함수가 개체 노드로부터 이름과 요약 필드를 반환한다는 정의와 일치합니다. + +### 검색 + +검색 단계 \\(\varphi\\)의 내부를 들여다보면, Zep은 세 가지 검색 함수를 구현하고 있습니다. 코사인 의미 유사도 검색 \\(\varphi_{\text{cos}}\\), Okapi BM25 전문 검색(full-text search) \\(\varphi_{\text{bm25}}\\), 그리고 너비 우선 검색(breadth-first search) \\(\varphi_{\text{bfs}}\\)입니다. 앞의 두 함수는 Neo4j의 Lucene 구현을 활용합니다. 이 세 검색 함수는 각각 관련 문서를 식별하는 데 서로 다른 능력을 발휘하며, 함께 작동함으로써 재순위화 이전에 후보 결과를 포괄적으로 확보합니다. + +여기서 짚어둘 점은 검색 대상 필드가 세 가지 객체 유형마다 다르다는 것입니다. 의미 엣지 \\(\mathcal{E}_s\\)에 대해서는 사실(fact) 필드를 검색하고, 개체 노드 \\(\mathcal{N}_s\\)에 대해서는 개체 이름(entity name)을 검색하며, 커뮤니티 노드 \\(\mathcal{N}_c\\)에 대해서는 커뮤니티 이름(community name)을 검색합니다. 앞서 커뮤니티 부분에서 살펴보았듯이, 이 커뮤니티 이름은 해당 커뮤니티가 다루는 핵심 키워드와 구절들로 구성됩니다. 논문은 자신들의 커뮤니티 검색 방식이 독자적으로 개발되었지만 LightRAG의 고수준 키 검색(high-level key search) 방법론과 유사한 측면이 있다고 밝히며, LightRAG의 접근법을 Graphiti와 같은 그래프 기반 시스템과 결합하는 것이 향후 연구의 유망한 방향이라고 제시합니다. + +세 검색 방식 가운데 코사인 유사도와 전문 검색은 RAG 분야에서 이미 잘 정립된 방법론입니다. 반면 지식 그래프 위에서의 너비 우선 검색은 RAG 영역에서 상대적으로 주목받지 못해 왔으며, 그래프 기반 RAG 시스템인 AriGraph와 Distill-SynthKG가 주목할 만한 예외로 꼽힙니다. Graphiti에서 너비 우선 검색은 초기 검색 결과를 보강하는 역할을 하는데, \\(n\\)-홉(hop) 이내에 있는 추가적인 노드와 엣지를 식별함으로써 이를 수행합니다. + +너비 우선 검색이 무엇을 하는지 직관적으로 풀어 보면 이렇습니다. 그래프에서 어떤 시작점(seed) 노드로부터 한 단계씩 인접한 노드로 퍼져 나가며 탐색하는 방식이 너비 우선 검색입니다. 예를 들어 "앨리스"라는 개체에서 시작해 1-홉을 탐색하면 앨리스와 직접 연결된 사실과 개체들이 드러나고, 2-홉까지 넓히면 그 개체들과 또다시 연결된 정보까지 맥락으로 끌어올 수 있습니다. 더욱 흥미로운 점은 \\(\varphi_{\text{bfs}}\\)가 노드를 검색 매개변수로 받아들일 수 있다는 것입니다. 이는 검색 함수에 대한 한층 정교한 제어를 가능하게 하는데, 특히 최근 에피소드들을 너비 우선 검색의 시작점으로 사용할 때 그 진가가 발휘됩니다. 이렇게 하면 시스템은 최근에 언급된 개체와 관계를 검색된 맥락에 자연스럽게 포함시킬 수 있습니다. + +이 세 검색 방식이 서로 보완적인 이유는 각각이 겨냥하는 유사성의 종류가 다르기 때문입니다. 전문 검색은 단어 수준의 유사성(word similarity)을 포착하고, 코사인 유사도는 의미적 유사성(semantic similarity)을 포착하며, 너비 우선 검색은 맥락적 유사성(contextual similarity)을 드러냅니다. 여기서 맥락적 유사성이란, 그래프 상에서 서로 가까이 위치한 노드와 엣지일수록 더 유사한 대화 맥락에서 등장한다는 통찰에 기반합니다. 단어가 겹치지도 않고 의미 임베딩이 가깝지도 않더라도, 그래프 구조상 인접해 있다는 사실 자체가 관련성을 시사한다는 것입니다. 이처럼 세 가지 측면을 동시에 공략하는 다면적 접근 덕분에, 후보 결과를 식별하는 단계에서 최적의 맥락을 발견할 가능성이 극대화됩니다. + +### 재순위화 + +초기 검색 방식들이 높은 재현율(recall), 즉 관련 정보를 가능한 한 빠뜨리지 않고 폭넓게 끌어모으는 것을 목표로 한다면, 재순위화는 정밀도(precision)를 높이는 데 기여합니다. 다시 말해 폭넓게 모은 후보 가운데 가장 관련성 높은 결과를 앞쪽에 배치하는 역할입니다. 검색과 재순위화의 이러한 역할 분담은 정보 검색 분야의 전형적인 2단계 설계로, 먼저 값싼 방법으로 후보를 넓게 모은 뒤 더 정교한 방법으로 추려내는 것이 계산 효율과 품질을 동시에 잡는 길이기 때문입니다. + +Zep은 여러 재순위화 방식을 지원합니다. 그중에는 상호 순위 융합(Reciprocal Rank Fusion, RRF)과 최대 한계 관련성(Maximal Marginal Relevance, MMR) 같은 기존 방법이 포함됩니다. 상호 순위 융합은 서로 다른 검색 방법들이 각각 산출한 여러 순위 리스트를, 각 결과의 역순위(reciprocal rank)를 합산하는 방식으로 하나의 통합된 순위로 융합하는 기법입니다. 앞서 살펴본 세 가지 검색 방식이 서로 다른 순위를 내놓을 때, 이들을 일관된 하나의 순서로 결합하는 데 적합합니다. 최대 한계 관련성은 질의와의 관련성을 높이면서 동시에 이미 선택된 결과와의 중복성을 낮추도록 결과를 선별하는 기법으로, 결과의 다양성을 확보해 비슷비슷한 정보가 맥락을 가득 채우는 것을 방지합니다. + +이 두 가지 일반적 방법 외에도, Zep은 그래프 구조를 활용한 고유한 재순위화 방식들을 구현합니다. 첫째는 **에피소드 언급(episode-mentions) 재순위화**로, 대화 안에서 어떤 개체나 사실이 언급된 빈도를 기준으로 결과의 우선순위를 정합니다. 이를 통해 자주 참조되는 정보일수록 더 쉽게 접근할 수 있는 시스템을 구현합니다. 둘째는 **노드 거리(node distance) 재순위화**로, 지정된 중심 노드(centroid node)로부터의 그래프 상 거리를 기준으로 결과를 재정렬합니다. 이는 지식 그래프의 특정 영역에 국한된 맥락을 제공하고자 할 때 유용한데, 관심 있는 중심점 주변의 정보를 우선적으로 가져올 수 있기 때문입니다. 앞서 재순위화 함수 \\(\rho\\)의 시그니처에 줄임표가 있었던 것을 떠올리면, 이러한 중심 노드 같은 부가 인자가 바로 그 줄임표가 가리키던 추가 입력에 해당함을 알 수 있습니다. + +가장 정교한 재순위화 능력은 **교차 인코더(cross-encoder)**를 활용합니다. 교차 인코더란 노드와 엣지를 질의와 함께 평가하여 관련성 점수를 생성하는 LLM으로, 교차 어텐션(cross-attention)을 이용해 이 평가를 수행합니다. 여기서 핵심은 질의와 후보를 분리해 각각 임베딩하는 것이 아니라, 둘을 하나의 입력으로 묶어 모델에 통과시킨다는 점입니다. 이렇게 하면 질의의 모든 토큰과 후보의 모든 토큰이 어텐션을 통해 직접 상호작용하므로, 단순한 벡터 유사도보다 훨씬 정밀한 관련성 판단이 가능해집니다. 다만 이 방식은 후보마다 LLM을 한 번씩 통과시켜야 하므로 가장 높은 계산 비용을 수반합니다. 결국 재순위화 방식의 선택은 정밀도와 계산 비용 사이의 트레이드오프를 어떻게 다룰 것인가의 문제이며, Zep이 다섯 가지에 이르는 재순위화 전략을 모두 제공하여 상황에 맞게 선택할 수 있도록 한 점은 앞서 강조한 높은 설정 가능성을 다시 한번 구체적으로 입증합니다. +## 실험 + +지금까지 Zep의 지식 그래프 구축 방식과 메모리 검색 파이프라인을 형식적으로 살펴보았다면, 이 부분에서는 그러한 설계가 실제로 어떤 성능을 내는지를 두 개의 LLM 메모리 벤치마크를 통해 검증합니다. 첫 번째는 MemGPT가 자신들의 핵심 평가 지표로 확립한 **Deep Memory Retrieval(DMR)** 과제이고, 두 번째는 더 길고 복잡한 대화를 다루는 **LongMemEval** 벤치마크입니다. + +두 실험 모두 동일한 평가 절차를 공유합니다. 먼저 대화 기록 전체를 Zep의 API를 통해 지식 그래프로 수집(ingestion)하고, 앞서 메모리 검색 부분에서 설명한 기법을 사용하여 가장 관련성이 높은 **20개의 엣지(사실)와 개체 노드(개체 요약)**를 검색합니다. 그런 다음 시스템은 이 데이터를 하나의 맥락 문자열(context string)로 재구성하여 LLM 에이전트에 제공합니다. 여기서 짚어둘 점은, 이 두 실험이 Graphiti의 핵심 검색 능력을 보여주기는 하지만 시스템이 제공하는 전체 검색 기능의 일부분에 불과하다는 것입니다. 논문은 기존 벤치마크와의 명확한 비교를 위해 의도적으로 평가 범위를 좁혔으며, 지식 그래프의 추가적 기능에 대한 탐구는 향후 연구로 남겨 두었음을 밝힙니다. + +### 모델 선택 + +실험 구성을 정확히 이해하려면 어떤 모델들이 어떤 역할에 투입되었는지를 구분할 필요가 있습니다. 재순위화(reranking)와 임베딩(embedding) 작업에는 BAAI의 **BGE-m3 모델**이 사용되었습니다. 이 모델은 [Chen 등](https://doi.org/10.18653/v1/2024.findings-acl.137)이 제안한 자기 지식 증류(self-knowledge distillation) 기반의 다기능(multi-functionality)·다언어(multi-linguality)·다중 입자도(multi-granularity) 임베딩 모델로, 밀집 검색(dense retrieval)·희소 검색(sparse retrieval)·재순위화를 하나의 통합 아키텍처에서 처리할 수 있다는 점이 특징입니다. 이러한 다기능성은 Zep의 검색 파이프라인이 의미적 유사성 매칭과 후보 재순위화를 모두 요구한다는 점과 자연스럽게 맞아떨어집니다. + +그래프 구축에는 gpt-4o-mini-2024-07-18이 사용되었고, 제공된 맥락에 응답을 생성하는 채팅 에이전트로는 gpt-4o-mini-2024-07-18과 gpt-4o-2024-11-20 두 가지가 모두 활용되었습니다. 또한 MemGPT의 DMR 결과와 직접 비교할 수 있도록, DMR 평가에서는 gpt-4-turbo-2024-04-09를 추가로 사용했습니다. 실험에 쓰인 노트북과 프롬프트는 GitHub 저장소와 부록을 통해 공개됩니다. + +### Deep Memory Retrieval (DMR) + +DMR 평가는 [Xu, Szlam, Weston](https://doi.org/10.18653/v1/2022.acl-long.356)이 제안한 것으로, Multi-Session Chat 데이터셋의 500개 대화로 구성됩니다. 각 대화는 5개의 채팅 세션을 담고, 세션마다 최대 12개의 메시지가 포함되어 대화당 약 60개의 메시지가 됩니다. 그리고 각 대화에는 메모리 능력을 평가하기 위한 질문/답변 쌍이 하나씩 들어 있습니다. 기존에는 MemGPT 프레임워크가 gpt-4-turbo로 93.4%의 정확도를 달성하여 최고 성능을 기록했는데, 이는 재귀적 요약(recursive summarization)의 35.3% 베이스라인을 크게 앞지른 수치였습니다. + +비교 기준을 마련하기 위해 두 가지 일반적인 LLM 메모리 접근법이 구현되었습니다. 하나는 대화 전체를 그대로 맥락으로 넣는 **전체 대화(full-conversation)** 방식이고, 다른 하나는 **세션 요약(session summaries)** 방식입니다. gpt-4-turbo 기준으로 전체 대화 방식은 94.4%로 MemGPT의 보고 수치를 근소하게 앞섰고, 세션 요약은 78.6%에 그쳤습니다. gpt-4o-mini를 쓰면 두 방식 모두 향상되어 각각 98.0%와 88.0%를 기록했습니다. 다만 MemGPT의 결과는 공개된 방법론의 세부 정보가 부족해 gpt-4o-mini로는 재현할 수 없었다고 논문은 밝힙니다. + +Zep을 평가할 때는 대화를 수집한 뒤 검색 함수로 상위 10개의 노드와 엣지를 가져왔으며, LLM 심판(judge)이 에이전트의 응답을 정답(golden answer)과 비교했습니다. 그 결과 Zep은 gpt-4-turbo에서 94.8%, gpt-4o-mini에서 98.2%를 달성하여 MemGPT와 전체 대화 베이스라인 양쪽을 모두 근소하게 앞섰습니다. 전체 결과는 다음 표에 정리되어 있습니다. + +| 메모리 방식 | 모델 | 점수 | +|---|---|---| +| 재귀적 요약† | gpt-4-turbo | 35.3% | +| 대화 요약 | gpt-4-turbo | 78.6% | +| MemGPT† | gpt-4-turbo | 93.4% | +| 전체 대화 | gpt-4-turbo | 94.4% | +| Zep | gpt-4-turbo | 94.8% | +| 대화 요약 | gpt-4o-mini | 88.0% | +| 전체 대화 | gpt-4o-mini | 98.0% | +| Zep | gpt-4o-mini | 98.2% | + +(†는 MemGPT 논문에서 보고된 수치입니다.) + +그러나 논문은 이 결과를 맥락 속에서 해석해야 한다고 강조합니다. 각 대화가 60개의 메시지에 불과해 현재 LLM의 컨텍스트 윈도우 안에 손쉽게 들어가기 때문입니다. 즉 메모리 시스템의 진가가 발휘되어야 할 "맥락이 넘쳐 검색이 필요한 상황"이 애초에 벌어지지 않는 것입니다. 더 나아가 논문은 DMR 벤치마크 설계 자체의 약점을 비판적으로 분석합니다. 첫째, 평가가 오로지 **단일 턴(single-turn)의 사실 검색 질문**에만 의존하여 복잡한 메모리 이해 능력을 측정하지 못합니다. 둘째, "긴장을 풀 때 즐겨 마시는 음료"나 "특이한 취미"처럼 대화에서 그렇게 명시적으로 규정되지 않은 개념을 가리키는 **모호한 표현**이 많습니다. 셋째, 가장 결정적으로 이 데이터셋은 LLM 에이전트의 실제 기업용 사용 사례를 제대로 반영하지 못합니다. 현대 LLM을 쓴 단순 전체 맥락 접근법이 이미 94~98%대의 높은 성능을 내는 **천장 효과(ceiling effect)**는 이 벤치마크가 메모리 시스템을 변별하기에 부적절함을 더욱 부각합니다. 이러한 한계는 대화 길이가 늘어날수록 LLM 성능이 급격히 떨어진다는 LongMemEval의 발견과 대비되며, 자연스럽게 더 까다로운 두 번째 벤치마크로 논의가 이어집니다. + +### LongMemEval (LME) + +LongMemEval은 [Wu 등](https://arxiv.org/pdf/2410.10813v2)이 제안한 벤치마크로, 실제 비즈니스 응용 환경을 더 잘 반영하는 대화와 질문을 제공합니다. 실험에는 평균 약 115,000 토큰에 달하는 긴 대화를 담은 **LongMemEval \(s\)** 데이터셋이 사용되었습니다. 이 길이는 상당히 크지만 여전히 최신 프런티어 모델의 컨텍스트 윈도우 안에 들어가기 때문에, 전체 맥락 베이스라인을 의미 있게 설정하여 Zep과 비교할 수 있게 해 줍니다. 데이터셋은 single-session-user, single-session-assistant, single-session-preference, multi-session, knowledge-update, temporal-reasoning이라는 여섯 가지 질문 유형을 포함하는데, 이들은 데이터셋 전반에 균일하게 분포되어 있지는 않습니다. + +실험은 2024년 12월부터 2025년 1월 사이에 진행되었으며, 보스턴의 일반 가정에서 소비자용 노트북으로 AWS us-west-2에 호스팅된 Zep 서비스에 접속하는 방식으로 수행되었습니다. 이러한 분산 구조 탓에 Zep 평가에는 추가적인 네트워크 지연이 끼어들었지만, 베이스라인 평가에는 이 지연이 없었다는 점을 유의해야 합니다(즉 Zep의 지연 시간은 다소 불리하게 측정된 셈입니다). 답변 평가에는 인간 평가자와 높은 상관관계를 보이는 것으로 입증된 질문별 프롬프트를 사용하여 GPT-4o가 활용되었습니다. + +여기서 흥미로운 대목은, 현재 최고 수준 시스템인 MemGPT와의 직접 비교를 시도했으나 실패했다는 점입니다. MemGPT 프레임워크가 기존 메시지 기록의 직접 수집을 지원하지 않아, 대화 메시지를 아카이브 기록(archival history)에 추가하는 우회책을 구현했지만 성공적인 질문 응답을 얻지 못했습니다. 논문은 다른 연구팀들의 평가를 기대한다고 밝히며 비교 데이터의 부재를 솔직하게 인정합니다. + +#### LongMemEval 결과 + +Zep은 두 모델 변형 모두에서 정확도와 지연 시간 양쪽에 걸쳐 상당한 개선을 보였습니다. gpt-4o-mini에서는 베이스라인 대비 정확도가 15.2%포인트 향상되었고, gpt-4o에서는 18.5%포인트 향상되었습니다. 특히 주목할 부분은 검색을 통해 프롬프트 크기를 극적으로 줄임으로써 지연 시간까지 대폭 절감했다는 점입니다. 다음 표가 이를 종합적으로 보여줍니다. + +| 메모리 방식 | 모델 | 점수 | 지연 시간 | 지연 IQR | 평균 맥락 토큰 | +|---|---|---|---|---|---| +| 전체 맥락 | gpt-4o-mini | 55.4% | 31.3 s | 8.76 s | 115k | +| Zep | gpt-4o-mini | 63.8% | 3.20 s | 1.31 s | 1.6k | +| 전체 맥락 | gpt-4o | 60.2% | 28.9 s | 6.01 s | 115k | +| Zep | gpt-4o | 71.2% | 2.58 s | 0.684 s | 1.6k | + +이 표에서 가장 인상적인 것은 평균 맥락 토큰이 115k에서 1.6k로 줄었다는 사실입니다. 약 70분의 1 수준으로 압축하면서도 정확도는 오히려 높아진 것인데, 이는 지식 그래프 검색이 방대한 대화에서 정말로 필요한 사실만을 정밀하게 길어 올린다는 것을 의미합니다. 토큰 수가 줄어든 만큼 LLM의 추론 시간도 약 90% 단축되어, 30초 안팎이던 응답 시간이 3초 이하로 떨어졌습니다. 정확도와 지연 시간이 보통은 상충 관계에 놓이기 쉬운데, Zep은 두 마리 토끼를 동시에 잡은 셈입니다. + +질문 유형별로 세분하여 분석하면 Zep의 강점과 약점이 더 분명하게 드러납니다. 다음 표는 전체 맥락 베이스라인과 Zep의 유형별 성능과 그 변화율(Delta)을 정리한 것입니다. + +| 질문 유형 | 모델 | 전체 맥락 | Zep | 변화 | +|---|---|---|---|---| +| 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% ↑ | + +이 변화율은 절대 차이가 아니라 베이스라인 대비 상대적 향상률(혹은 하락률)임에 유의해야 합니다. 예컨대 gpt-4o의 single-session-preference에서 20.0%가 56.7%로 오른 것은 상대적으로 184%에 달하는 극적인 증가입니다. 분석을 정리하면, gpt-4o-mini와 Zep의 조합은 여섯 유형 중 네 가지에서 개선을 보였으며, 특히 single-session-preference, multi-session, temporal-reasoning이라는 복잡한 질문 유형에서 가장 큰 폭의 향상이 나타났습니다. 더 강력한 gpt-4o를 쓰면 knowledge-update 유형에서도 추가적인 개선이 관찰되어, Zep이 더 유능한 모델과 결합할 때 그 효과가 한층 커진다는 점이 부각됩니다. 이는 직관적으로도 이해되는데, 시간 정보가 풍부하게 담긴 Zep의 맥락을 제대로 활용하려면 모델 자체의 추론 능력도 뒷받침되어야 하기 때문입니다. 반대로 능력이 떨어지는 모델은 Zep의 시간적 데이터를 충분히 이해하지 못해, 이 부분에는 추가적인 개발이 필요하다고 논문은 인정합니다. + +가장 두드러진 예외는 **single-session-assistant** 유형입니다. 이 유형에서는 gpt-4o가 17.7%, gpt-4o-mini가 9.06% 하락하여, Zep의 전반적으로 일관된 개선 흐름에서 벗어났습니다. single-session-assistant는 어시스턴트가 생성한 발화 내용에 관한 질문인데, 그래프 추출 과정에서 어시스턴트 측 콘텐츠가 충분히 보존·표현되지 못했을 가능성을 시사합니다. 논문은 이 부분이 추가 연구와 엔지니어링이 필요한 영역임을 솔직하게 짚어 둡니다. 종합하면, Zep은 모델 규모 전반에 걸쳐 성능을 끌어올리되 복잡하고 미묘한 질문 유형에서 가장 큰 효과를 발휘하며, 정확도를 높이면서도 응답 시간을 약 90% 줄인다는 점이 이 실험의 핵심 결론입니다. +## 결론 + +지금까지 살펴본 Zep은 의미 기억(semantic memory)과 일화 기억(episodic memory)을 개체 및 커뮤니티 요약과 함께 통합한 그래프 기반 LLM 메모리 접근법입니다. 논문은 평가를 통해 Zep이 기존 메모리 벤치마크에서 최첨단(state-of-the-art) 성능을 달성하면서도 토큰 비용을 절감하고 현저히 낮은 지연 시간으로 동작함을 입증했다고 강조합니다. 다만 저자들은 Graphiti와 Zep이 거둔 성과가 인상적이긴 하나, 그래프 기반 메모리 시스템 연구의 초기 단계(initial advances)에 해당할 가능성이 크다고 명확히 위치 짓습니다. 즉 이 프레임워크 위에서 다른 GraphRAG 접근법을 Zep의 패러다임에 통합하거나 본 연구를 새롭게 확장하는 등 다양한 후속 연구의 여지가 남아 있다는 것입니다. + +### 미세 조정된 추출 모델의 가능성 + +첫 번째 향후 연구 방향은 개체와 엣지 추출을 담당하는 LLM을 미세 조정(fine-tuning)하는 것입니다. 앞서 지식 그래프 구축 부분에서 살펴보았듯, Zep은 개체 추출·해소와 사실 추출을 모두 범용 LLM(gpt-4o-mini)에 의존했습니다. 그런데 논문은 GraphRAG 패러다임 안에서 개체·엣지 추출에 특화되도록 미세 조정된 모델이 이미 그 가치를 입증했음을 지적합니다. 구체적으로 Triplex와 Distill-SynthKG에 해당하는 선행 연구들은 미세 조정을 통해 추출 정확도를 높이는 동시에 비용과 지연 시간을 줄일 수 있음을 보여 주었습니다. 같은 맥락에서, Graphiti의 프롬프트(부록에 정리된 개체 추출·해소·사실 추출·사실 해소·시간 추출 프롬프트들)에 맞추어 미세 조정된 모델을 도입한다면, 특히 복잡한 대화에서 지식 추출의 품질이 한층 향상될 수 있다는 것입니다. + +### 도메인 특화 온톨로지의 잠재력 + +두 번째 방향은 온톨로지(ontology)의 활용입니다. 현재 LLM이 생성하는 지식 그래프에 관한 연구는 대체로 형식적 온톨로지 없이(without formal ontologies) 작동해 왔습니다. 온톨로지란 특정 도메인에서 개체와 관계의 종류, 그리고 그들 사이에 허용되는 구조를 미리 정의한 개념적 스키마를 뜻하는데, LLM 이전 시대의 지식 그래프 연구에서는 이 그래프 온톨로지가 토대를 이루는 핵심 요소였습니다. 논문은 도메인 특화 온톨로지가 상당한 잠재력을 지니며, Graphiti 프레임워크 안에서 이를 재탐구할 가치가 충분하다고 전망합니다. 즉 LLM의 유연한 추출 능력과 전통적 온톨로지의 구조적 엄밀함을 결합하는 것이 흥미로운 연구 과제로 남아 있다는 것입니다. + +### 메모리 벤치마크의 필요성 + +세 번째이자 논문이 비중 있게 다루는 방향은 더 나은 평가 벤치마크의 필요성입니다. 저자들이 적합한 메모리 벤치마크를 탐색하는 과정에서 발견한 것은, 선택지가 매우 제한적이며 기존 벤치마크가 견고성과 복잡성을 결여한 채 단순한 "건초 더미에서 바늘 찾기(needle-in-a-haystack)" 식의 사실 검색 질문으로 귀결되는 경우가 많다는 점이었습니다. 이는 앞서 실험 부분에서 DMR 벤치마크의 천장 효과와 단일 턴 사실 검색의 한계를 비판적으로 분석했던 맥락과 그대로 이어집니다. 따라서 논문은 메모리 접근법들을 효과적으로 평가하고 변별하기 위해, 특히 고객 경험(customer experience) 과제와 같은 비즈니스 응용을 반영하는 추가적인 메모리 벤치마크가 필요하다고 강조합니다. 더욱이 저자들은 Zep이 대화 기록과 정형 비즈니스 데이터(structured business data)를 함께 처리하고 종합하는 능력을 적절히 평가할 수 있는 기존 벤치마크가 전무하다는 점을 지적합니다. Zep의 설계가 비정형 메시지와 정형 데이터를 모두 수집하도록 의도되었음에도, 이 통합적 능력을 검증할 평가 체계 자체가 아직 존재하지 않는다는 것입니다. + +### 확장성 평가 및 향후 과제 + +마지막으로 논문은 Zep의 전통적 RAG 능력과 생산 시스템으로서의 확장성을 함께 짚습니다. Zep은 LLM 메모리에 초점을 두고 있지만, 그 기반에는 전통적인 검색 증강 생성 기능도 자리하고 있으므로, 이 능력은 정립된 RAG 벤치마크들에 대해 별도로 평가될 필요가 있다고 권고합니다. 한편 저자들은 현재의 LLM 메모리 및 RAG 시스템 관련 문헌이 비용과 지연 시간 측면에서의 생산 시스템 확장성(production system scalability)을 충분히 다루지 못하고 있다는 점을 비판적으로 지적합니다. 실제로 많은 연구가 정확도 지표에 집중하면서, 실제 배포 환경에서 결정적으로 중요한 비용과 응답 속도는 상대적으로 소홀히 다루어져 온 것입니다. Zep은 이러한 공백을 메우기 위한 첫걸음으로 검색 메커니즘에 대한 지연 시간 벤치마크를 포함시켰으며, 이는 이러한 운영 지표를 우선시한 LightRAG 저자들의 선례를 따른 것입니다. 결국 이 결론은 Zep이 정확도, 비용, 지연 시간을 균형 있게 고려하는 실용적 메모리 시스템임을 다시 한번 강조하는 동시에, 그래프 기반 에이전트 메모리라는 분야가 이제 막 열리기 시작한 풍부한 연구 지평임을 시사하며 마무리됩니다. +## 부록: 그래프 구축 프롬프트 + +지금까지 살펴본 Zep의 지식 그래프 구축 파이프라인—개체 추출과 해소, 사실 추출과 중복 제거, 시간 정보 추출과 엣지 무효화—은 모두 LLM에게 전달되는 구체적인 프롬프트(prompt)에 의해 실제로 작동합니다. 부록은 바로 이 다섯 단계의 프롬프트 템플릿 전문을 공개하여, 앞서 형식적으로 정의했던 추출 절차가 실제로 LLM에게 어떤 언어적 지시로 구현되는지를 투명하게 드러냅니다. 이 프롬프트들은 추상적 알고리즘과 실제 동작 사이의 다리를 놓아 주므로, 그 설계 의도를 하나씩 짚어 보는 것은 시스템 전체를 이해하는 데 큰 도움이 됩니다. + +### 개체 추출 프롬프트 + +개체 추출 프롬프트는 이전 메시지들(`{previous_messages}`)을 맥락으로 제공하고, 현재 메시지(`{current_message}`)로부터 명시적 혹은 암묵적으로 언급된 개체 노드를 추출하도록 LLM에게 지시합니다. 이 프롬프트의 가장 흥미로운 설계는 여섯 가지 지침(guideline)에 압축되어 있습니다. 첫째 지침은 **발화자/행위자(speaker/actor)를 언제나 첫 번째 노드로 추출**하라는 것인데, 발화자는 각 대화 줄에서 콜론(`:`) 앞부분으로 식별됩니다. 앞서 메시지 처리에서 발화자가 자동으로 개체로 추출된다고 설명했던 부분이 바로 이 지침으로 구현되는 셈입니다. + +나머지 지침들은 무엇을 추출하지 **말아야** 하는지를 명확히 규정함으로써 추출 품질을 통제합니다. 관계나 행동은 노드로 만들지 말 것(셋째), 날짜·시간·연도 같은 시간 정보는 노드로 만들지 말 것(넷째, 이들은 이후 엣지에 부착됨), 그리고 가능한 한 전체 이름(full name)을 써서 노드 이름을 명시적으로 지을 것(다섯째)이 핵심입니다. 시간 정보를 노드가 아닌 엣지에 귀속시킨다는 원칙은 앞서 살펴본 이중 시간 모델과 정확히 맞물립니다. 시간은 "어떤 개체"가 아니라 "어떤 관계가 언제 참이었는가"의 속성이기 때문입니다. + +실제 Graphiti 구현에서는 이 추출 원칙이 한층 더 정교하게 발전되어 있음을 코드에서 확인할 수 있습니다. 결합형 노드·엣지 추출 프롬프트는 추출되어서는 안 되는 항목들을 매우 구체적으로 열거합니다. + +```python +# graphiti_core/prompts/extract_nodes_and_edges.py 의 ENTITY RULES 발췌 +# 6. 개체 이름은 사용자의 세계에 존재하는 '구체적 사물'을 가리키는 +# 명사구(NOUN PHRASE)여야 한다. 다음은 추출 금지: +# a. 대명사·관사·미해소 지시어: "the city", "this issue" → 실제 지시 대상으로 +# 해소하거나 건너뜀 +# b. 모호한 추상명사·군더더기 명사: balance, growth, motivation, day, life +# c. 구체적 시각·날짜를 개체로: "5:54 am", "January 15" → 사건의 속성이므로 +# fact 텍스트 안에 보존 +# d. 단순 수량·측정값·기간·가격: "7-8 hours", "$150/hour", "10-pound dumbbells" +# e. 지리 좌표·위도·경도 +# f. 객관식 답안 레이블이나 프롬프트 템플릿 토큰 +# g. 명령형 조언 구절("Buy in bulk") → 명사형 주제("bulk buying")로 전환 +``` + +이렇게 금지 항목을 길게 열거하는 이유는 직관적으로 분명합니다. LLM은 방치하면 "균형(balance)"이나 "동기부여(motivation)" 같은 추상명사, 혹은 "7-8시간" 같은 수치까지 개체로 만들어 버리는 경향이 있는데, 이런 항목들은 그래프의 노드로서 검색에 활용될 만한 **구체적 지시 대상**이 되지 못합니다. 오히려 그래프를 잡음으로 가득 채워 검색 정밀도를 떨어뜨립니다. 따라서 시각·날짜·수량 같은 정보는 노드가 아니라 사실(fact) 텍스트 안에 속성으로 보존하라는 원칙이 일관되게 관철되는 것입니다. + +### 개체 해소 프롬프트 + +개체 해소 프롬프트는 기존 노드 목록(`{existing_nodes}`), 메시지, 그리고 새로 추출된 노드(`{new_node}`)를 입력으로 받아, 새 노드가 기존 노드 중 하나와 **동일한 개체의 중복(duplicate)**인지를 판별합니다. 앞서 개체 해소를 "새로 추출한 앨리스가 이미 그래프에 있던 앨리스와 동일 인물인지 판별하는 과정"이라 설명했는데, 이 프롬프트가 바로 그 판별을 수행하는 장치입니다. + +이 프롬프트가 LLM에게 요구하는 출력은 세 가지입니다. 중복 여부를 나타내는 `is_duplicate` 불리언 값, 중복일 경우 기존 노드의 `uuid`, 그리고 중복일 경우 **가장 완전한 전체 이름**을 새 노드 이름으로 반환하는 것입니다. 특히 주목할 지침은 "노드의 이름과 요약(summary)을 **모두** 사용하여 중복 여부를 판단하라. 중복 노드는 서로 다른 이름을 가질 수 있다"는 부분입니다. 이는 앞서 개체 추출 단계에서 각 개체에 대해 요약을 함께 추출했던 설계가 왜 필요했는지를 명확히 보여 줍니다. 예컨대 "앨리스"와 "앨리스 김"은 이름만으로는 동일 인물인지 불확실하지만, 각자의 요약에 담긴 맥락 정보(직업, 소속, 관계 등)를 대조하면 동일성을 한층 신뢰도 높게 판단할 수 있기 때문입니다. 여기서 BGE-M3 같은 다기능 임베딩 모델이 의미적 유사도와 어휘적 유사도를 함께 포착해 후보를 좁혀 주고, 그렇게 모은 후보에 대해 이 프롬프트가 최종 판단을 내리는 분업 구조가 형성됩니다. + +### 사실 추출 프롬프트 + +사실 추출 프롬프트는 메시지들과 함께 이미 추출된 개체 목록(`{entities}`)을 받아, 현재 메시지로부터 그 개체들에 관련된 모든 사실을 추출합니다. 다섯 가지 지침의 핵심은 사실이 갖추어야 할 구조적 요건을 규정하는 데 있습니다. 사실은 반드시 제공된 개체들 **사이에서만** 추출되어야 하고(첫째), 각 사실은 **서로 구별되는 두 노드 사이의 명확한 관계**를 나타내야 합니다(둘째). 그리고 관계 유형(`relation_type`)은 `LOVES`, `IS_FRIENDS_WITH`, `WORKS_FOR`처럼 간결한 대문자 표기로 기술하되(셋째), 모든 관련 정보를 담은 더 상세한 사실 서술을 별도로 제공하며(넷째), 관계의 시간적 측면을 고려하라(다섯째)는 것입니다. + +실제 구현에서 이 사실 추출은 Pydantic 스키마로 엄밀하게 정형화되어 있어, LLM의 출력이 일관된 구조를 갖도록 강제합니다. + +```python +# graphiti_core/prompts/extract_edges.py 의 Edge 스키마 +class Edge(BaseModel): + source_entity_name: str # ENTITIES 목록에 있는 출발 개체 이름 + target_entity_name: str # ENTITIES 목록에 있는 도착 개체 이름 + relation_type: str # SCREAMING_SNAKE_CASE (예: WORKS_AT, LIVES_IN) + fact: str # 원문을 의역한 자연어 관계 서술 + valid_at: str | None # 관계가 참이 된 시점 (ISO 8601) + invalid_at: str | None # 관계가 거짓이 된 시점 (ISO 8601) + episode_indices: list[int] # 이 사실이 도출된 에피소드 번호 목록 +``` + +이 스키마에서 가장 중요한 추출 규칙은 `source_entity_name`과 `target_entity_name`이 반드시 제공된 ENTITIES 목록의 이름만 사용해야 한다는 점입니다. 구현 코드는 "목록에 없는 이름을 쓰면 해당 엣지가 거부된다(rejected)"고 명시하며, 실제로 후처리 단계에서 출발·도착 개체가 추출된 노드 집합에 존재하는지를 대소문자를 무시하고 검증한 뒤, 어느 한쪽이라도 없으면 그 엣지를 건너뜁니다. 이는 LLM이 환각으로 만들어낸 존재하지 않는 개체를 잇는 거짓 엣지가 그래프에 유입되는 것을 원천 차단하는 안전장치입니다. 또한 구현 프롬프트는 "Alice feels happy"처럼 모호한 단일 개체 상태는 나쁜 예로, "Alice feels happy about Bob's promotion"처럼 두 개체를 잇는 구체적 사실은 좋은 예로 제시하면서, 게임기 모델명이나 브랜드명 같은 **구체적 세부 사항을 일반화하지 말 것**을 강하게 요구합니다. "Gamecube"를 "게임 콘솔"로, "Ford Mustang"을 "자동차"로 뭉뚱그리지 말라는 것입니다. 이는 검색 시점에 원본 대화가 더 이상 존재하지 않고 오직 추출된 사실만 남기 때문에, 사실 텍스트 자체가 최대한의 구체성을 보존해야 한다는 통찰에 기반합니다. + +### 사실 해소 프롬프트 + +사실 해소 프롬프트는 새 엣지(`{new_edge}`)가 기존 엣지 목록(`{existing_edges}`) 중 어느 것과 동일한 사실 정보를 나타내는지를 판별합니다. 개체 해소와 마찬가지로 중복 여부(`is_duplicate`)와 중복일 경우 기존 엣지의 `uuid`를 반환하도록 요구합니다. 이 프롬프트의 단 하나뿐인 지침이 핵심을 찌릅니다. "사실들이 완전히 동일할 필요는 없으며, **같은 정보를 표현하기만 하면** 중복으로 본다"는 것입니다. 즉 표면적인 문구가 달라도 의미가 같으면 중복으로 통합합니다. 앞서 사실 중복 제거가 동일 개체 쌍으로 검색을 한정한다고 설명했던 제약이 여기에 결합되어, "앨리스—근무한다—회사 X"라는 같은 개체 쌍 안에서 표현만 다른 중복을 효율적으로 합치게 됩니다. + +### 시간 추출 프롬프트 + +마지막 시간 추출 프롬프트는 다섯 프롬프트 중 가장 정교하며, 앞서 상세히 다룬 이중 시간 모델의 사건 타임라인 타임스탬프를 실제로 산출하는 장치입니다. 이 프롬프트는 이전·현재 메시지와 함께 참조 타임스탬프(`{reference_timestamp}`)와 대상 사실(`{fact}`)을 받아, 그 사실에 관한 시간 정보를 추출합니다. 가장 중요한 전제는 프롬프트 첫머리에 대문자로 강조된 "**오직 제공된 사실의 일부인 시간 정보만 추출하라. 그 외에 언급된 시간은 무시하라**"는 원칙입니다. 이는 관련 없는 사건의 날짜를 사실에 잘못 결합시키는 환각을 방지하기 위한 핵심 안전장치입니다. + +이 프롬프트는 두 개의 출력 필드를 정의합니다. `valid_at`은 엣지가 기술하는 관계가 참이 되거나 성립된 시점이고, `invalid_at`은 그 관계가 참이기를 멈추거나 종료된 시점입니다. 앞서 사건 타임라인 \\(T\\)에 속하는 \\(t_{\text{valid}}\\)와 \\(t_{\text{invalid}}\\)로 정의했던 두 타임스탬프가 바로 이 두 필드로 구현되는 것입니다. 아홉 가지에 이르는 지침은 매우 실용적인 시간 처리 규칙들을 담고 있는데, 핵심을 정리하면 다음과 같습니다. 모든 날짜·시각은 ISO 8601 형식(`YYYY-MM-DDTHH:MM:SS.SSSSSSZ`)으로 표기하고, 참조 타임스탬프를 현재 시각으로 삼아 상대적 시간을 계산하며, 사실이 현재 시제로 쓰였으면 `valid_at`을 참조 타임스탬프로 설정합니다. 또한 관계의 성립이나 변화를 확립하는 시간 정보가 없으면 필드를 null로 두고, **관련 사건으로부터 날짜를 추론하지 말 것**이며, 시각 없이 날짜만 언급되면 자정(00:00:00)을, 연도만 언급되면 1월 1일 자정을 사용하고, 시간대가 명시되지 않으면 UTC(`Z`)를 붙입니다. + +이 규칙들이 어떻게 코드로 구현되는지는 Graphiti의 시간 추출 프롬프트 함수에서 간결하게 드러납니다. + +```python +# graphiti_core/prompts/extract_edges.py 의 extract_timestamps +# 시스템 메시지: "사실로부터 시간 경계를 추출하라. 절대 날짜를 환각하지 말 것." +"""Given a FACT and its REFERENCE TIME, determine when the fact became true +(valid_at) and when it stopped being true (invalid_at). + +Rules: +- "last week", "2 years ago", "yesterday" 같은 상대 표현은 REFERENCE TIME으로 해소 +- 사실이 현재 시제(진행 중)면 valid_at을 REFERENCE TIME으로 설정 +- 변화나 종료가 표현되면 invalid_at을 해당 시점으로 설정 +- 시간이 언급되지 않거나 해소 불가능하면 둘 다 null +- 날짜만 있고 시각이 없으면 00:00:00으로 가정 +- ISO 8601에 Z 접미사 사용 (예: 2025-04-30T00:00:00Z) +- 관련 없는 사건으로부터 날짜를 추론하거나 환각하지 말 것 +""" +``` + +특히 흥미로운 구현 세부는, 실제 시스템이 여러 사실의 타임스탬프를 **하나의 배치(batch) LLM 호출**로 한꺼번에 추출한다는 점입니다. `extract_timestamps_batch` 함수는 여러 사실과 각각의 참조 시각을 함께 전달하여, 입력 순서와 동일한 순서로 타임스탬프 목록을 반환받습니다. 후처리 코드는 반환된 타임스탬프 개수가 엣지 개수와 일치하는지를 검증하고, `Z` 접미사를 `+00:00`으로 치환한 뒤 ISO 형식을 파싱하여 각 엣지의 `valid_at`·`invalid_at` 필드에 UTC로 정규화해 채워 넣습니다. 이렇게 개별 호출 대신 배치 호출을 사용하는 것은 LLM 추론 횟수를 줄여 비용과 지연 시간을 절감하려는, 앞서 여러 차례 확인했던 Zep의 일관된 효율성 철학이 프롬프트 수준에서도 관철되는 사례입니다. + +다섯 개의 프롬프트를 종합해 보면, Zep의 그래프 구축은 "유연성이 필요한 추출과 판단은 LLM에게, 정확성이 필요한 검증과 삽입은 결정론적 코드에게"라는 역할 분담을 프롬프트 설계 곳곳에서 구체화하고 있음을 알 수 있습니다. 각 프롬프트는 무엇을 추출할지뿐 아니라 무엇을 **추출하지 말지**, 그리고 어떤 형식으로 출력할지를 엄격히 규정함으로써, 범용 LLM을 신뢰할 수 있는 지식 그래프 구축 엔진으로 길들이는 역할을 수행합니다. 이는 앞서 결론에서 향후 과제로 제시되었던 "개체·엣지 추출에 특화된 미세 조정 모델"이 바로 이 부록의 프롬프트들을 학습 신호로 삼아 더욱 정확하고 저렴하게 동일한 작업을 수행할 수 있으리라는 전망과도 자연스럽게 연결됩니다. +- - - +### References +* [Zep: A Temporal Knowledge Graph Architecture for Agent Memory](https://arxiv.org/pdf/2501.13956v1) +* [getzep/graphiti](https://github.com/getzep/graphiti) \ No newline at end of file From 9d083ed52471c5c6f6f7e346b4880530782c27a6 Mon Sep 17 00:00:00 2001 From: bits-bytes-nn Date: Sun, 7 Jun 2026 23:14:32 +0900 Subject: [PATCH 2/2] chore(zep): rename to publish date, set RAG category and cover --- ...mporal-knowledge-graph-architecture-for-agent-memory.md} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename _posts/{2025-01-20-zep-a-temporal-knowledge-graph-architecture-for-agent-memory.md => 2026-06-07-zep-a-temporal-knowledge-graph-architecture-for-agent-memory.md} (99%) diff --git a/_posts/2025-01-20-zep-a-temporal-knowledge-graph-architecture-for-agent-memory.md b/_posts/2026-06-07-zep-a-temporal-knowledge-graph-architecture-for-agent-memory.md similarity index 99% rename from _posts/2025-01-20-zep-a-temporal-knowledge-graph-architecture-for-agent-memory.md rename to _posts/2026-06-07-zep-a-temporal-knowledge-graph-architecture-for-agent-memory.md index e3568f7..154f1ca 100644 --- a/_posts/2025-01-20-zep-a-temporal-knowledge-graph-architecture-for-agent-memory.md +++ b/_posts/2026-06-07-zep-a-temporal-knowledge-graph-architecture-for-agent-memory.md @@ -3,9 +3,9 @@ layout: post title: "Zep: A Temporal Knowledge Graph Architecture for Agent Memory" date: 2025-01-20 16:52:48 author: "Preston Rasmussen et al." -categories: ["Paper Reviews", "Retrieval-Augmented-Generation-(incl.-knowledge-graphs-&-ontologies)"] +categories: ["Paper Reviews", "Retrieval-Augmented-Generation"] tags: ["Temporally-Aware-Knowledge-Graph-Engine", "Bi-Temporal-Knowledge-Graph-Modeling", "Hierarchical-Knowledge-Graph-Construction", "Dynamic-Edge-Invalidation-for-Temporal-Reasoning", "Episodic-and-Semantic-Memory-Subgraphs", "Community-Detection-with-Label-Propagation", "Hybrid-Search-with-Breadth-First-Graph-Traversal", "Non-Lossy-Knowledge-Graph-Updates", "Multi-Hop-Entity-and-Relationship-Extraction", "Graph-Based-Memory-Retrieval-for-LLM-Agents"] -cover: /assets/images/default.jpg +cover: /assets/images/retrieval-augmented-generation.jpg use_math: true --- ### TL;DR @@ -658,4 +658,4 @@ Rules: - - - ### References * [Zep: A Temporal Knowledge Graph Architecture for Agent Memory](https://arxiv.org/pdf/2501.13956v1) -* [getzep/graphiti](https://github.com/getzep/graphiti) \ No newline at end of file +* [getzep/graphiti](https://github.com/getzep/graphiti)