벡터 RAG를 넘어: 2026년형 GraphRAG(지식 그래프 기반 RAG) 구현 실전 가이드
들어가며
일반적인 Vector RAG는 “질문과 가장 비슷한 chunk 몇 개”를 가져오는 데는 강하지만, 관계(relationship) 가 답의 핵심인 문제에서 자주 무너집니다. 예를 들면:
- “A 서비스 장애의 근본 원인은 뭐였고, 그게 어떤 배포/설정 변경과 연결돼?” (원인-결과/의존성)
- “이 계약 조항이 바뀌면, 어떤 하위 규정과 예외 조항까지 영향이 가?” (참조/파생)
- “이 논문 결론을 뒷받침하는 실험 조건과 관련 work를 연결해서 설명해줘” (다중 홉)
이때 GraphRAG는 “텍스트 덩어리”가 아니라 엔터티/관계로 구성된 knowledge graph를 기반으로, multi-hop traversal과 subgraph context assembly를 통해 LLM에게 구조화된 근거 묶음을 제공합니다. Neo4j는 GraphRAG 패턴으로 “contextual subgraph retrieval”, “hybrid vector-graph search”, “multi-hop reasoning” 등을 강조합니다. (neo4j.com)
다만 만능은 아닙니다. 2026년 연구들에서 반복적으로 지적되는 포인트는: GraphRAG를 모든 쿼리에 강제 적용하면 latency/cost가 급증하고, 실전에서는 오히려 vanilla RAG보다 성능이 떨어질 수도 있다는 점입니다. 그래서 최근 트렌드는 “Graph가 필요할 때만 쓰는 라우팅/하이브리드” 쪽으로 갑니다. (arxiv.org)
언제 쓰면 좋나
- 질문이 본질적으로 관계/경로/의존성(원인→결과, 조직→프로젝트→배포, 법령→하위조항)을 요구
- 데이터가 문서 여러 개에 흩어져 있고, “한 chunk”로 답이 안 나오는 multi-hop 질의가 빈번
- 설명 가능성(왜 이 답이 나왔는지)이 중요한 제품(감사/규제/법무/의료/엔터프라이즈)
언제 쓰면 안 되나
- 대부분이 single-hop FAQ, 정책 문구 검색처럼 “그냥 관련 문단”이면 충분
- 그래프 구축/유지 비용을 감당 못 함(LLM 추출 비용 + 인덱싱 + 재처리)
- 스키마/엔터티 정의가 아직 불명확한 초기 단계(그래프가 금방 오염됨)
🔧 핵심 개념
1) GraphRAG의 본질: “chunk retrieval”이 아니라 “subgraph retrieval”
GraphRAG 구현은 대개 아래 2개의 인덱스를 함께 씁니다.
1) Vector index: 문서 chunk 임베딩(검색 recall 확보)
2) Graph index: 엔터티/관계/출처 provenance(정확한 연결 + 설명 가능성)
검색 시나리오(권장 흐름)는 대개 이렇습니다.
- Seed retrieval(초기 후보 찾기)
- vector로 관련 chunk top-k 확보 (cheap recall)
- Entity linking(그래프 앵커 생성)
- 후보 chunk에서 엔터티(서비스, 팀, 릴리즈, RFC, 장애 티켓…)를 식별하고 그래프 노드에 매핑
- Subgraph expansion(다중 홉 확장)
- “서비스 A”에서 “의존 서비스”, “관련 배포”, “연관 인시던트” 등 N-hop 확장
- Context assembly(컨텍스트 조립)
- subgraph를 그대로 던지지 말고 “답에 필요한 경로/근거” 중심으로 정리(요약/필터/랭킹)
- LLM generation
- (질문 + subgraph 근거 + 출처 링크/문서 snippet)로 답 생성
Neo4j 쪽 자료에서도 GraphRAG는 단순 벡터 검색이 놓치는 “관계 기반 컨텍스트”를 주는 방식으로 설명합니다. (neo4j.com)
2) 2026년 관점에서 중요한 차이점: “항상 Graph”가 아니라 “필요할 때 Graph”
2026년 논문에서는 GraphRAG가 실전에서 느리고 비싸며, 쉬운 질문에서는 오히려 손해가 날 수 있다고 봅니다. 그래서 쿼리 복잡도를 측정해 RAG/GraphRAG를 라우팅하거나 경계 케이스는 fusion으로 합치는 접근이 제안됩니다. (arxiv.org)
→ 실무적 결론: GraphRAG는 ‘고난도 질문을 위한 고급 모드’로 설계하는 게 비용/성능 균형에 유리합니다.
3) 구현 선택지(2026년 4월 기준)
- Microsoft GraphRAG: 모듈형 파이프라인/변환 스위트로 “비정형 텍스트 → 구조화 데이터(그래프 메모리)”를 목표. 2026-04-13 기준 v3.0.9 릴리즈가 확인됩니다. (github.com)
- Neo4j GraphRAG 패턴/라이브러리: 그래프 DB 네이티브 traversal + (환경에 따라) 벡터/필터 결합. (neo4j.com)
- LlamaIndex KG/GraphStore 계열: Neptune 같은 관리형 graph store와 결합하는 루트도 존재. (aws.amazon.com)
💻 실전 코드
아래 예제는 “사내 Incident Postmortem + RFC + Deploy 로그” 같은 문서 코퍼스를 Neo4j에 (1) chunk + (2) 엔터티 그래프로 적재해두었다는 전제에서, 질문 시 vector로 씨앗을 잡고 → 그래프를 N-hop 확장 → 근거 포함 답변까지 가는 현실적인 패턴입니다.
(핵심은 toy가 아니라, 운영 데이터에서 흔한 ‘원인/영향/연관 변경’ 질문을 다룬다는 점입니다.)
0) 의존성/환경
1
2
3
4
5
6
7
8
9
10
11
python -m venv .venv && source .venv/bin/activate
pip install neo4j neo4j-graphrag python-dotenv openai
# .env
cat > .env <<'EOF'
NEO4J_URI=bolt://localhost:7687
NEO4J_USERNAME=neo4j
NEO4J_PASSWORD=your_password
NEO4J_DATABASE=neo4j
OPENAI_API_KEY=your_openai_key
EOF
1) (전제) Neo4j에 벡터 인덱스 + 기본 스키마가 이미 있다
예를 들어 :Chunk 노드에 text, doc_id, ts, service 같은 프로퍼티가 있고, 벡터 인덱스 이름이 chunksEmbedding 라고 가정합니다.
또한 엔터티 그래프는 다음을 최소로 갖추는 게 운영에서 유용합니다.
(:Service {name})(:Deploy {id, ts, version})(:Incident {id, ts, severity})- 관계 예:
(Service)-[:DEPENDS_ON]->(Service)(Deploy)-[:AFFECTS]->(Service)(Incident)-[:IMPACTS]->(Service)(Incident)-[:RELATED_TO_DEPLOY]->(Deploy)(Chunk)-[:MENTIONS]->(Service|Deploy|Incident)+ 출처 연결
2) VectorRetriever로 seed chunk 확보 + Graph traversal로 subgraph 확장
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import os
from dotenv import load_dotenv
from neo4j import GraphDatabase
from neo4j_graphrag.embeddings.openai import OpenAIEmbeddings
from neo4j_graphrag.retrievers import VectorRetriever
from neo4j_graphrag.llm import OpenAILLM
from neo4j_graphrag.generation import GraphRAG
load_dotenv()
driver = GraphDatabase.driver(
os.environ["NEO4J_URI"],
auth=(os.environ["NEO4J_USERNAME"], os.environ["NEO4J_PASSWORD"]),
)
embedder = OpenAIEmbeddings(model="text-embedding-3-large")
retriever = VectorRetriever(
driver,
neo4j_database=os.environ.get("NEO4J_DATABASE", "neo4j"),
index_name="chunksEmbedding",
embedder=embedder,
return_properties=["text", "doc_id", "ts", "service"],
)
llm = OpenAILLM(
model_name="gpt-4o-mini",
model_params={"temperature": 0}
)
rag = GraphRAG(retriever=retriever, llm=llm)
QUESTION = "지난주 결제 서비스 장애의 근본 원인과, 연관된 배포 변경(버전/시간)을 근거와 함께 요약해줘."
# 1) seed: vector로 관련 chunk top-k
seed = retriever.search(query_text=QUESTION, top_k=8)
# 2) seed chunk -> 엔터티 앵커를 따라 subgraph 확장 (Cypher 예시)
# 운영에서는 hop=1~2 정도가 대부분 비용 대비 효율이 좋습니다.
cypher = """
UNWIND $docIds AS docId
MATCH (c:Chunk {doc_id: docId})
OPTIONAL MATCH (c)-[:MENTIONS]->(e)
WITH collect(DISTINCT e) AS ents
UNWIND ents AS e
OPTIONAL MATCH p1=(e)-[r1*0..2]-(n)
WITH collect(DISTINCT p1)[0..40] AS paths // 폭발 방지
RETURN paths
"""
doc_ids = [r["doc_id"] for r in seed.items] # VectorRetriever 결과 구조는 환경에 따라 다를 수 있음
with driver.session(database=os.environ.get("NEO4J_DATABASE", "neo4j")) as session:
paths = session.run(cypher, docIds=doc_ids).data()
# 3) LLM에 넣을 컨텍스트 조립(“그래프 덤프” 금지: 경로/노드 요약으로 축약)
def summarize_paths(paths):
# 실무에서는 node/rel 타입별로 정규화해 텍스트를 만들고,
# 중요도(최근 ts, severity, deploy 영향도 등)로 상위만 남깁니다.
return f"Subgraph paths count={len(paths)} (trimmed). Include Incident/Deploy/Service edges."
graph_context = summarize_paths(paths)
prompt = f"""
You are an SRE assistant. Answer in Korean.
Question: {QUESTION}
Evidence:
- Retrieved chunks (top 8) are available, plus a related subgraph summary.
- Graph context: {graph_context}
Constraints:
- Include incident id, deploy id/version/time if present.
- If evidence is insufficient, say what is missing.
"""
answer = rag.search(query_text=prompt, retriever_config={"top_k": 8})
print(answer.answer)
driver.close()
예상 출력(형태)
- “Incident INC-2026-0412 … 결제 서비스 결함…”
- “Deploy DEP-8391 v2.18.4 at 2026-04-12 03:12Z … AFFECTS PaymentService”
- “근본 원인: X 설정 변경 → 의존 서비스 타임아웃 → 재시도 폭증”
- “근거: Postmortem 문서 doc_id=…, 변경 RFC doc_id=…”
이 예제의 포인트는:
- vector는 recall 확보용(빠르게 관련 문서 찾기)
- graph는 “왜/어떻게 연결되는가”를 경로로 강제
- 최종 컨텍스트는 “필요한 경로만 요약”해서 LLM 토큰/혼선을 줄임
⚡ 실전 팁 & 함정
Best Practice 1) “GraphRAG 라우팅”을 기본값으로
2026년 연구 흐름처럼, 질문 복잡도가 낮으면 Vector RAG로 끝내고, 관계/다중 홉이 필요한 질문에만 그래프 확장을 켜세요. 이게 latency/cost를 가장 크게 줄입니다. (arxiv.org)
- 휴리스틱 예: “원인/영향/의존/관련/변경/경로/비교/왜” 키워드 + 엔터티 개수/조건절 많으면 Graph 모드
- 경계 케이스는 vector+graph 결과를 fusion(예: RRF)하는 쪽이 안전
Best Practice 2) 그래프 확장은 폭발 제어가 1순위
현업에서 제일 흔한 장애는 “N-hop 한 번 잘못 돌려서 subgraph가 터지는 것”입니다.
- hop은 1~2로 시작
LIMIT, 경로 수 cap(예: 40), 타입 필터(Incident/Deploy/Service만) 강제- “최근 30일, severity>=SEV2” 같은 메타데이터 필터를 traversal에 넣기
Neo4j 쪽에서도 GraphRAG는 hybrid/필터링/최적화가 핵심 패턴으로 반복 등장합니다. (neo4j.com)
Best Practice 3) provenance(출처) 없으면 GraphRAG는 오히려 독
엔터티/관계만 있으면 LLM이 그럴듯하게 엮어버립니다.
반드시:
(:Chunk)-[:MENTIONS]->(:Entity)(:Relation {source_doc_id, source_chunk_id, confidence, extracted_at})
같은 식으로 “이 관계가 어느 문서에서 나왔는지”를 남기세요.
흔한 함정/안티패턴
- LLM이 만든 그래프를 ‘정답’으로 취급: 추출 오류는 누적되며, 한 번 오염되면 검색 결과가 계속 틀어집니다.
- Graph-only 검색으로 vector를 버림: seed recall이 약해져서 “그래프에 이미 연결된 것만” 답하게 됩니다.
- 스키마를 너무 크게 시작: 엔터티 타입 30개로 시작하면 대부분 품질 관리 실패합니다. 핵심 5~8개 타입부터.
비용/성능/안정성 트레이드오프
- 그래프 구축(indexing)이 비싸다는 경고는 Microsoft GraphRAG 쪽에서도 명시적으로 강조합니다(“start small”). (github.com)
- 따라서 “전 문서 전량 그래프화”보다,
- 핫 도메인(인시던트/규정/계약/제품 의존성)만 그래프화
- 나머지는 vector + 메타데이터 필터 로 단계적 도입이 현실적입니다.
🚀 마무리
GraphRAG는 “검색 정확도” 자체보다, 관계 기반 근거를 조립해 multi-hop 질문에 답하게 만드는 구조가 본질입니다. 2026년 시점의 가장 실용적인 결론은:
- GraphRAG는 강력하지만 비싸고 느릴 수 있다 → 항상 켜지 말고 라우팅/하이브리드로 설계 (arxiv.org)
- 운영에서 성공하는 GraphRAG는 (1) seed vector recall (2) 제한된 hop 확장 (3) provenance (4) 컨텍스트 요약/랭킹을 갖춘다 (neo4j.com)
- 도입 판단 기준: “우리 질문의 30% 이상이 multi-hop/의존성/영향 분석인가?”가 Yes면 PoC 가치가 큽니다. No면 Vector RAG + 메타데이터 필터가 더 싸고 빠릅니다.
다음 학습 추천(실전 순서): 1) Microsoft GraphRAG 최신 릴리즈/파이프라인 구조 파악(v3.0.9, 2026-04-13) (github.com)
2) Neo4j GraphRAG 패턴으로 “subgraph retrieval + hybrid search”를 작은 도메인부터 적용 (neo4j.com)
3) “Graph가 필요할 때만”이라는 라우팅 설계를 평가 지표(accuracy/latency/cost)로 고정 (arxiv.org)