Graph RAG는 문서에서 엔티티(개체)와 관계를 추출하여 지식 그래프를 구축하고, 이 그래프 구조를 검색에 활용하는 RAG 아키텍처입니다. 기존 RAG가 개별 문서 청크를 검색하는 반면, Graph RAG는 엔티티 간의 관계와 커뮤니티 구조를 활용하여 문서 전체에 걸친 질문에도 답변할 수 있습니다.
기존 벡터 검색 기반 RAG는 “이 데이터셋의 주요 주제는 무엇인가?”와 같은 전체 문서에 대한 요약형 질문(global sensemaking question)에 취약합니다. 개별 청크에는 전체 맥락이 담기지 않기 때문입니다.Graph RAG는 이 문제를 두 단계로 해결합니다.
1
인덱싱: 지식 그래프 구축
LLM으로 문서에서 엔티티와 관계를 추출하고, 그래프를 구축한 뒤, 커뮤니티 탐지 알고리즘으로 관련 엔티티를 그룹화하여 각 커뮤니티의 요약을 미리 생성합니다.
2
검색: 그래프 기반 질의 응답
질문 유형에 따라 Local Search(특정 엔티티 주변 탐색) 또는 Global Search(커뮤니티 요약 기반 종합 답변)를 수행합니다.
아래 Global/Local Search 구분은 Edge et al. (2024) 원 논문과 Microsoft GraphRAG 구현을 기반으로 합니다. 원 논문은 주로 Global Search (QFS, Query-Focused Summarization) 를 중심으로 기여를 주장하며, Local Search는 Microsoft의 제품 구현에서 확장된 것입니다.
from langchain.chat_models import init_chat_modelfrom langchain_core.prompts import ChatPromptTemplatefrom langchain_core.output_parsers import StrOutputParserllm = init_chat_model("gpt-4o-mini", temperature=0)def classify_question(state: GraphRAGState) -> GraphRAGState: """질문이 Global(전체 요약)인지 Local(특정 엔티티)인지 분류합니다.""" question = state["question"] prompt = ChatPromptTemplate.from_messages([ ("system", ( "질문의 유형을 분류하세요.\n" "- global: 전체 데이터에 대한 요약, 주제, 트렌드 질문\n" "- local: 특정 인물, 조직, 개념에 대한 구체적 질문\n" "'global' 또는 'local'만 출력하세요." )), ("human", "{question}"), ]) chain = prompt | llm | StrOutputParser() search_type = chain.invoke({"question": question}) return {"search_type": search_type.strip().lower()}
Copy
def global_search(state: GraphRAGState) -> GraphRAGState: """커뮤니티 요약을 기반으로 전체적인 답변을 생성합니다.""" question = state["question"] # 미리 생성된 커뮤니티 요약을 로드 (의사코드 — 실제 구현에서는 DB 조회) community_summaries = load_community_summaries() # 의사코드 # Map: 각 커뮤니티 요약에서 부분 답변 생성 partial_answers = [] for summary in community_summaries: prompt = ChatPromptTemplate.from_messages([ ("system", ( "커뮤니티 요약을 참고하여 질문에 관련된 정보를 추출하세요.\n" "관련 정보가 없으면 '해당 없음'이라고 답하세요.\n\n" "커뮤니티 요약: {summary}" )), ("human", "{question}"), ]) chain = prompt | llm | StrOutputParser() answer = chain.invoke({"summary": summary, "question": question}) if "해당 없음" not in answer: partial_answers.append(answer) context = "\n\n".join(partial_answers) return {"context": context, "community_summaries": community_summaries}
Copy
def local_search(state: GraphRAGState) -> GraphRAGState: """관련 엔티티와 서브그래프를 탐색하여 답변 컨텍스트를 구성합니다.""" question = state["question"] # 1. 질문에서 관련 엔티티 식별 entity_prompt = ChatPromptTemplate.from_messages([ ("system", "질문에서 핵심 엔티티(인물, 조직, 개념 등)를 추출하세요. 쉼표로 구분하여 출력하세요."), ("human", "{question}"), ]) chain = entity_prompt | llm | StrOutputParser() entities_text = chain.invoke({"question": question}) entity_names = [e.strip() for e in entities_text.split(",")] # 2. 그래프에서 관련 엔티티와 이웃 탐색 (의사코드 — 실제 구현에서는 Neo4j 등 사용) related_info = search_graph_neighbors(entity_names) # 의사코드 # 3. 관련 텍스트 청크 수집 (의사코드) related_chunks = retrieve_chunks_by_entities(entity_names) # 의사코드 context = f"엔티티 관계:\n{related_info}\n\n관련 문서:\n{related_chunks}" return {"context": context, "entities": entity_names}
# Global 질문result = app.invoke({"question": "이 데이터셋의 주요 주제와 트렌드는 무엇인가?"})# Local 질문result = app.invoke({"question": "A 회사와 B 기관의 협력 관계는?"})print(result["generation"])
Graph RAG의 인덱싱 단계에서는 모든 문서 청크에 대해 LLM으로 엔티티/관계를 추출하고, 커뮤니티별 요약을 생성해야 합니다. 이로 인해 인덱싱 비용이 벡터 RAG 대비 크게 증가합니다. 대규모 문서에 적용할 때는 비용을 사전에 추정하세요.