에이전트·RAG

AGENT / 22번째 글

Graph RAG: 지식 그래프로 RAG의 한계를 극복하다

엔티티·관계 추출과 개체 해소, 구축 비용의 자릿수, 커뮤니티 요약과 Local·Global Search, 그래프 갱신의 어려움까지 — Graph RAG를 언제 쓰고 언제 쓰지 않는지 정리한다.

PALDYN Team28 MIN READ

지난 글에서 에이전트가 자율적으로 도구를 선택하는 Agentic RAG를 배웠다. 그 글은 검색을 몇 번 어떻게 돌릴지를 바꾸는 접근이었다. 이번에는 검색의 횟수가 아니라 색인의 모양을 바꾸는 쪽을 본다. 문서를 청크로 쪼개 벡터로 두는 대신, 문서에서 개체와 관계를 뽑아 그래프로 저장하는 Graph RAG다.

2024년 Microsoft Research가 발표한 GraphRAG 논문이 이 접근을 한자리에 모았다. 그 논문의 문제 제기는 명확했다 — 벡터 검색은 「이 문서에 무엇이 적혀 있나」에는 강하지만 「전체를 통틀어 무엇이 문제인가」에는 구조적으로 답할 수 없다는 것이다.

전역 질문과 관계 질문

청크가 못 담는 것

「회사 전체의 주요 리스크 요인은?」을 생각해 보자. 벡터 검색은 질문과 가까운 청크 다섯 개를 돌려준다. 그런데 이 질문의 답은 어느 한 청크에도 없다 — 수백 개 문서에 흩어진 언급을 세어 봐야 「이 주제가 자주 나온다」가 드러나기 때문이다. 검색기는 상위 k건만 주도록 만들어져 있고, k를 아무리 키워도 컨텍스트 창이 먼저 찬다.

이것이 전역 질문(global question)이다. 답이 특정 구절에 있는 것이 아니라 자료 전체의 분포에 있는 질문을 말한다. 「가장 자주 언급된 것」, 「전반적인 논조」, 「이 자료가 다루지 않는 것」이 모두 여기에 든다. 벡터 검색이 이 질문에 약한 것은 튜닝의 문제가 아니라 상위 k건을 뽑는다는 구조 자체의 성질이다.

관계의 청크 경계

두 번째로 약한 자리는 관계 질문이다. 「A의 CEO가 이전에 있던 회사는?」은 두 걸음을 밟아야 한다 — A에서 CEO의 이름을 얻고, 그 이름으로 이전 경력을 찾는다. 벡터 검색은 질문 문장 하나로 유사한 청크를 찾으므로, 두 사실이 서로 다른 문서에 있으면 한 번의 검색으로 둘 다 데려오기 어렵다.

더 나쁜 경우는 청크 경계가 관계를 자르는 것이다. 「A가 B를 인수했다. 그 뒤 B는 C와 파트너십을 맺었다」가 청크 경계에 걸리면, 앞 문장이 든 청크에는 A·B만 있고 뒤 청크에는 B·C만 있다. 두 청크를 다 가져와도 모델이 B가 같은 B라는 것을 확신할 근거가 청크 안에 없다. 청크 전략에서 겹침(overlap)을 두는 이유가 여기인데, 겹침은 인접한 문장만 살릴 뿐 문서를 건너뛰는 관계는 못 살린다.

Vector RAG vs Graph RAG

트리플과 그래프

Graph RAG는 이 둘을 색인의 모양으로 푼다. 문서에서 트리플(triple) — (주어, 관계, 목적어) 형태의 사실 한 조각 — 을 뽑아 그래프로 쌓는다. 트리플의 주어와 목적어가 그래프의 노드이고 관계가 엣지다.

(삼성전자, CEO, 이재용)
(이재용, 소속, DS사업부)
(DS사업부, 주력제품, 반도체)
(반도체, 경쟁사, TSMC)

이렇게 쌓아 두면 앞의 관계 질문이 검색 문제가 아니라 탐색 문제가 된다. 「이재용과 두 걸음 안에 연결된 것 전부」는 그래프에서 정확히 정의되는 연산이고, 결과에 빠짐이 없다. 벡터 검색이 「비슷한 것을 대략」 가져오는 것과 성질이 다르다.

Graph RAG 파이프라인

엔티티와 관계 추출

그래프는 공짜로 생기지 않는다. 문서에서 트리플을 뽑는 단계가 Graph RAG의 전부라 해도 될 만큼 크고, 여기서 정해지는 것이 나중에 답할 수 있는 질문의 범위를 정한다.

스키마 고정과 자유 추출

첫 갈림길은 노드와 관계의 종류를 미리 정할 것인가다.

스키마를 고정하면 — 노드는 Person·Company·Product 셋, 관계는 CEO·ACQUIRED·COMPETES_WITH 셋으로 못 박으면 — 그래프가 깔끔해지고 쿼리를 쓰기 쉬워진다. 대신 스키마에 없는 사실은 통째로 버려진다. 규제 문서에서 「X 조항이 Y 조항을 대체한다」가 중요한데 관계 목록에 SUPERSEDES가 없으면 그 사실은 그래프에 안 들어간다.

자유롭게 뽑게 두면 놓치는 것은 적지만 같은 관계가 다른 이름으로 수십 개 생긴다. CEO·대표이사·is_ceo_of·leads가 전부 따로 서면 쿼리 하나로는 절반밖에 못 찾는다.

실무의 절충은 두 번 도는 것이다. 표본 문서 수십 건을 자유 추출로 돌려 어떤 관계가 실제로 나오는지 보고, 거기서 상위 열 개 남짓을 골라 스키마로 못 박은 뒤 전체를 다시 돌린다. 처음부터 스키마를 짐작으로 정하면 대개 도메인에 안 맞는다.

개체 해소

가장 자주 그래프를 망가뜨리는 자리다. 개체 해소(entity resolution)는 서로 다르게 적힌 표현이 같은 대상을 가리키는지 판정하는 일을 말한다. 「삼성전자」·「삼성전자㈜」·「Samsung Electronics」가 세 노드로 서면, 「삼성전자와 연결된 것 전부」가 셋으로 쪼개져 어느 것을 물어도 답이 3분의 1이 된다.

해소는 세 단계로 한다. 정규화 — 공백·법인격 표기·대소문자를 통일한다. 이것만으로 절반 넘게 붙는다. 별칭 사전 — 도메인에서 아는 이름 짝을 손으로 적어 둔다. 사내 문서라면 부서 약칭이 여기 들어간다. 임베딩 유사도 — 이름 벡터가 가까운 짝을 후보로 올려 사람이 확인한다. 세 번째를 자동으로 확정하면 안 된다. 「LG전자」와 「LG화학」은 이름이 매우 가깝지만 다른 회사다.

추출 품질

추출이 잘됐는지는 어떻게 아는가. 눈으로 그래프를 보는 것은 규모가 조금만 커져도 안 된다. 실무에서 쓰는 신호가 셋 있다.

고아 노드 비율 — 엣지가 하나도 없는 노드가 많으면 관계 추출이 실패하고 있다는 뜻이다. 관계 종류의 분포 — 상위 세 종류가 전체의 90%를 넘으면 나머지 관계를 못 뽑고 있는 것이다. 표본 대조 — 문서 스무 건을 골라 사람이 트리플을 적고, 그래프에 들어간 것과 맞춰 본다. 앞의 둘은 자동으로 세어지고 마지막 하나가 실제 재현율을 알려 준다.

from langchain_experimental.graph_transformers import LLMGraphTransformer
from langchain_neo4j import Neo4jGraph
from langchain_anthropic import ChatAnthropic
from langchain_community.document_loaders import TextLoader

graph = Neo4jGraph(url="bolt://localhost:7687", username="neo4j", password="password")

llm = ChatAnthropic(model="claude-sonnet-4-6", temperature=0)
transformer = LLMGraphTransformer(
    llm=llm,
    # 표본 추출로 실제 나오는 관계를 본 뒤에 못 박는다
    allowed_nodes=["Person", "Company", "Product", "Technology"],
    allowed_relationships=["CEO", "ACQUIRED", "PRODUCES", "COMPETES_WITH"],
)

docs = TextLoader("company_reports.txt").load()
graph_docs = transformer.convert_to_graph_documents(docs)
graph.add_graph_documents(graph_docs)

구축 비용

Graph RAG를 검토하다 접는 이유는 대개 성능이 아니라 여기다. 색인을 만드는 비용이 벡터 색인과 자릿수가 다르다.

호출 수

문서 1,000건이 있고 문서 하나가 평균 다섯 청크로 나뉜다고 하자. 벡터 색인은 청크 5,000개를 임베딩 모델에 한 번씩 넣으면 끝이다. 임베딩은 생성 모델보다 훨씬 싸고, 배치로 묶어 보낼 수 있다.

Graph RAG는 같은 청크 5,000개를 생성 모델에 한 번씩 넣어 트리플을 뽑아야 한다. 청크 하나가 500토큰이고 뽑힌 트리플이 200토큰이면 호출당 700토큰, 5,000회면 350만 토큰이다. 여기에 커뮤니티 요약이 더 붙는다. 개체 해소에 모델을 쓰면 또 붙는다.

정확한 배수는 모델과 문서에 따라 달라지지만, 자릿수는 기억해 둘 만하다 — 같은 문서를 벡터로 색인하는 비용과 그래프로 색인하는 비용은 대개 한두 자릿수 차이가 난다. 그리고 이 비용은 문서가 늘면 선형으로 는다.

시간도 함께 봐야 한다. 임베딩은 배치로 수백 개씩 묶어 보낼 수 있어 5,000청크가 몇 분에 끝나지만, 트리플 추출은 청크마다 생성 호출이 하나씩이라 동시 요청 수 제한에 걸린다. 열 개씩 병렬로 돌려 호출당 3초가 걸리면 5,000회에 25분이고, 여기에 재시도와 실패 청크가 붙는다. 색인이 몇 시간짜리 작업이 된다는 것이 운영에서 먼저 부딪히는 벽이다 — 파라미터를 하나 고쳐 다시 돌려 보는 일이 하루에 몇 번 안 된다는 뜻이기 때문이다.

비용을 줄이는 수

세 가지가 실제로 듣는다.

추출 모델을 작은 것으로 내린다. 트리플 추출은 자유로운 생성이 아니라 정해진 형식으로 뽑는 일이라, 생성 품질보다 형식 준수가 중요하다. 작은 모델로도 상당 부분이 되고, 여기서 절감이 가장 크다.

전부 그래프로 만들지 않는다. 관계 질문이 실제로 오는 문서 종류가 정해져 있는 경우가 많다. 조직도·계약서·규정처럼 관계가 촘촘한 자료만 그래프로 만들고 나머지는 벡터로 두면, 색인 비용이 자료 비중만큼만 든다.

청크를 크게 잡는다. 추출에 쓰는 청크는 검색에 쓰는 청크보다 커도 된다. 오히려 커야 한 청크 안에서 관계가 완결되어 트리플이 잘 나온다. 청크를 두 배로 잡으면 호출 수는 절반이 된다.

커뮤니티 요약

Microsoft GraphRAG가 단순한 엔티티 그래프에서 한 걸음 더 간 자리가 여기다. 그래프만으로는 앞서 본 전역 질문이 여전히 안 풀리기 때문이다 — 노드가 수만 개면 「전체의 주요 리스크」를 물어도 어느 노드에서 출발할지 정할 수가 없다.

Leiden 군집

그래서 그래프를 커뮤니티로 묶는다. 커뮤니티는 서로 촘촘히 연결된 노드 무리를 말하고, GraphRAG는 이 무리를 찾는 데 Leiden 알고리즘을 쓴다. 「반도체 사업 관련 노드들」, 「해외 규제 관련 노드들」처럼 자연스러운 주제 덩어리가 나온다. 이 묶기는 사람이 주제 목록을 미리 정하는 것이 아니라 연결의 밀도에서 자동으로 나온다는 점이 중요하다.

계층 요약

Leiden은 계층을 만든다. 큰 커뮤니티가 작은 커뮤니티들로 나뉘고, 그것이 또 나뉜다. GraphRAG는 바닥 층부터 요약을 만들어 위로 올린다 — 작은 커뮤니티의 트리플을 모아 요약하고, 그 요약들을 모아 상위 커뮤니티의 요약을 만든다. 색인 단계에서 이 요약을 전부 만들어 저장해 둔다.

커뮤니티 요약의 계층 — 바닥 층부터 요약을 만들어 위로 올린다

전역 질문의 답

전역 질문이 들어오면 상위 커뮤니티 요약들을 꺼내 각각에 질문을 던지고, 나온 부분 답들을 하나로 합친다. 맵-리듀스와 같은 모양이다.

이 구조가 전역 질문에 답이 되는 이유는 분명하다. 자료 전체를 한 번 다 읽고 만든 것이 커뮤니티 요약이기 때문이다. 벡터 검색이 질문 시점에 상위 k건만 보는 것과 달리, 요약은 색인 시점에 전부를 봤다. 비싼 이유와 전역 질문에 답할 수 있는 이유가 같은 것이다.

어느 계층의 요약을 꺼낼지는 조절할 수 있고, 이것이 Global Search에서 실제로 만지는 손잡이다. 위 계층만 쓰면 요약이 몇 개뿐이라 호출이 적고 답이 뭉뚱그려지며, 아래 계층까지 내려가면 요약 수가 수백 개로 늘어 비용과 지연이 함께 오른다. 「이 자료가 무엇에 대한 것인가」에는 위 계층이면 충분하고, 「어떤 리스크가 어느 사업부에 걸려 있는가」처럼 갈래가 필요한 질문은 한두 층 내려가야 답이 나온다.

그리고 요약은 원문이 아니라 모델이 다시 쓴 문장이다. 계층을 올라갈수록 원문에서 멀어지고, 아래 층의 요약 열 개를 합쳐 만든 위 층 요약에는 숫자와 고유명이 지워져 있기 마련이다. Global Search의 답에 출처를 붙이기 어려운 이유가 여기이고, 뒤에 볼 오라우팅 회복이 필요한 이유도 여기다.

# graphrag init --root ./ragtest        # 설정 파일 생성
# graphrag index --root ./ragtest       # 그래프 + 커뮤니티 요약까지 한 번에

# 특정 엔티티 중심 — 서브그래프를 따라간다
graphrag query --root ./ragtest --method local  --query "이재용의 경영 전략은?"

# 전체 조망 — 커뮤니티 요약을 맵-리듀스로 합친다
graphrag query --root ./ragtest --method global --query "회사 전체의 주요 리스크는?"

두 경로

이름이 비슷해서 헷갈리기 쉬운데, 둘은 읽는 대상이 아예 다르다.

Local Search Global Search
출발점 질문에 나온 엔티티 커뮤니티 요약 전체
읽는 것 그 노드 주변 서브그래프와 원문 상위 계층 요약들
비용 벡터 RAG와 비슷 요약 수만큼 호출이 는다
잘 푸는 질문 「누가·언제·얼마」 「전반적으로·주로·가장 많이」

Local 쪽에는 구현이 두 갈래 있다. 그래프 질의를 모델이 짜게 하는 방식은 자연어 질문을 Cypher 같은 그래프 질의로 옮겨 그대로 실행한다. 질문의 모양에 제한이 없다는 것이 장점이고, 스키마에 없는 이름을 지어내거나 방향이 거꾸로인 관계를 쓰는 질의가 나온다는 것이 단점이다. 실패하면 결과가 0건으로 나오지 실행 오류로 나오지 않아, 「자료에 없다」와 구별이 안 된다.

탐색을 코드로 고정하는 방식은 질문에서 엔티티만 뽑고 그 노드에서 두 걸음 이내를 가져오는 식으로 경로를 못 박는다. 지어낼 자리가 없어 안정적이지만 답할 수 있는 질문의 모양이 좁다. 처음에는 뒤쪽으로 시작해 실제로 안 풀리는 질문이 쌓이면 앞쪽을 얹는 순서가 안전하다.

라우팅 판단

질문이 오면 어느 쪽으로 보낼지 정해야 한다. 판단의 기준은 하나다 — 답이 특정 개체에 붙어 있는가, 자료의 분포에 있는가.

실무에서는 세 방법을 겹쳐 쓴다. 엔티티 매칭 — 질문에서 뽑은 이름이 그래프의 노드와 맞으면 Local이다. 가장 싸고 대부분을 가른다. 어휘 신호 — 「전반적으로」·「주요한」·「가장 많이」 같은 말이 들어가면 Global 쪽이다. 분류기 — 앞 둘로 안 갈리는 것만 작은 모델에게 묻는다.

오라우팅 회복

라우팅은 틀린다. 중요한 것은 틀렸을 때 알아채는가다.

Local로 보냈는데 출발 노드를 못 찾으면 그래프 탐색이 빈손으로 끝나고, 이건 코드에서 바로 감지된다 — 결과가 0건이면 Global로 다시 보낸다. 반대 방향이 어렵다. Global은 언제나 무언가를 답하기 때문에 틀렸다는 신호가 안 나온다. 「이 회사의 2025년 매출은?」을 Global로 보내면 요약들에서 그럴듯한 문장을 뽑아 오지만 정확한 숫자는 안 나올 수 있다.

그래서 Global 경로에는 답에 구체적인 수치나 고유명이 요구되는지를 한 번 보는 층을 둔다. 요구되는데 근거로 쓴 것이 요약뿐이라면 Local로 다시 태운다. RAG 평가에서 다루는 근거 충실도가 이 판정에 그대로 쓰인다.

그래프 갱신

한 문서의 파급

문서 한 건이 바뀌면 무엇을 다시 만들어야 하는가. 벡터 색인이라면 답이 간단하다 — 그 문서의 청크만 다시 임베딩해 갈아 끼운다. 다른 문서에 영향이 없다.

그래프는 다르다. 바뀐 문서에서 나온 트리플이 지워지거나 더해지면, 그 트리플이 걸려 있던 노드의 연결이 바뀐다. 연결이 바뀌면 그 노드가 속한 커뮤니티가 바뀔 수 있고, 커뮤니티가 바뀌면 그 커뮤니티의 요약과 그 위 계층의 요약이 전부 낡은 것이 된다. 한 문서의 변경이 그 문서와 상관없는 요약까지 무르게 만든다.

증분 갱신의 어려움

그렇다고 매번 전체를 다시 만들 수도 없다. 앞 절에서 본 비용이 바뀔 때마다 다시 드는 셈이기 때문이다. 증분 갱신이 어려운 이유는 군집이 전역 연산이라는 데 있다 — Leiden은 그래프 전체의 연결 밀도를 보고 무리를 나누므로, 엣지 몇 개가 바뀌었을 때 어느 커뮤니티가 영향을 받는지가 국소적으로 정해지지 않는다.

재구축 주기

그래서 현실적인 운영은 층을 나누는 것이다. 트리플과 노드는 바뀐 문서에 대해 바로 갱신한다 — Local Search는 이것만으로 최신이 된다. 커뮤니티와 요약은 주기적으로 통째로 다시 만든다. 주기는 자료가 얼마나 자주 바뀌는지가 정하는데, 사내 문서라면 주 단위, 규정처럼 드물게 바뀌는 자료라면 분기 단위가 흔하다.

이 구조를 받아들이면 한 가지를 인정하게 된다. Global Search의 답은 마지막 재구축 시점의 답이다. 「최근 한 달의 전반적인 동향」 같은 질문을 Graph RAG에 물으면 안 되는 이유가 여기다. 신선도가 중요한 질문은 RAG 신선도 관리 쪽 수단으로 푼다.

하이브리드와 안 맞는 자리

질문 유형 라우팅

현업에서 Graph RAG를 단독으로 쓰는 경우는 드물다. 대부분은 벡터 RAG를 기본으로 두고 관계 질문과 전역 질문만 그래프로 보낸다. 앞의 라우팅이 두 갈래에서 세 갈래로 늘어나는 셈이다 — 벡터, Local, Global.

라우팅 비용을 아끼는 수가 하나 있다. 벡터를 먼저 돌리고 결과가 부실할 때만 그래프로 넘기는 것이다. 단일 사실 조회가 질문의 대부분이라면 이 순서가 평균 지연과 비용에서 유리하다.

순위 합치기

두 경로를 다 돌려 결과를 합칠 때는 문제가 하나 생긴다. 벡터 검색은 유사도 점수를 주고 그래프 탐색은 점수를 안 준다 — 「두 걸음 안에 연결됨」은 참·거짓이지 정도가 아니다. 두 결과를 점수로 섞을 수가 없다.

실무의 해법은 점수를 섞지 않고 순위만 섞는 것이다. 각 경로에서 나온 순위로 RRF 같은 순위 결합을 쓰거나, 아예 섞지 않고 「그래프에서 나온 사실」과 「문서에서 나온 근거」를 프롬프트에 따로 붙인다. 뒤쪽이 더 자주 쓰인다 — 모델이 둘을 다른 성격의 근거로 읽고, 답변에서 출처를 갈라 적을 수 있기 때문이다.

쓰지 말 곳

마지막으로, Graph RAG가 손해인 자리 셋이다.

관계가 드문 비정형 텍스트 — 상담 기록, 리뷰, 자유 서술 보고서에서는 뽑히는 트리플이 적고 그마저 품질이 낮다. 고아 노드 비율을 재 보면 바로 드러난다.

문서가 자주 바뀌는 도메인 — 앞 절에서 본 재구축 비용을 자주 치러야 한다. 뉴스나 실시간 자료가 여기다.

질문이 대부분 단일 사실 조회인 경우 — 사내 FAQ나 제품 문서 검색은 벡터 RAG로 충분하고, 그래프를 얹으면 색인 비용만 늘고 답은 그대로다. 검토를 시작하기 전에 실제 질문 로그에서 관계 질문과 전역 질문의 비중을 세어 보는 것이 가장 확실한 판단 근거다. 그 비중이 한 자릿수 퍼센트면 그래프는 아직 이르다.


읽어주셔서 감사합니다. 😊

LATEST

에이전트·RAG의 최신 글

에이전트·RAG2026.08.23

에이전트는 어디서 어긋나기 시작하는가

에이전트가 이상한 답을 낼 때 증상은 마지막에 보이지만 어긋난 자리는 그보다 앞입니다. 한 걸음이 무너지는 여섯 자리를 나누고, 증상에서 원인을 거슬러 찾는 방법과 자리별 처방을 정리합니다.

11 MIN
에이전트·RAG2026.08.23

에이전트가 무엇을 했는지 나중에 알 수 있게 만들기

에이전트는 같은 입력에도 다른 경로로 갑니다. 그래서 로그 몇 줄로는 왜 그렇게 됐는지 복원이 안 됩니다. 트레이스를 어떻게 나누고 구간마다 무엇을 붙이며 어떤 지표를 볼지 정리합니다.

11 MIN
에이전트·RAG2026.08.23

에이전트 비용은 걸음 수보다 빨리 늘어난다

걸음이 두 배면 비용은 두 배가 아니라 서너 배입니다. 왜 그렇게 되는지, 어디서 새는지, 상한을 몇 겹으로 어떻게 거는지와 실제로 효과가 큰 순서대로의 대응을 정리합니다.

13 MIN