
LLM 기반 챗봇을 사용하다 보면 이런 경험을 할 때가 있다.
틀린 답은 아닌데, 정작 내가 알고 싶은 핵심을 놓친 답변.
왜 이런 일이 발생할까?
대표적인 원인 중 하나가 정보 자체는 검색했지만, 정보들 사이의 관계를 제대로 연결하지 못했기 때문이다.
이 문제를 해결하기 위해 등장한 접근 방법 중 하나가 GraphRAG 다.
GraphRAG는 단순히 관련 문서를 찾아 LLM에게 전달하는 것을 넘어, 사람·회사·장소·약물·사건 같은 개체와 이들 사이의 관계를 그래프로 구성하고 검색에 활용한다.
예를 들어 인도 델리 여행을 준비하면서 AI에게 다음과 같이 질문했다고 해보자.
델리에서 꼭 가봐야 할 관광지는 어디이고, 효율적으로 이동하려면 어떻게 해야 할까?
일반적인 RAG 시스템은 관광지나 교통수단에 대한 정보를 꽤 잘 찾아낼 수 있다.
하지만 실제 여행 계획에서 더 중요한 것은 관광지 목록 자체가 아니라 관광지 사이의 관계다.
Red Fort
│
├─ 가까움 ─ Jama Masjid
│
└─ 가까움 ─ Chandni Chowk
즉 실제로 필요한 것은 단순한 장소 정보가 아니다.
장소
+
거리
+
지역
+
교통
+
시간
+
혼잡도
+
장소 사이의 관계
같은 맥락이 함께 필요하다.
기본적인 RAG는 문서를 일정 크기의 Chunk로 나눈다.
Document
↓
Chunk
↓
Embedding
↓
Vector DB
사용자가 질문하면 질문 역시 임베딩으로 변환하고 Vector DB에서 의미적으로 가까운 문서를 검색한다.
Question
↓
Embedding
↓
Vector Search
↓
Relevant Chunks
↓
LLM
↓
Answer
이 방법은
회사 연차 규정은 어떻게 되나요?
처럼 특정 문서에서 답을 찾는 질문에는 매우 효과적이다.
하지만 다음과 같이 여러 관계를 연결해야 하는 질문에서는 난도가 올라간다.
프로젝트
→ 시스템
→ 장애
→ 담당 팀
→ 담당자
이때 GraphRAG가 활용될 수 있다.
GraphRAG는 RAG 검색 과정에서 Graph 구조를 활용하는 접근법이다.
일반 RAG의 핵심 질문이
이 질문과 가장 비슷한 문서는 무엇인가?
라면 GraphRAG에서는 다음 질문까지 함께 고려할 수 있다.
질문에 등장하는 개체는 무엇인가?
이 개체와 연결된 개체는 무엇인가?
두 개체는 어떤 관계인가?
A와 C 사이에 어떤 연결 경로가 존재하는가?
결국 검색 범위가
Text
에서
Text
+
Entity
+
Relationship
+
Graph Structure
로 확장되는 것이다.
Neo4j 역시 GraphRAG를 단일 구현 기법이 아니라 그래프 구조를 Retrieval에 활용하는 RAG 아키텍처 패턴으로 설명하고 있다.
GraphRAG를 이해하려면 먼저 Knowledge Graph, 지식 그래프를 알아야 한다.
지식 그래프는 크게
형태로 데이터를 표현한다.
예를 들어 다음 정보가 있다고 해보자.
Steve Jobs founded Apple.
Apple competes with Microsoft.
Bill Gates co-founded Microsoft.
이를 그래프로 표현하면 다음과 같다.
Steve Jobs
│
FOUNDED
↓
Apple
│
COMPETES_WITH
↓
Microsoft
↑
CO_FOUNDED
│
Bill Gates
이 구조에서는 단순히 Apple이라는 단어가 존재하는 것만 알 수 있는 것이 아니다.
Apple과 Microsoft가 경쟁 관계이고, Microsoft와 Bill Gates가 창업자 관계라는 사실까지 표현할 수 있다.
Knowledge Graph에서는 관계를 흔히 다음과 같은 Triple 형태로 표현한다.
Subject - Predicate - Object
예를 들면,
Einstein - BORN_IN - Ulm
Einstein - DEVELOPED - Theory of Relativity
Newton - DISCOVERED - Law of Universal Gravitation
이 Triple이 쌓이면서 거대한 그래프가 만들어진다.
과거에는 문서에서 Entity와 Relationship을 추출하는 작업 자체가 상당한 비용이 들었다.
하지만 LLM은 자연어에서
Entity
Relationship
Property
를 구조화된 형태로 추출하는 데 활용할 수 있다.
예를 들어,
Albert Einstein developed the theory of relativity.
He was born in Ulm.
이라는 문서가 있다면 LLM을 이용해 다음과 같이 변환할 수 있다.
Albert Einstein
│
DEVELOPED
↓
Theory of Relativity
Albert Einstein
│
BORN_IN
↓
Ulm
LangChain의 LLMGraphTransformer 역시 이런 방식으로 비정형 텍스트에서 Node와 Relationship을 추출해 GraphDocument 형태로 변환할 수 있다.
전체적인 구조를 단순화하면 다음과 같다.
[원본 문서]
│
▼
Chunking
│
┌────────┴────────┐
│ │
▼ ▼
Embedding Entity 추출
│ │
▼ ▼
Vector DB Relationship 추출
│
▼
Knowledge Graph
│
▼
Graph DB
│
┌─────────────────┴──────────────┐
▼ ▼
Vector Search Graph Search
│ │
└──────────────┬─────────────────┘
▼
Context
│
▼
LLM
│
▼
Answer
다만 GraphRAG라고 해서 항상 이 구조를 전부 사용해야 하는 것은 아니다.
Graph Search만 사용하는 방식도 있고,
Natural Language
↓
Cypher
↓
Neo4j
Vector Search와 Graph Search를 결합하는 방식도 있다.
GraphRAG의 중요한 특징 중 하나가 관계를 여러 단계 따라갈 수 있다는 것이다.
예를 들어,
Apple
↓
COMPETES_WITH
↓
Microsoft
↓
CO_FOUNDED_BY
↓
Bill Gates
라는 그래프가 존재한다고 하자.
사용자가
Apple과 경쟁하는 회사를 설립한 사람은 누구인가?
라고 질문하면 하나의 문장만 검색하는 것이 아니라
Apple
→ Microsoft
→ Bill Gates
라는 관계를 따라갈 수 있다.
이처럼 여러 관계를 거치는 질문을 Multi-hop Query라고 볼 수 있다.
| 구분 | 일반 RAG | GraphRAG |
|---|---|---|
| 주요 검색 대상 | Text Chunk | Entity + Relationship + Text |
| 대표 저장소 | Vector DB | Graph DB 또는 Graph + Vector |
| 주요 검색 | 의미 유사도 | 관계 탐색 + 의미 검색 |
| 단순 문서 검색 | 강함 | 가능 |
| 복잡한 관계 질문 | 상대적으로 제한적 | 강점 |
| Multi-hop | 제한적 | 적합 |
| 구축 난이도 | 비교적 낮음 | 높음 |
| 운영 복잡도 | 낮음 | 상대적으로 높음 |
GraphRAG가 무조건 기존 RAG의 상위 호환이라고 보는 것은 맞지 않는다.
단순 FAQ나 매뉴얼 검색이라면 Vector RAG가 훨씬 단순하고 효율적일 수 있다.
GraphRAG는 관계가 중요한 데이터에서 특히 활용 가치가 높다.
Patient
├─ Diagnosis
├─ Medication
├─ Allergy
├─ Laboratory Result
└─ Procedure
예를 들어,
특정 질환을 가진 환자가 Drug A를 복용하고 있을 때 Drug B를 추가하면 어떤 문제가 있을까?
라는 질문에서는 질환, 약물, 부작용, 상호작용 등의 관계를 연결해야 한다.
Person
↓
Company
↓
Account
↓
Transaction
↓
Another Company
각각의 거래만 보면 정상처럼 보이더라도 연결 관계를 따라가면 자금 세탁이나 사기 네트워크가 드러날 수 있다.
Customer
↓
Project
↓
System
↓
Incident
↓
Engineer
고객, 프로젝트, 시스템, 장애, 담당자를 연결하여 검색할 수 있다.
Customer
↓
Ticket
↓
Product
↓
Version
↓
Error
여러 상담 티켓을 연결하여 특정 제품 버전에서 반복되는 문제를 찾을 수도 있다.
이제 개념에서 한 단계 더 나아가 직접 간단한 GraphRAG를 만들어보자.
이번 실습의 목표는 다음과 같다.
텍스트 입력
↓
LLM
↓
Entity / Relationship 추출
↓
Neo4j 저장
↓
자연어 질문
↓
Cypher 생성
↓
그래프 검색
↓
LLM 답변
이번 예제는 Neo4j + LangChain 기반 NL2Cypher GraphRAG의 가장 단순한 형태다.
즉 아직 Vector Search까지 결합한 Hybrid RAG는 아니다.
현재 Neo4j는 LangChain 연동을 위한 langchain-neo4j 패키지를 제공하고 있다.
Knowledge Graph 생성에 사용하는 LLMGraphTransformer는 langchain-experimental 패키지에 포함된다.
pip install -U \
langchain \
langchain-openai \
langchain-neo4j \
langchain-experimental \
langchain-text-splitters \
neo4j \
python-dotenv
기존 예제 중에는 다음처럼 사용하는 코드가 많다.
from langchain_community.graphs import Neo4jGraph
현재 Neo4j 공식 통합에서는 다음 패키지를 사용하는 방법이 제공된다.
from langchain_neo4j import Neo4jGraph
GraphCypherQAChain 역시 다음처럼 가져올 수 있다.
from langchain_neo4j import GraphCypherQAChain
가장 간단한 방법은 두 가지다.
1. Neo4j Aura
2. Local Neo4j
Neo4j의 공식 GraphRAG Python 패키지는 현재 Neo4j 5.18.1 이상 및 Aura 5.18.0 이상을 지원한다고 명시하고 있다.
로컬에서는 Docker를 사용할 수도 있다.
docker run \
--name neo4j-graphrag \
-p 7474:7474 \
-p 7687:7687 \
-e NEO4J_AUTH=neo4j/password1234 \
neo4j:latest
실행한 다음 브라우저에서 Neo4j Browser로 그래프를 확인할 수 있다.
API Key와 DB 비밀번호를 코드에 직접 넣는 방식은 피하는 것이 좋다.
프로젝트 루트에 .env를 만든다.
OPENAI_API_KEY=your-openai-api-key
NEO4J_URI=neo4j://localhost:7687
NEO4J_USERNAME=neo4j
NEO4J_PASSWORD=password1234
그리고 Python에서 읽는다.
import os
from dotenv import load_dotenv
load_dotenv()
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
NEO4J_URI = os.getenv("NEO4J_URI")
NEO4J_USERNAME = os.getenv("NEO4J_USERNAME")
NEO4J_PASSWORD = os.getenv("NEO4J_PASSWORD")
.env 파일은 Git에 올라가지 않도록 .gitignore에 추가한다.
.env
필요한 클래스를 가져온다.
import os
from dotenv import load_dotenv
from langchain_core.documents import Document
from langchain_experimental.graph_transformers import LLMGraphTransformer
from langchain_openai import ChatOpenAI
from langchain_neo4j import Neo4jGraph, GraphCypherQAChain
load_dotenv()
graph = Neo4jGraph(
url=os.getenv("NEO4J_URI"),
username=os.getenv("NEO4J_USERNAME"),
password=os.getenv("NEO4J_PASSWORD")
)
구조는 단순하다.
Python
↓ Bolt
Neo4j
Graph 추출과 Cypher 생성에 사용할 LLM을 만든다.
llm = ChatOpenAI(
model="gpt-4.1-mini",
temperature=0
)
지식 그래프 생성에서는 같은 입력에 대해 가능한 한 일관된 구조를 만드는 것이 중요하기 때문에 일반적으로 창의적인 생성보다는 구조화된 추출이 중요하다.
먼저 아주 작은 문서로 실습해보자.
sample_text = """
알버트 아인슈타인은 1879년 독일 울름에서 태어난 물리학자이다.
아인슈타인은 상대성 이론을 발전시켰다.
1921년 광전효과 연구에 대한 공로로 노벨 물리학상을 받았다.
아이작 뉴턴은 잉글랜드 출신의 물리학자이자 수학자이다.
뉴턴은 만유인력의 법칙을 정립했다.
아인슈타인과 뉴턴은 물리학 발전에 중요한 영향을 끼쳤다.
"""
documents = [
Document(page_content=sample_text)
]
이번 샘플은 문서가 짧기 때문에 Chunking 과정을 생략한다.
실제 PDF나 보고서를 대상으로 한다면 문서를 적절한 크기로 분할하는 과정이 필요하다.
예를 들어 다음처럼 나눌 수 있다.
from langchain_text_splitters import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=150
)
documents = text_splitter.create_documents([sample_text])
전체 구조는 다음과 같다.
PDF / Wiki / Document
↓
Text Loader
↓
Chunking
↓
LLMGraphTransformer
문서가 커질수록 Chunk 크기와 Overlap 설정은 Entity와 Relationship 추출 결과에도 영향을 줄 수 있다.
LangChain의 LLMGraphTransformer를 사용한다.
llm_transformer = LLMGraphTransformer(llm=llm)
graph_documents = llm_transformer.convert_to_graph_documents(
documents
)
이 과정에서 LLM이 문서를 분석해서
Node
Relationship
Property
형태로 변환한다.
개념적으로 다음과 같은 결과를 기대할 수 있다.
Albert Einstein
│
BORN_IN
↓
Ulm
Albert Einstein
│
DEVELOPED
↓
Theory of Relativity
Neo4j 공식 예제에서도 LLMGraphTransformer를 이용해 자연어에서 Node와 Relationship을 추출하여 Knowledge Graph를 만드는 방식을 제공한다.
기본 설정에서는 LLM이 자유롭게 Node와 Relationship을 추출한다.
실습에서는 괜찮지만 실제 서비스에서는 그래프 구조가 엉망이 될 수 있다.
예를 들어 동일한 의미가 다음처럼 만들어질 수 있다.
BORN_IN
BIRTH_PLACE
BORN_AT
PLACE_OF_BIRTH
그래서 실제 시스템에서는 Schema를 제한하는 것이 좋다.
llm_transformer = LLMGraphTransformer(
llm=llm,
allowed_nodes=[
"Person",
"Location",
"Theory",
"Award",
"Field"
],
allowed_relationships=[
"BORN_IN",
"DEVELOPED",
"RECEIVED",
"DISCOVERED",
"CONTRIBUTED_TO"
]
)
Neo4j에서도 Node와 Relationship Schema를 미리 정의하면 추출 품질을 높일 수 있다고 안내하고 있다.
즉,
LLM이 알아서 그래프 만들어줘
보다
Person
Location
Disease
Drug
BORN_IN
HAS_DISEASE
TAKES_DRUG
INTERACTS_WITH
같이 Domain Schema를 설계해 두는 것이 실제 시스템에서는 훨씬 중요하다.
DB에 저장하기 전에 무엇이 추출됐는지 확인해보는 것도 좋다.
for graph_document in graph_documents:
print("--- Nodes ---")
for node in graph_document.nodes:
print(node)
print("--- Relationships ---")
for relationship in graph_document.relationships:
print(relationship)
예상 형태는 다음과 비슷하다.
Person: Albert Einstein
Location: Ulm
Theory: Theory of Relativity
Albert Einstein
──BORN_IN──>
Ulm
Albert Einstein
──DEVELOPED──>
Theory of Relativity
이 단계가 중요한 이유는 LLM이 잘못 만든 그래프를 그대로 DB에 넣지 않기 위해서다.
추출된 GraphDocument를 Neo4j에 저장한다.
graph.add_graph_documents(
graph_documents,
include_source=True
)
include_source=True를 사용하면 원본 Document와 추출된 Entity 사이의 연결 정보도 저장할 수 있다.
Neo4j의 LLM Knowledge Graph Builder 역시 Document → Chunk → Entity 구조를 연결하여 GraphRAG에 활용하는 방식을 사용한다.
저장 이후 Schema를 갱신한다.
graph.refresh_schema()
print(graph.schema)
중요한 점이 하나 있다.
add_graph_documents()는 그래프의 Node와 Relationship을 저장하는 역할을 한다.
이것을
필요한 성능 인덱스를 전부 자동으로 구성해준다.
라고 이해하면 안 된다.
실제 서비스에서는 데이터 모델에 따라 Constraint, Index, Vector Index 등을 별도로 설계해야 한다.
Neo4j Browser에서 다음 Cypher를 실행할 수 있다.
MATCH (n)
RETURN n
LIMIT 100;
Relationship까지 보고 싶다면,
MATCH (a)-[r]->(b)
RETURN a, r, b
LIMIT 100;
와 같이 확인할 수 있다.
예를 들면 화면에서 다음과 같은 그래프가 만들어진다.
Einstein
├── BORN_IN ──> Ulm
│
├── DEVELOPED ──> Relativity
│
└── RECEIVED ──> Nobel Prize
Newton
└── DISCOVERED ──> Universal Gravitation
여기까지가 Knowledge Graph 구축 단계다.
사용자가 다음처럼 자연어로 질문한다고 해보자.
아인슈타인과 뉴턴의 공통점과
각각의 주요 업적은 무엇인가요?
사용자가 직접 Cypher를 작성하도록 만들 필요는 없다.
LangChain의 GraphCypherQAChain을 이용하면
자연어 질문
↓
LLM
↓
Cypher
↓
Neo4j
↓
Query Result
↓
LLM
↓
자연어 답변
흐름을 만들 수 있다.
Neo4j는 GraphCypherQAChain을 자연어 질문을 Cypher로 변환하고, 그래프 조회 결과를 다시 자연어 답변 생성에 사용하는 구성 요소로 설명하고 있다.
chain = GraphCypherQAChain.from_llm(
llm=llm,
graph=graph,
verbose=True,
validate_cypher=True,
allow_dangerous_requests=True
)
verbose=True로 설정하면 생성된 Cypher를 확인할 수 있다.
질문을 실행한다.
query = """
아인슈타인과 뉴턴의 공통점과
각각의 주요 업적은 무엇인가요?
"""
response = chain.invoke({
"query": query
})
print(response["result"])
사용자는 자연어만 입력한다.
아인슈타인과 뉴턴의 주요 업적은?
LLM이 Graph Schema와 질문을 바탕으로 Cypher를 생성한다.
개념적으로는 다음과 비슷하다.
MATCH (person:Person)-[r]->(achievement)
WHERE person.id IN [
"Albert Einstein",
"Isaac Newton"
]
RETURN person, type(r), achievement
Neo4j가 결과를 반환한다.
Einstein → DEVELOPED → Relativity
Einstein → RECEIVED → Nobel Prize
Newton → DISCOVERED → Universal Gravitation
LLM은 이 데이터를 자연어로 다시 구성한다.
아인슈타인과 뉴턴은 모두 물리학 발전에 큰 영향을 준 학자입니다.
아인슈타인은 상대성 이론을 발전시켰으며,
광전효과 연구로 노벨 물리학상을 받았습니다.
뉴턴은 만유인력의 법칙을 정립했습니다.
즉 핵심은 이것이다.
Natural Language
↓
Cypher
↓
Graph Traversal
↓
Structured Context
↓
LLM
여기서 다음 옵션을 그냥 지나치면 안 된다.
allow_dangerous_requests=True
이 옵션은 LLM이 생성한 Cypher를 DB에서 실행하는 것을 허용한다는 의미다.
문제는 LLM이 항상 읽기 전용 쿼리만 생성한다고 보장할 수 없다는 것이다.
Neo4j GraphAcademy에서도 GraphCypherQAChain 사용 시 LLM이 잘못된 Cypher를 생성하거나 데이터에 영향을 줄 가능성을 경고하며, 운영 환경에서는 읽기 전용 사용자나 RBAC 등을 이용해 권한을 제한할 것을 권장한다.
따라서 운영에서는 다음 구조가 훨씬 안전하다.
Application
↓
Neo4j Read-only User
↓
MATCH / RETURN 허용
CREATE / DELETE / SET 제한
즉,
LLM에게 DB 관리자 권한 주기
는 피해야 한다.
import os
from dotenv import load_dotenv
from langchain_core.documents import Document
from langchain_experimental.graph_transformers import LLMGraphTransformer
from langchain_openai import ChatOpenAI
from langchain_neo4j import Neo4jGraph, GraphCypherQAChain
# --------------------------------------------------
# 1. 환경 변수
# --------------------------------------------------
load_dotenv()
NEO4J_URI = os.getenv("NEO4J_URI")
NEO4J_USERNAME = os.getenv("NEO4J_USERNAME")
NEO4J_PASSWORD = os.getenv("NEO4J_PASSWORD")
# --------------------------------------------------
# 2. Neo4j 연결
# --------------------------------------------------
graph = Neo4jGraph(
url=NEO4J_URI,
username=NEO4J_USERNAME,
password=NEO4J_PASSWORD
)
# --------------------------------------------------
# 3. LLM
# --------------------------------------------------
llm = ChatOpenAI(
model="gpt-4.1-mini",
temperature=0
)
# --------------------------------------------------
# 4. Sample Document
# --------------------------------------------------
sample_text = """
알버트 아인슈타인은 1879년 독일 울름에서 태어난 물리학자이다.
아인슈타인은 상대성 이론을 발전시켰다.
1921년 광전효과 연구에 대한 공로로 노벨 물리학상을 받았다.
아이작 뉴턴은 잉글랜드 출신의 물리학자이자 수학자이다.
뉴턴은 만유인력의 법칙을 정립했다.
아인슈타인과 뉴턴은 물리학 발전에 중요한 영향을 끼쳤다.
"""
documents = [
Document(page_content=sample_text)
]
# --------------------------------------------------
# 5. Knowledge Graph 추출
# --------------------------------------------------
llm_transformer = LLMGraphTransformer(
llm=llm,
allowed_nodes=[
"Person",
"Location",
"Theory",
"Award",
"Field"
],
allowed_relationships=[
"BORN_IN",
"DEVELOPED",
"RECEIVED",
"DISCOVERED",
"CONTRIBUTED_TO"
]
)
graph_documents = (
llm_transformer
.convert_to_graph_documents(documents)
)
# --------------------------------------------------
# 6. 추출 결과 확인
# --------------------------------------------------
for graph_document in graph_documents:
print("--- Nodes ---")
for node in graph_document.nodes:
print(node)
print("--- Relationships ---")
for relationship in graph_document.relationships:
print(relationship)
# --------------------------------------------------
# 7. Neo4j 저장
# --------------------------------------------------
graph.add_graph_documents(
graph_documents,
include_source=True
)
graph.refresh_schema()
print(graph.schema)
# --------------------------------------------------
# 8. GraphRAG QA Chain
# --------------------------------------------------
chain = GraphCypherQAChain.from_llm(
llm=llm,
graph=graph,
verbose=True,
validate_cypher=True,
allow_dangerous_requests=True
)
# --------------------------------------------------
# 9. 질문
# --------------------------------------------------
query = """
아인슈타인과 뉴턴의 공통점과
각각의 주요 업적은 무엇인가요?
"""
response = chain.invoke({
"query": query
})
print("\n--- Question ---")
print(query)
print("\n--- Answer ---")
print(response["result"])
이 정도면 가장 기본적인
Text
→ Knowledge Graph
→ Neo4j
→ NL2Cypher
→ Graph Retrieval
→ LLM
파이프라인을 직접 확인할 수 있다.
여기서 중요한 구분이 있다.
지금 만든 예제는 다음 방식이다.
Natural Language
↓
NL2Cypher
↓
Graph DB
↓
LLM
즉 Graph 기반 Retrieval이다.
하지만 처음 설명했던 GraphRAG 구조에서는 Vector Search까지 함께 사용할 수도 있다.
┌─ Vector Search
│
Question ────────────┤
│
└─ Graph Search
│
▼
Context Merge
│
▼
LLM
Neo4j 역시 GraphRAG Retrieval 패턴으로 Vector + Graph, Entity 기반 검색, Cluster 기반 Global Retrieval, NL2Cypher 등의 여러 방식을 설명한다.
다음 단계에서는 Chunk 자체에 Embedding을 저장한다.
Document
↓
Chunk
├── Embedding
│
└── Entity
↓
Relationship
예를 들면,
Chunk 101
│
MENTIONS
↓
Drug A
│
INTERACTS_WITH
↓
Drug B
그리고 질문이 들어오면 먼저 Vector Search를 사용한다.
Question
↓
Embedding
↓
Top-K Chunk
그다음 해당 Chunk에 연결된 Entity를 찾아간다.
Chunk
↓
Entity
↓
Neighbor Entity
↓
Relationship
즉,
Semantic Search
+
Graph Traversal
을 결합하는 것이다.
Neo4j 공식 GraphRAG 설명에서도 Vector Search로 관련 Text Chunk를 찾은 후 해당 Chunk와 연결된 Entity를 통해 그래프를 확장하는 패턴을 소개하고 있다.
예를 들어 질문이 다음과 같다고 하자.
심부전 환자가 현재 복용 중인 약과 상호작용할 가능성이 있는 약물을 찾아줘.
Vector Search만 사용하면,
심부전
약물
상호작용
같은 표현과 비슷한 문서를 찾아올 수 있다.
Graph Search에서는 다음 관계를 탐색할 수 있다.
Patient
↓
HAS_DISEASE
↓
Heart Failure
Patient
↓
TAKES
↓
Drug A
Drug A
↓
INTERACTS_WITH
↓
Drug B
둘을 결합하면
관련 문서 검색
+
정확한 Entity 관계 확인
이라는 구조를 만들 수 있다.
실제 프로젝트에서는 단순히 Neo4j를 연결했다고 끝나지 않는다.
다음 문제들이 상당히 중요하다.
다음 표현들이 같은 대상임을 알아야 한다.
Microsoft
Microsoft Corp.
Microsoft Corporation
MS
그렇지 않으면 서로 다른 Node가 생성된다.
WORKED_AT
EMPLOYED_BY
WORKS_FOR
MEMBER_OF
처럼 같은 의미의 관계가 제각각 생성되지 않도록 Schema를 통제해야 한다.
Graph에 저장된 정보가 어떤 문서에서 나온 정보인지 추적할 수 있어야 한다.
Document
↓
Chunk
↓
Entity
↓
Relationship
구조를 유지하는 이유다.
기업이나 의료 데이터에서는 시간도 중요하다.
2024
Person A
↓
WORKED_AT
Company B
2026
Person A
↓
WORKED_AT
Company C
단순히 관계만 저장하면 현재와 과거 정보를 구분하기 어렵다.
LLM이 만든 관계가 실제 문서와 일치하는지도 평가해야 한다.
Entity Accuracy
Relationship Accuracy
Retrieval Accuracy
Answer Groundedness
등을 함께 살펴볼 필요가 있다.
GraphRAG라는 이름으로 널리 알려진 또 하나의 구현이 Microsoft GraphRAG다.
Microsoft GraphRAG는 단순히 Entity와 Relationship만 저장하지 않는다.
문서에서
TextUnit
↓
Entity / Relationship
↓
Knowledge Graph
↓
Community Detection
↓
Community Summary
구조를 만든다.
Microsoft 공식 문서에서는 Graph를 계층적인 Community로 클러스터링하고 각 Community의 Summary를 생성하여 데이터셋 전체에 대한 질문에도 활용한다.
Microsoft GraphRAG에서는 대표적으로 Local Search와 Global Search가 있다.
특정 Entity 중심의 질문에 적합하다.
Question
↓
Entity Search
↓
Connected Entities
↓
Relationships
↓
Relevant Text
예를 들어,
Drug A와 관련된 부작용과 임상시험을 알려줘.
같은 질문이다.
Microsoft의 Local Search 역시 질문과 의미적으로 관련된 Entity를 시작점으로 관련 Entity, Relationship, Text Unit 등을 함께 검색한다.
데이터 전체의 패턴을 묻는 질문에 적합하다.
예를 들면,
이 연구 자료 전체에서 가장 중요한 연구 주제 다섯 가지는 무엇인가?
이 질문은 특정 Chunk 하나를 검색해서는 제대로 답하기 어렵다.
그래서 Community Summary를 이용한다.
Knowledge Graph
↓
Community 1 ─ Summary
Community 2 ─ Summary
Community 3 ─ Summary
↓
Map
↓
Reduce
↓
Global Answer
Microsoft GraphRAG의 Global Search는 Community Report를 Map-Reduce 방식으로 종합하여 전체 데이터셋에 대한 질문에 답하는 구조를 사용한다.
처음부터 모든 것을 구현하려고 하면 상당히 복잡하다.
다음 순서가 현실적이다.
STEP 1
Document
↓
LLMGraphTransformer
↓
Neo4j
먼저 Knowledge Graph를 만들어본다.
그다음,
STEP 2
Natural Language
↓
GraphCypherQAChain
↓
Cypher
↓
Neo4j
NL2Cypher 검색을 구현한다.
다음으로,
STEP 3
Embedding
+
Neo4j Vector Index
+
Graph Traversal
Hybrid Retrieval을 구현한다.
그 이후,
STEP 4
Entity Resolution
Schema Validation
Reranking
Source Citation
Evaluation
품질을 높인다.
마지막으로 데이터 규모가 커지고 전체 데이터 분석이 필요하다면,
STEP 5
Community Detection
Community Summary
Local Search
Global Search
같은 GraphRAG 패턴으로 확장하는 것이 좋다.
Basic RAG
Question
↓
Vector Search
↓
Chunk
↓
LLM
↓
Graph QA
Question
↓
NL2Cypher
↓
Neo4j
↓
LLM
↓
Hybrid GraphRAG
Question
↓
┌────────────────┐
│ Vector Search │
│ Graph Search │
└────────────────┘
↓
Context
↓
LLM
↓
Advanced GraphRAG
Document
↓
Knowledge Graph
↓
Community Detection
↓
Community Summary
↓
Local / Global Retrieval
↓
LLM
GraphRAG를 공부할 때 이 단계들을 한꺼번에 같은 것으로 생각하면 오히려 이해하기 어렵다.
GraphRAG의 핵심은 단순히 Neo4j를 사용하는 것이 아니다.
중요한 것은 Retrieval에서 관계 구조를 활용한다는 것이다.
기존 Vector RAG가
질문과 비슷한 문서를 찾는다.
에 집중한다면 GraphRAG는
관련 Entity를 찾는다.
↓
Entity 사이의 관계를 탐색한다.
↓
필요한 문서와 구조화된 정보를 함께 가져온다.
↓
LLM에게 Context로 제공한다.
로 발전한다.
그리고 실제 구현에서는 다음처럼 단계적으로 접근하는 것이 좋다.
LLMGraphTransformer
↓
Knowledge Graph
↓
Neo4j
↓
GraphCypherQAChain
↓
Vector + Graph Hybrid Search
↓
Entity Resolution
↓
Community / Global Search
즉 GraphRAG를 한 문장으로 정리하면,
일반 RAG가 관련된 문서를 찾는 기술이라면, GraphRAG는 관련 정보가 서로 어떻게 연결되어 있는지까지 Retrieval에 활용하는 방식이다.
그리고 Neo4j와 LangChain을 이용하면 자연어 문서에서 Entity와 Relationship을 자동으로 추출하고, Graph DB에 저장한 뒤, 사용자의 자연어 질문을 Cypher로 변환하여 실제 관계 기반 검색까지 직접 구현해볼 수 있다.