이 글은 Graph RAG 를 구축하는 방법을 설명한다. 그래프 RAG는 node, edge 로 구성된 연결된 형식의 자료구조를 RAG 처리하는 기술을 의미한다.
이 기술을 접근하는 방법은 크게 두가지로 나뉜다.
첫째, 실용적으로 사용하기 위해서 크게 neo4j 와 같이 그래프 질의 언어(cypher query, SparQL 등) 언어를 통해 그래프 DB를 질의할 수 있도록 만든 그래프 DB를 적용한다. 이를 통해 다음과 같은 정보 질의가 가능하다.
둘째, 주어진 로우 텍스트 데이터를 그래프 구조로 변환해(예. 주어, 목적어, 서술어 형식), RAG 처리하는 GraphRAG 를 적용한다. DBMS 시스템은 아니라서 자체 질의 언어는 없지만, RAG 처리된 벡터 검색을 통해 정보를 LLM에서 생성하는 방식으로 동작할 수 있다.
이 글은 이 두 가지 방법을 모두 설명하고, 구체적으로 개발 방법도 예시한다.
Graph database 를 이용한 RAG 방법
Neo4j 설치
Neo4j는 그래프 DBMS로 SparQL을 통해 그래프 DB 질의를 효과적으로 지원 관리하는 시스템이다.
Neo4j는 오픈소스로 langchain 커뮤니티 모듈에 연결해 사용할 수 있다.
다음 터미널 명령을 실행해 설치한다.
pip install lanchain
pip install openai
pip install neo4j
개발
다음과 같이 코딩한다.
import os
import json
import pandas as pd
from dotenv import load_dotenv
load_dotenv()
오픈API 키를 설정한다.
# os.environ["OPENAI_API_KEY"] = "<your_api_key>"
# print(os.environ["OPENAI_API_KEY"])
실습할 데이터셋 로딩한다.
# Loading a json dataset from a file
file_path = 'data/amazon_product_kg.json'
with open(file_path, 'r') as file:
jsonData = json.load(file)
df = pd.read_json(file_path)
df.head()
데이터베이스에 연결하고, 파싱한다.
url = "bolt://localhost:7687"
username ="neo4j"
password = "<your_password_here>"
from langchain.graphs import Neo4jGraph
graph = Neo4jGraph(
url=url,
username=username,
password=password
)
def sanitize(text):
text = str(text).replace("'","").replace('"','').replace('{','').replace('}', '')
return text
각 JSON 파일의 객체를 데이터베이스에 추가한다.
i = 1
for obj in jsonData:
print(f"{i}. {obj['product_id']} -{obj['relationship']}-> {obj['entity_value']}")
i+=1
query = f'''
MERGE (product:Product {{id: {obj['product_id']}}})
ON CREATE SET product.name = "{sanitize(obj['product'])}",
product.title = "{sanitize(obj['TITLE'])}",
product.bullet_points = "{sanitize(obj['BULLET_POINTS'])}",
product.size = {sanitize(obj['PRODUCT_LENGTH'])}
MERGE (entity:{obj['entity_type']} {{value: "{sanitize(obj['entity_value'])}"}})
MERGE (product)-[:{obj['relationship']}]->(entity)
'''
graph.query(query)
질의 처리를 위해 각 유형 속성에 대한 벡터 인덱스를 생성한다. 이는 임베딩 함수를 사용해 처리한다.
from langchain.vectorstores.neo4j_vector import Neo4jVector
from langchain.embeddings.openai import OpenAIEmbeddings
embeddings_model = "text-embedding-3-small"
vector_index = Neo4jVector.from_existing_graph(
OpenAIEmbeddings(model=embeddings_model),
url=url,
username=username,
password=password,
index_name='products',
node_label="Product",
text_node_properties=['name', 'title'],
embedding_node_property='embedding',
)
def embed_entities(entity_type):
vector_index = Neo4jVector.from_existing_graph(
OpenAIEmbeddings(model=embeddings_model),
url=url,
username=username,
password=password,
index_name=entity_type,
node_label=entity_type,
text_node_properties=['value'],
embedding_node_property='embedding',
)
entities_list = df['entity_type'].unique()
for t in entities_list:
embed_entities(t)
데이터베이스 질의를 한다.
from langchain.chains import GraphCypherQAChain
from langchain.chat_models import ChatOpenAI
chain = GraphCypherQAChain.from_llm(
ChatOpenAI(temperature=0), graph=graph, verbose=True,
)
chain.run("""
Help me find curtains
""")
프롬프트 템플릿을 이용해, 좀 더 상세한 결과가 나올 수 있도록 한다.
entity_types = {
"product": "Item detailed type, for example 'high waist pants', 'outdoor plant pot', 'chef kitchen knife'",
"category": "Item category, for example 'home decoration', 'women clothing', 'office supply'",
"characteristic": "if present, item characteristics, for example 'waterproof', 'adhesive', 'easy to use'",
"measurement": "if present, dimensions of the item",
"brand": "if present, brand of the item",
"color": "if present, color of the item",
"age_group": "target age group for the product, one of 'babies', 'children', 'teenagers', 'adults'. If suitable for multiple age groups, pick the oldest (latter in the list)."
}
relation_types = {
"hasCategory": "item is of this category",
"hasCharacteristic": "item has this characteristic",
"hasMeasurement": "item is of this measurement",
"hasBrand": "item is of this brand",
"hasColor": "item is of this color",
"isFor": "item is for this age_group"
}
entity_relationship_match = {
"category": "hasCategory",
"characteristic": "hasCharacteristic",
"measurement": "hasMeasurement",
"brand": "hasBrand",
"color": "hasColor",
"age_group": "isFor"
}
system_prompt = f'''
You are a helpful agent designed to fetch information from a graph database.
The graph database links products to the following entity types:
{json.dumps(entity_types)}
Each link has one of the following relationships:
{json.dumps(relation_types)}
Depending on the user prompt, determine if it possible to answer with the graph database.
The graph database can match products with multiple relationships to several entities.
Example user input:
"Which blue clothing items are suitable for adults?"
There are three relationships to analyse:
1. The mention of the blue color means we will search for a color similar to "blue"
2. The mention of the clothing items means we will search for a category similar to "clothing"
3. The mention of adults means we will search for an age_group similar to "adults"
Return a json object following the following rules:
For each relationship to analyse, add a key value pair with the key being an exact match for one of the entity types provided, and the value being the value relevant to the user query.
For the example provided, the expected output would be:
{{
"color": "blue",
"category": "clothing",
"age_group": "adults"
}}
If there are no relevant entities in the user prompt, return an empty json object.
'''
print(system_prompt)
from openai import OpenAI
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY", "<your OpenAI API key if not set as env var>"))
질의 함수를 정의하고, 원하는 제품 아이템을 질의해본다. Cypher 쿼리를 사용한다.
def define_query(prompt, model="gpt-4-1106-preview"):
completion = client.chat.completions.create(
model=model,
temperature=0,
response_format= {
"type": "json_object"
},
messages=[
{
"role": "system",
"content": system_prompt
},
{
"role": "user",
"content": prompt
}
]
)
return completion.choices[0].message.content
example_queries = [
"Which pink items are suitable for children?",
"Help me find gardening gear that is waterproof",
"I'm looking for a bench with dimensions 100x50 for my living room"
]
for q in example_queries:
print(f"Q: '{q}'\n{define_query(q)}\n")
추출된 엔티티가 현재 로딩한 데이터와 정확히 일치하지 않을 수 있으므로, GDS 코사인 유사도 함수를 이용해 질의된 제품을 반환할 수 있도록 한다.
def create_embedding(text):
result = client.embeddings.create(model=embeddings_model, input=text)
return result.data[0].embedding
# The threshold defines how closely related words should be. Adjust the threshold to return more or less results
def create_query(text, threshold=0.81):
query_data = json.loads(text)
# Creating embeddings
embeddings_data = []
for key, val in query_data.items():
if key != 'product':
embeddings_data.append(f"${key}Embedding AS {key}Embedding")
query = "WITH " + ",\n".join(e for e in embeddings_data)
# Matching products to each entity
query += "\nMATCH (p:Product)\nMATCH "
match_data = []
for key, val in query_data.items():
if key != 'product':
relationship = entity_relationship_match[key]
match_data.append(f"(p)-[:{relationship}]->({key}Var:{key})")
query += ",\n".join(e for e in match_data)
similarity_data = []
for key, val in query_data.items():
if key != 'product':
similarity_data.append(f"gds.similarity.cosine({key}Var.embedding, ${key}Embedding) > {threshold}")
query += "\nWHERE "
query += " AND ".join(e for e in similarity_data)
query += "\nRETURN p"
return query
def query_graph(response):
embeddingsParams = {}
query = create_query(response)
query_data = json.loads(response)
for key, val in query_data.items():
embeddingsParams[f"{key}Embedding"] = create_embedding(val)
result = graph.query(query, params=embeddingsParams)
return result
example_response = '''{
"category": "clothes",
"color": "blue",
"age_group": "adults"
}'''
result = query_graph(example_response)
# Result
print(f"Found {len(result)} matching product(s):\n")
for r in result:
print(f"{r['p']['name']} ({r['p']['id']})")
질의 결과, neo4j 데이터베이스를 검색한 후, 이를 바탕으로 LLM 생성된 텍스트를 json 형식으로 결과 얻을 수 있다.
GraphRAG 기술
GraphRAG는 비정형 텍스트 데이터로부터 지식 그래프(Knowledge Graph)를 자동으로 구축하고, 이를 기반으로 검색 증강 생성(Retrieval-Augmented Generation, RAG)을 수행하는 차세대 프레임워크이다. GraphRAG의 핵심 철학은 개별 데이터 포인트를 독립된 정보 조각으로 취급하는 기존 방식에서 벗어나, 데이터 간의 관계로 연결된 유기적인 네트워크로 파악하는 데에 있다. 즉, 텍스트에 흩어져 있는 정보들을 단순한 나열이 아닌, 서로 연결된 맥락의 집합체로 재구성하는 것이다. 이를 통해 인공지능이 데이터의 전체적인 구조와 복잡한 맥락을 깊이 있게 이해하고, 표면적인 정보 검색을 넘어선 깊이 있는 추론을 통해 정확하고 종합적인 답변을 생성하도록 돕는 기술이다.
이 기술은 마이크로소프트 리서치(Microsoft Research. 2023/12)에서 주도하여 개발한 것이다. 현재는 누구나 자유롭게 활용하고 기술 발전에 기여할 수 있도록 오픈소스 프로젝트로 공개되어 있다. 따라서 전 세계 개발자들이 해당 기술을 자신의 프로젝트에 적용하고 개선하는 데 참여할 수 있는 구조이다.
GraphRAG의 탄생 배경
LLM의 고질적인 문제인 환각(Hallucination)을 줄이기 위해 등장한 기존의 벡터 검색 기반 RAG는 획기적인 기술이었으나, 명확한 한계를 내포하고 있었다.
기존 RAG는 거대한 도서관에서 질문과 관련된 책 몇 권을 찾아주는 사서와 비슷하다. 하지만 이 사서는 책의 '내용'보다는 '제목'이나 '키워드'의 유사성에 의존하여 책을 추천한다. 이로 인해 다음과 같은 문제점이 발생한다. 첫째, 의미적 한계이다. "A와 B의 관계가 C에 어떤 영향을 미쳤는가?"와 같이 여러 데이터에 걸친 복합적인 관계나 인과관계를 추론하는 질문에는 매우 취약하다. 이는 여러 책에 나뉘어 서술된 사건의 전후 관계를 파악하지 못하는 것과 같다. 둘째, 맥락의 파편화이다. 문서를 단순히 고정된 크기의 덩어리(chunk)로 자르는 과정에서 중요한 전체 맥락이 소실된다. 이는 책의 각 페이지를 낱장으로 찢어 보관하는 것과 같아서, 1페이지와 50페이지에 담긴 연속적인 내용을 함께 이해하기 어렵게 만든다. 셋째, 숨겨진 인사이트 발견의 어려움이다. 데이터 전체를 조망해야만 알 수 있는 숨겨진 트렌드, 주요 그룹, 핵심 인물 등의 전체론적(holistic)인 질문에는 거의 답할 수 없다.
GraphRAG는 이러한 기존 RAG의 문제를 해결하기 위해 탄생했다. 이는 도서관의 모든 책을 읽고, 등장인물, 사건, 개념 간의 관계를 모두 파악하여 거대한 인물 관계도나 사건 지도를 만드는 사서와 같다. 텍스트에 명시적이거나 암묵적으로 존재하는 관계들을 지식 그래프라는 구조로 명시적으로 표현함으로써, 파편화된 정보를 유기적으로 연결하고 데이터의 전체적인 그림을 AI에게 제공한다. 이를 통해 단순한 정보 검색을 넘어, 관계를 기반으로 한 진정한 의미의 추론과 분석이 가능해지는 것이다.
GraphRAG의 전체 프로세스는 크게 인덱싱(데이터를 지식 그래프로 변환하는 단계)과 쿼리(그래프를 활용해 답변을 생성하는 단계)의 두 가지 핵심 단계로 구성된다.
인덱싱 단계: 지식의 구조화
데이터 로딩 (Data Loading): 지정된 input 폴더에 있는 .txt, .pdf, .csv 등 다양한 형식의 원본 문서들을 시스템이 읽어들인다. 이 단계에서 각기 다른 형식의 파일들은 후속 처리가 용이하도록 표준화된 텍스트 형식으로 변환된다.
텍스트 분할 (Text Chunking): LLM이 한 번에 처리할 수 있는 일정한 크기로 긴 문서를 분할한다. 이때 단순히 기계적으로 자르는 것이 아니라, 문단이나 문장의 경계를 최대한 존중하여 의미 있는 정보 단위(chunk)로 나누는 전략을 사용한다.
개체 및 관계 추출 (Entity & Relationship Extraction): 이 단계는 GraphRAG의 심장부라 할 수 있다. 시스템은 사전 정의된 정교한 프롬프트를 사용하여 LLM에게 각 텍스트 덩어리에서 핵심 개체(인물, 조직, 장소, 특정 개념 등)와 그들 사이의 관계를 추출하도록 요청한다. 예를 들어, '스티브 잡스는 스티브 워즈니악과 함께 애플을 공동 창업했다.'라는 텍스트가 있다면, LLM은 (스티브 잡스, '공동 창업하다', 애플), (스티브 워즈니악, '공동 창업하다', 애플), (스티브 잡스, '협력하다', 스티브 워즈니악)과 같은 관계 트리플(triplet)을 추출해낸다.
지식 그래프 구축 (Knowledge Graph Construction): 앞서 추출된 모든 개체와 관계 정보들은 하나의 거대한 네트워크로 통합된다. 추출된 개체는 그래프의 노드(Node)가 되고, 관계는 이 노드들을 연결하는 엣지(Edge)가 된다. 여러 문서에서 반복적으로 등장하는 '애플'과 같은 개체는 하나의 노드로 병합되어 정보가 중앙에서 관리된다.
그래프 강화 (Graph Enrichment): 구축된 그래프는 추가적인 분석을 통해 더욱 유용하게 강화된다. 먼저, 그래프 분석 알고리즘을 통해 서로 밀접하게 연결된 노드들의 군집인 커뮤니티(Community)를 탐지한다. 그 후, LLM을 사용해 각 개별 노드, 관계, 그리고 커뮤니티에 대한 상세한 요약문을 생성한다. 이 과정은 데이터에 대한 다층적이고 계층적인 이해를 가능하게 만든다.
쿼리 단계: 지식의 활용
사용자 질의 분석 (User Query Analysis): 사용자가 자연어로 질문을 입력하면, 시스템은 먼저 LLM을 활용해 질문의 핵심 의도를 파악하고, 질문에 언급된 주요 개체를 식별한다. 이후 질문의 성격에 따라 지역 검색(Local Search)과 전역 검색(Global Search) 중 더 적합한 전략을 동적으로 선택한다.
컨텍스트 검색 (Context Retrieval - The "R"):
지역 검색 (Local Search): "스티브 잡스는 애플에서 어떤 역할을 했는가?"와 같이 특정 대상에 대한 상세하고 구체적인 질문에 사용된다. 시스템은 그래프에서 '스티브 잡스' 노드를 시작점으로 하여, 그와 직접적으로 연결된 '애플' 노드 및 '공동 창업하다'와 같은 관계 엣지들을 탐색하며 정밀한 정보를 수집한다.
전역 검색 (Global Search): "이 기술 문서들의 핵심 주제와 주요 기여자는 누구인가?"처럼 데이터 전체를 아우르는 넓은 질문에 사용된다. 이 경우, 시스템은 인덱싱 단계에서 미리 생성해 둔 커뮤니티 요약 정보를 활용하여 데이터셋의 거시적인 구조와 핵심 주제를 신속하게 파악한다.
컨텍스트 증강 및 프롬프트 생성 (Context Augmentation & Prompting): 그래프에서 검색된 모든 관련 정보(노드, 관계, 요약문 등)는 LLM이 가장 잘 이해할 수 있는 형태로 가공된다. 이 가공된 정보는 사용자의 원본 질문과 결합되어, 풍부한 맥락을 담은 새로운 프롬프트를 동적으로 생성한다.
최종 답변 생성 (Answer Generation - The "G"): 최종적으로 생성된 증강 프롬프트는 LLM에게 전달된다. LLM은 이 풍부한 컨텍스트를 바탕으로 논리적이고 종합적인 답변을 생성한다. 중요한 점은, 답변의 근거가 된 원본 데이터 소스를 함께 제시함으로써 사용자가 직접 사실을 검증할 수 있도록 하여 답변의 신뢰성을 극대화한다는 것이다.
표. 일반 RAG vs. GraphRAG 비교
구분 | 일반 RAG (Standard RAG) | GraphRAG |
데이터 구조 | 텍스트 덩어리 (Chunk)의 비구조적 목록 | 지식 그래프 (Knowledge Graph): 개체(노드)와 관계(엣지)의 구조적 네트워크 |
검색 방식 | 벡터 유사도 검색 (의미적으로 가까운 것) | 그래프 탐색 (직접/간접적으로 연결된 모든 것) |
맥락 이해 | 단편적, 지역적 (Local) | 구조적, 전체론적 (Global & Local) |
강점 | 간단한 사실 확인, 단답형 질문 | 복합/다중 홉(multi-hop) 질문, 관계 추론, 종합적 분석 |
질의 예시 | "클라우드 컴퓨팅의 정의는?" | "프로젝트 A의 실패 원인과 관련된 핵심 인물은 누구이며, 그들의 주장은 어떠했는가?" |
GraphRAG의 내부 동작 메커니즘
GraphRAG는 BERT NER 모델과 같은 전통적인 모델을 사용할 수도 있지만, 핵심적인 강점은 BERT와 같은 특정 모델에 의존하기보다 거대 언어 모델(LLM) 자체를 활용하는 데 있다. 기존의 BERT NER 모델은 '인물', '기관' 등 미리 정해진 유형(pre-defined type)의 개체를 추출하는 데 특화되어 있다. 하지만 이 방식은 새로운 도메인이나 새로운 유형의 개체를 인식하려면 별도의 데이터로 모델을 재학습(fine-tuning)해야 하는 번거로움이 있다.
반면, GraphRAG는 LLM의 강력한 제로샷/퓨샷(Zero-shot/Few-shot) 능력을 활용한다. 즉, 별도의 학습 없이 정교하게 설계된 프롬프트(prompt)를 통해 텍스트의 맥락에 맞는 핵심 개체와 관계를 동적으로 추출한다. 이는 훨씬 유연하고 강력한 접근법으로, 단순한 '개체명'을 넘어 '개념', '사건', '주장' 등 추상적인 요소까지 추출할 수 있게 한다.
아래 의사코드는 데이터 입력부터 최종 답변 생성까지 GraphRAG의 전체 계산 과정을 단계별로 상세히 나타낸 것이다.
먼저, 지식 그래프를 구성하는 데 필요한 기본 데이터 구조를 정의한다.
// 그래프 전체를 담는 구조체
STRUCTURE Graph
Nodes: LIST<Node> // 그래프의 모든 노드 리스트
Edges: LIST<Edge> // 그래프의 모든 엣지 리스트
CommunitySummaries: MAP<CommunityID, TEXT> // 커뮤니티 ID와 요약문 매핑
END STRUCTURE
// 개체를 나타내는 노드 구조체
STRUCTURE Node
ID: TEXT (고유 식별자, 예: "스티브 잡스")
Type: TEXT (예: "인물")
Description: TEXT (LLM이 생성한 개체 요약)
SourceDocs: LIST<TEXT> // 이 노드가 언급된 원본 문서 목록
END STRUCTURE
// 관계를 나타내는 엣지 구조체
STRUCTURE Edge
SourceNodeID: TEXT
TargetNodeID: TEXT
Relationship: TEXT (예: "공동 창업하다")
Description: TEXT (LLM이 생성한 관계 요약)
Weight: FLOAT (관계의 중요도 또는 빈도)
END STRUCTURE
// LLM이 텍스트에서 추출하는 정보 단위
STRUCTURE Triple
Subject: TEXT
Predicate: TEXT
Object: TEXT
END STRUCTURE
비정형 텍스트를 입력받아 지식 그래프를 구축한다.
// 메인 인덱싱 함수
FUNCTION BuildKnowledgeGraph(documents: LIST<TEXT>) -> Graph
// 1. 그래프 초기화
graph = new Graph()
// 2. 모든 문서를 순회하며 처리
FOR EACH doc IN documents
// 2-1. 문서를 의미 있는 덩어리(chunk)로 분할
chunks = ChunkText(doc)
FOR EACH chunk IN chunks
// 2-2. LLM을 호출하여 트리플(주어, 동사, 목적어 등) 추출
triples = ExtractTriplesFromChunk(chunk)
// 2-3. 추출된 트리플을 그래프에 추가/병합
graph = AddToGraph(graph, triples, source_doc=doc)
END FOR
END FOR
// 3. 그래프 강화: 커뮤니티 탐지 및 요약
graph = DetectAndSummarizeCommunities(graph)
RETURN graph
END FUNCTION
// LLM을 통해 트리플을 추출하는 헬퍼 함수
FUNCTION ExtractTriplesFromChunk(chunk: TEXT) -> LIST<Triple>
// LLM에 전달할 프롬프트 정의
prompt = "
다음 텍스트에서 모든 중요한 개체와 그들 사이의 관계를 추출하여
(주어, 관계, 목적어) 형식의 목록으로 만들어줘.
텍스트: '{chunk}'
"
// LLM API 호출 (예: OpenAI, Gemini 등)
llm_response = LLM_API_CALL(prompt)
// LLM 응답을 파싱하여 Triple 구조체 리스트로 변환
triples = ParseResponseToTriples(llm_response)
RETURN triples
END FUNCTION
// 추출된 트리플을 그래프에 추가하는 헬퍼 함수
FUNCTION AddToGraph(graph: Graph, triples: LIST<Triple>, source_doc: TEXT) -> Graph
FOR EACH triple IN triples
// 주어(Subject)와 목적어(Object)가 그래프에 노드로 존재하는지 확인
subject_node = FindOrCreateNode(graph, triple.Subject)
object_node = FindOrCreateNode(graph, triple.Object)
// 노드의 소스 문서 정보 업데이트
AddSourceDocToNode(subject_node, source_doc)
AddSourceDocToNode(object_node, source_doc)
// 두 노드 사이에 관계(Predicate) 엣지가 존재하는지 확인
// 존재하지 않으면 새로운 엣지를 생성하고, 존재하면 가중치(Weight) 등을 업데이트
FindOrCreateEdge(graph, subject_node.ID, object_node.ID, triple.Predicate)
END FOR
RETURN graph
END FUNCTION
구축된 그래프를 바탕으로 사용자 질문에 검색 및 답변한다.
// 메인 쿼리 함수
FUNCTION QueryGraph(graph: Graph, user_question: TEXT) -> TEXT
// 1. 사용자 질문 분석 (핵심 개체 추출 및 검색 전략 결정)
analysis_result = AnalyzeQuery(user_question)
query_entities = analysis_result.entities
search_strategy = analysis_result.strategy // "local" or "global"
retrieved_context = ""
// 2. 검색 전략에 따라 컨텍스트 검색
IF search_strategy == "local"
// 지역 검색: 질문 속 개체와 관련된 서브 그래프를 탐색
subgraph_data = LocalSearch(graph, query_entities)
retrieved_context = FormatContextForLLM(subgraph_data)
ELSE IF search_strategy == "global"
// 전역 검색: 질문과 관련된 커뮤니티 요약을 검색
summaries = GlobalSearch(graph, user_question)
retrieved_context = FormatContextForLLM(summaries)
END IF
// 3. 검색된 컨텍스트를 바탕으로 최종 답변 생성
final_answer = GenerateAnswer(retrieved_context, user_question)
RETURN final_answer
END FUNCTION
// 지역 검색 헬퍼 함수
FUNCTION LocalSearch(graph: Graph, entities: LIST<TEXT>) -> LIST<Node, Edge>
relevant_subgraph = new LIST()
// 질문에서 추출된 각 개체를 시작점으로 설정
FOR EACH entity_name IN entities
start_node = FindNodeByID(graph, entity_name)
// 시작 노드와 직접 연결된 이웃 노드 및 엣지를 수집 (1-hop)
neighbors = GetNeighbors(graph, start_node)
// 수집된 정보를 relevant_subgraph에 추가
AddDataToSubgraph(relevant_subgraph, start_node, neighbors)
END FOR
RETURN relevant_subgraph
END FUNCTION
// 최종 답변 생성 헬퍼 함수
FUNCTION GenerateAnswer(context: TEXT, user_question: TEXT) -> TEXT
// LLM에 전달할 최종 프롬프트 구성
prompt = "
주어진 '컨텍스트' 정보만을 사용하여 아래 '질문'에 대해 상세히 답변해줘.
컨텍스트에 없는 정보는 언급하지 마.
[컨텍스트 시작]
{context}
[컨텍스트 끝]
[질문]
{user_question}
"
final_answer = LLM_API_CALL(prompt)
RETURN final_answer
END FUNCTION
이런 방식으로 동작하므로, BERT 기반 NER 에 비해 좀 더 범용적인 Graph RAG 가 가능한 것이다.
설치 및 초기 설정
GraphRAG를 사용하기 위한 첫 단계는 Python 환경에 관련 라이브러리를 설치하는 것이다. 터미널에서 아래 명령어를 실행하여 설치를 진행한다.
pip install graphrag
설치가 완료되면, 프로젝트를 진행할 작업 폴더를 생성하고 해당 폴더에서 다음 명령어를 실행하여 프로젝트를 초기화한다. 이 명령은 기본 설정 파일인 settings.yaml과 API 키를 관리할 .env 파일을 자동으로 생성해준다.
graphrag --init --root .
마지막으로, 생성된 .env 파일을 열어 GRAPHRAG_API_KEY="sk-..." 형식으로 자신이 사용하는 LLM의 API 키를 입력하여 설정을 완료해야 한다.
인덱싱 실행
설정이 완료되면, 분석하고자 하는 모든 텍스트 파일(.txt, .pdf 등)을 프로젝트 내 input/ 폴더에 위치시킨다. 이후, 터미널에서 아래 명령어를 실행하면 인덱싱 프로세스가 시작된다. 이 과정은 데이터의 양과 복잡성에 따라 상당한 시간이 소요될 수 있다.
명령어(CLI) 사용
graphrag --root . index
Python 코드 사용
Python 스크립트를 통해 인덱싱을 실행하고자 할 경우, 제공되는 run_pipeline_with_config 함수를 사용하여 보다 세밀한 제어가 가능하다.
import asyncio
from graphrag.index import run_pipeline_with_config
async def main():
# 현재 폴더의 설정 파일을 기준으로 인덱싱 파이프라인 실행
result = await run_pipeline_with_config(
config_or_path="./settings.yaml",
root_path=".",
verbose=True
)
if result.errors:
print("인덱싱 중 오류가 발생했습니다:", result.errors)
else:
print("인덱싱이 성공적으로 완료되었습니다.")
print(f"소요 시간 정보: {result.timing_info}")
if __name__ == "__main__":
asyncio.run(main())
쿼리 실행
인덱싱이 성공적으로 완료되면 output/ 폴더에 그래프 데이터 파일이 생성되며, 이를 대상으로 질문을 할 수 있다.
전역 검색 (Global Search): 데이터셋 전체에 대한 거시적인 질문은 아래와 같이 실행한다.
graphrag --root . query "이 데이터셋의 주요 테마는 무엇인가?"
지역 검색 (Local Search): 특정 개체에 대한 상세하고 구체적인 정보가 필요할 경우에는 --method local 옵션을 명시하여 질의를 수행한다.
graphrag --root . query --method local "등장인물 'Alice'의 역할과 주요 관계는 무엇인가?"
좀 더 상세한 사용법은 다음 공식 튜토리얼을 참고한다.
graphrag query --root ./christmas --method local --query "Who is Scrooge and what are his main relationships?" 질의 결과 예시
부록: GraphRAG 성능 병목 지점
GraphRAG는 마이크로소프트가 개발한 RAG 프레임워크로, 텍스트 데이터로부터 지식 그래프를 구축하여 심층적인 답변을 생성한다. 이 기술 노트는 GraphRAG의 구체적인 작동 방식과 그 과정에서 발생하는 성능 병목의 원인을 분석하고, 이에 대한 최적화 방안을 서술하는 것을 목적으로 한다.
GraphRAG의 작동 방식은 크게 지식 그래프를 구축하는 전체 흐름과, 그 기반이 되는 벡터 검색 기술로 나누어 이해할 수 있다.
1. 지식 그래프 구축의 전체 흐름
GraphRAG의 핵심은 여러 단계에 걸쳐 텍스트로부터 구조화된 지식을 추출하는 과정이다.
- 전처리 단계: 먼저 입력된 텍스트는 의미 있는 단위로 나뉘는 청킹(Chunking) 및 전처리 과정을 거친다.
- 정보 추출 단계: 이 단계부터 본격적인 LLM 호출이 시작된다. 전처리된 텍스트 덩어리에서 엔티티(Entity)와 그들 간의 관계(Relationship)를 추출한다. 이어서 추출된 각 엔티티에 대한 설명을 요약하고, 관련 엔티티들을 그룹화하여 커뮤니티를 탐지하고 리포트를 생성하는 과정이 순차적으로 진행된다.
- 인덱싱 단계: 추출되고 요약된 모든 정보는 임베딩 모델을 통해 벡터로 변환되며, 검색을 위해 벡터 저장소에 저장된다.
이러한 과정을 통해 원본 텍스트는 단순한 문자열이 아닌, 검색과 추론이 가능한 정교한 지식 그래프로 재탄생한다.
2. 핵심 기술: 벡터 검색과 공간 인덱싱
위 흐름의 기반에는 벡터 검색 기술이 있다. 이 과정은 텍스트를 임베딩 모델로 벡터화하고, 이를 검색이 용이하도록 공간 인덱스에 저장한 뒤, KNN/ANN 같은 알고리즘으로 유사도를 측정해 결과를 찾는 방식으로 동작한다.
이때 사용되는 공간 인덱싱 기술은 여러 종류가 있다. 트리 기반 인덱스는 R-Tree, KD-Tree, Ball Tree 등이 있으며, 공간을 계층적으로 분할하여 다차원 데이터 검색에 활용된다. 해시 기반 인덱스는 LSH나 Random Projection 기법을 통해 유사한 벡터를 같은 버킷에 묶어 근사 검색의 속도를 높인다. 그래프 기반 인덱스는 HNSW나 NSW처럼 데이터 포인트를 노드로 하는 그래프를 구성하여 정확하고 빠른 검색을 지원하며, LanceDB와 같은 최신 벡터 DB에서 주로 사용된다. 마지막으로 양자화 기반 인덱스는 IVF나 PQ 기법을 통해 벡터를 압축하거나 클러스터링하여 메모리 사용량을 획기적으로 줄이는 데 초점을 맞춘다.
GraphRAG의 실제 성능 저하는 벡터 검색 단계가 아닌, 지식 그래프 구축 과정에서 대규모 언어 모델(LLM)을 호출하는 단계에서 집중적으로 발생한다. 앞서 설명한 엔티티 및 관계 추출, 엔티티 설명 요약, 커뮤니티 리포트 생성 등 각 단계마다 LLM 호출이 반복적으로 발생하며, 이 과정이 전체 처리 시간의 대부분을 차지하는 핵심적인 병목 지점이다.
병목의 주된 원인은 두 가지로 분석된다. 첫 번째 원인은 엔티티 추출 단계에서 사용되는 프롬프트의 크기가 매우 크다는 점이다. 이 프롬프트에는 LLM의 역할, 목표, 상세한 처리 절차 등 방대한 양의 지침이 포함되며, 여기에 수 킬로바이트에 달하는 원문 텍스트가 더해진다. 이처럼 거대한 프롬프트는 LLM의 연산 부담을 가중시켜 처리 속도를 현저히 저하시킨다.
# GraphRAG의 extract_graph 프롬프트 예시
ENTITY_EXTRACTION_PROMPT = """
-Role-
You are a helpful assistant responsible for generating a comprehensive,
structured list of entities from the given text...
-Goal-
Given a text document that is potentially very long, extract all entities...
-Steps-
1. Identify all entities of the following types: {entity_types}
2. For each entity, extract...
[매우 긴 프롬프트 템플릿]
-Real Data-
{input_text} # 실제 문서 텍스트 (수KB~수MB)
"""
두 번째 원인은 다단계에 걸친 LLM 호출이 순차적으로 연결된 체인 구조를 가진다는 것이다. 실제 처리 흐름을 보면, 먼저 큰 프롬프트를 사용해 텍스트에서 엔티티를 추출하는 느린 LLM 호출이 발생한다. 그 후, 추출된 각각의 엔티티에 대해 요약을 생성하기 위해 LLM 호출이 반복적으로 일어난다. 이어서 관계 추출과 커뮤니티 리포트 생성을 위한 LLM 호출이 순차적으로 계속된다. 이러한 의존적이고 반복적인 호출 구조는 각 단계에서 발생하는 지연 시간을 누적시켜 전체적인 성능 저하를 유발한다. 또한, 쿼리 처리 시에도 빠른 벡터 검색 이후 수집된 방대한 컨텍스트를 바탕으로 최종 답변을 생성하기 위해 또다시 느린 LLM 호출이 필요하다.
# 실제 GraphRAG 처리 흐름
def process_document(text):
# 1단계: Entity 추출 (큰 프롬프트)
entities = llm_call_1(extract_prompt + text) # 느림
# 2단계: 각 Entity 요약
for entity in entities:
summary = llm_call_2(summarize_prompt + entity) # 반복 호출
# 3단계: 관계 추출
relationships = llm_call_3(relation_prompt + entities) # 느림
# 4단계: 커뮤니티 리포트
reports = llm_call_4(community_prompt + graph_data) # 느림
# 쿼리 처리 시 다단계 LLM 호출
def query_graphrag(question):
# 1. 벡터 검색 (빠름)
similar_entities = vector_search(question_embedding)
# 2. 그래프 탐색으로 관련 데이터 수집
context_data = collect_related_data(similar_entities)
# 3. 최종 답변 생성 (LLM 호출 - 느림)
answer = llm_call(context_data + question) # 큰 컨텍스트
성능 최적화 방안
GraphRAG의 성능을 최적화하기 위한 방안은 주로 병목의 원인인 LLM 호출을 효율화하는 데 집중된다.
- LLM 호출 최적화: 설정 파일에서 동시에 처리할 수 있는 요청 수를 늘려 병렬 처리를 강화하고, 비동기 방식으로 작업을 처리하여 대기 시간을 줄일 수 있다. 또한, 최대 생성 토큰 수를 제한하거나, 입력 텍스트의 청크 크기를 기본값보다 작게 조절하여 LLM의 연산 부담을 낮추는 것도 효과적인 전략이다.
- 캐시 활용: 한 번 처리된 LLM의 응답 결과를 파일 기반 캐시에 저장해두면, 동일한 입력에 대해 다시 LLM을 호출할 필요 없이 저장된 결과를 즉시 재사용할 수 있다. 이는 반복적인 데이터 처리 과정에서 불필요한 연산을 제거하여 속도를 크게 향상시킨다.
- 벡터 DB 최적화: LanceDB와 같이 HNSW 인덱스를 사용하는 고효율 벡터 저장소를 선택하고, 이미 생성된 인덱스를 덮어쓰지 않고 재활용하도록 설정하여 데이터 처리의 초기 준비 시간을 단축할 수 있다.
결론적으로 GraphRAG는 HNSW와 같은 효율적인 공간 인덱싱을 통해 벡터 검색을 수행하지만, 실제 성능은 대용량 프롬프트와 다단계 LLM 호출 체인 구조에 의해 크게 저하된다. LLM 호출이 전체 처리 시간의 대부분을 차지하므로, GraphRAG의 높은 정확도를 유지하면서 실용적인 속도를 확보하기 위해서는 캐시 활용, 병렬 처리 강화, 청크 크기 최적화와 같은 성능 개선 작업이 필수적이다.
부록: Ollama 연동 방법
다음은 GraphRAG와 Ollama의 gemma3 모델을 연동하는 설정 방법을 보여준다.
.env는 다음과 같이 설정한다.
GRAPHRAG_API_KEY=ollama
settings.yaml은 다음과 같이 설정한다.
### This config file contains required core defaults that must be set, along with a handful of common optional settings.
### For a full list of available settings, see https://microsoft.github.io/graphrag/config/yaml/
### LLM settings ###
## There are a number of settings to tune the threading and token limits for LLM calls - check the docs.
models:
default_chat_model:
type: chat # GraphRAG 호환 타입: 채팅/텍스트 생성 모델
model_provider: ollama # GraphRAG가 요구하는 필드명
model: gemma3
api_base: http://localhost:11434 # Ollama API 엔드포인트 명시
api_key: "not-required" # Ollama는 API 키가 필요 없지만 필드는 존재해야 함
model_supports_json: true
concurrent_requests: 25
async_mode: threaded
retry_strategy: native
max_retries: 10
tokens_per_minute: null
requests_per_minute: null
default_embedding_model:
type: embedding # GraphRAG 호환 타입: 임베딩 모델
model_provider: ollama # GraphRAG가 요구하는 필드명
model: nomic-embed-text
api_base: http://localhost:11434 # Ollama API 엔드포인트 명시
api_key: "not-required" # Ollama는 API 키가 필요 없지만 필드는 존재해야 함
concurrent_requests: 25
async_mode: threaded
retry_strategy: native
max_retries: 10
tokens_per_minute: null
requests_per_minute: null
### Input settings ###
input:
storage:
type: file # or blob
base_dir: "input"
file_type: text # [csv, text, json]
chunks:
size: 1200
overlap: 100
group_by_columns: [id]
### Output/storage settings ###
## If blob storage is specified in the following four sections,
## connection_string and container_name must be provided
output:
type: file # [file, blob, cosmosdb]
base_dir: "output"
cache:
type: file # [file, blob, cosmosdb]
base_dir: "cache"
reporting:
type: file # [file, blob]
base_dir: "logs"
vector_store:
default_vector_store:
type: lancedb
db_uri: output\lancedb
container_name: default
overwrite: True
### Workflow settings ###
embed_text:
model_id: default_embedding_model
vector_store_id: default_vector_store
extract_graph:
model_id: default_chat_model
prompt: "prompts/extract_graph.txt"
entity_types: [organization,person,geo,event]
max_gleanings: 1
summarize_descriptions:
model_id: default_chat_model
prompt: "prompts/summarize_descriptions.txt"
max_length: 500
extract_graph_nlp:
text_analyzer:
extractor_type: regex_english # [regex_english, syntactic_parser, cfg]
async_mode: threaded # or asyncio
cluster_graph:
max_cluster_size: 10
extract_claims:
enabled: false
model_id: default_chat_model
prompt: "prompts/extract_claims.txt"
description: "Any claims or facts that could be relevant to information discovery."
max_gleanings: 1
community_reports:
model_id: default_chat_model
graph_prompt: "prompts/community_report_graph.txt"
text_prompt: "prompts/community_report_text.txt"
max_length: 2000
max_input_length: 8000
embed_graph:
enabled: false # if true, will generate node2vec embeddings for nodes
umap:
enabled: false # if true, will generate UMAP embeddings for nodes (embed_graph must also be enabled)
snapshots:
graphml: false
embeddings: false
### Query settings ###
## The prompt locations are required here, but each search method has a number of optional knobs that can be tuned.
## See the config docs: https://microsoft.github.io/graphrag/config/yaml/#query
local_search:
chat_model_id: default_chat_model
embedding_model_id: default_embedding_model
prompt: "prompts/local_search_system_prompt.txt"
global_search:
chat_model_id: default_chat_model
map_prompt: "prompts/global_search_map_system_prompt.txt"
reduce_prompt: "prompts/global_search_reduce_system_prompt.txt"
knowledge_prompt: "prompts/global_search_knowledge_system_prompt.txt"
drift_search:
chat_model_id: default_chat_model
embedding_model_id: default_embedding_model
prompt: "prompts/drift_search_system_prompt.txt"
reduce_prompt: "prompts/drift_search_reduce_prompt.txt"
basic_search:
chat_model_id: default_chat_model
embedding_model_id: default_embedding_model
prompt: "prompts/basic_search_system_prompt.txt"
다음은 Ollama 를 이용해 GraphRAG의 벡터DB를 생성하는 과정을 보여준다.