기술

그래프가 찾은 걸 벡터는 모릅니다 — 동시에 돌리면 안 되는 이유 (상황 + 반전형)

2026.04.2711분 읽기

지금 우리 RAG는 두 검색을 동시에 돌립니다

지금 우리 RAG 파이프라인은 이렇게 동작합니다. 사용자 질문이 들어오면 벡터 검색과 그래프 검색을 동시에 병렬로 실행하고, 각각의 결과를 받아서 합칩니다.

질의 → ┌─ 벡터 검색 (문서)   ─┐
        │                    │ → 병합 → 답변
        └─ 그래프 검색 (관계) ─┘

문제는 둘이 서로의 결과를 모른다는 거예요. 예를 들어 "결제팀 시스템 장애가 어디까지 영향을 미치나?"라는 질문이 들어왔다고 해볼게요. 그래프 검색이 "결제팀 시스템은 물류팀 시스템과 정산팀 시스템에 의존한다"는 관계를 찾아냈다고 친다면, 벡터 검색은 그 결과를 모르니까 자기가 따로 찾은 결제팀 관련 문서만 가져옵니다. 정작 영향받을 물류팀·정산팀 문서는 안 들어와요.

그래프가 답의 윤곽을 잡았는데, 벡터가 그 윤곽 안의 구체적 정보를 못 채우는 상황입니다. 답변이 "관계는 있는데 디테일이 비어 있는" 형태로 나와요. 사용자는 "그래서 물류팀 쪽에선 뭘 봐야 해?"를 다시 물어봐야 합니다.


Microsoft GraphRAG는 순서대로 돌리고 있었습니다

GraphRAG 관련 자료를 보다가 Microsoft가 공개한 GraphRAG 구조를 보게 됐어요. 자료를 들여다보니 우리처럼 병렬이 아니라 순차 구조더라고요.

질의 → 엔티티 링킹 → 서브그래프 확장 → 타겟 검색 → 답변

각 단계가 다음 단계의 입력이 됩니다. 엔티티 링킹은 질문에서 "결제팀 시스템"이라는 단어를 그래프 노드로 매핑하는 단계예요. 서브그래프 확장은 그 노드를 시작점으로 2~3홉 안의 관계를 따라가서 연관 노드를 모으는 단계입니다. 타겟 검색은 그렇게 뽑힌 노드들의 이름을 다시 검색어로 써서 벡터 검색을 돌리는 단계고요.

이 흐름의 핵심은 "그래프가 먼저 답의 윤곽을 만들고, 벡터가 그 윤곽을 채운다"는 거였어요. 윤곽이 먼저 잡혀 있으니까 벡터는 헛다리 짚을 일이 없거든요.

비슷한 패턴은 다른 GraphRAG 사례에서도 자주 보이는 구조라고 합니다. 우리도 이 원리를 적용하기로 했어요.

1.jpeg


1단계 — 엔티티 링킹은 이미 만들어두었습니다

엔티티 링킹은 질문에서 등장하는 단어를 그래프의 실제 노드와 연결하는 작업이에요. "결제팀 시스템"이라고 사용자가 입력하면, 그게 그래프의 어떤 노드에 해당하는지 매핑하는 거죠. 이건 이전에 별도로 구현해두었어요. 시스템 별명·약어·동의어를 같이 인덱싱해서, 질문 표현이 그래프 노드 이름과 정확히 일치하지 않아도 매핑이 됩니다.

여기서 중요한 점은 링킹이 실패하면 순차 흐름을 안 탄다는 거예요. 사용자가 "어제 뭐했지?" 같은 그래프와 무관한 질문을 했을 때, 굳이 서브그래프를 만들 필요가 없거든요. 이런 경우엔 기존 병렬 방식으로 폴백합니다.

링킹 성공 + 의도가 chitchat 아님  → 순차 파이프라인

그 외 → 기존 병렬 방식 (폴백)

이 분기 덕분에 "순차 도입했는데 잡담 질문에서 이상해졌다" 같은 부작용이 없어요.


2단계 — 서브그래프 추출, 의도에 따라 탐색을 제한합니다

서브그래프 추출은 링킹된 노드를 시작점으로 BFS(너비 우선 탐색)으로 2~3홉 안의 이웃을 모으는 단계예요. 그런데 그냥 모든 관계를 다 따라가면 노드가 폭발합니다. 6만 건짜리 그래프에서 2홉만 풀어도 수천 노드가 나오거든요.

해법은 두 가지였어요. 질문 의도에 따라 탐색할 관계를 제한하는 것, 그리고 허브 노드는 더 확장하지 않는 것.

질문 의도부터 보면 이렇습니다.

의도

키워드

탐색 관계

방향

영향 분석

장애, 영향, 전파, 다운

DEPENDS_ON, AFFECTS, RUNS_ON

양방향

3

구조 파악

뭐야, 설명, 구조

INCLUDES, RUNS_ON, DEPENDS_ON

정방향

2

담당자

담당, 누가, 운영

MANAGES, BELONGS_TO

양방향

2

일반

나머지

전체

양방향

1

"장애 영향" 질문에서 MANAGES(담당자) 관계까지 따라갈 필요는 없거든요. 의도에 맞는 관계 타입만 탐색하면 노드 수가 확 줄어듭니다.

허브 방어는 또 다른 문제를 잡아요. 그래프에는 degree가 매우 큰 노드들이 있어요. 공통 DB라든지, 모든 시스템이 의존하는 라이브러리 같은 것들이요. 이런 노드를 만나면 거기서 또 수백 개 이웃으로 퍼져나갑니다. 그래서 degree가 일정 임계값(예: 50)을 넘는 노드는 관계만 기록하고 더 확장하지 않도록 해뒀어요.

if degree(neighbor) < HUB_DEGREE:
    queue.append((neighbor, dist + 1))
# 그 외에는 큐에 안 넣고 거기서 멈춤

의도 제한 + 허브 방어 두 가지를 같이 적용하니까 서브그래프가 50노드 이내로 안정적으로 나오더라고요.

2.jpeg


3단계 — 서브그래프 텍스트를 LLM 컨텍스트로 주입합니다

서브그래프를 만들었으면 그걸 LLM이 읽을 수 있는 텍스트로 바꿔야 합니다. 그래프 노드와 엣지를 그대로 JSON으로 던지는 것보다, 사람이 읽는 형태로 정리해서 주는 게 LLM 이해도가 훨씬 좋더라고요.

## 관련 구조 (질문 대상 엔티티 중심)

[System] 결제팀 시스템
  - DEPENDS_ON → [System] 물류팀 시스템
  - INCLUDES_PROCESS → [Process] payment_was, payment_batch
  - 소속: [Domain] 정산도메인

[Process] payment_was
  - RUNS_ON → [Host] 물류서버-A, 물류서버-B

[System] 물류팀 시스템 (결제팀의 의존 대상)
  - INCLUDES_PROCESS → [Process] logistics_was
  - 소속: [Domain] 물류도메인

이 텍스트를 답변 생성 프롬프트의 앞부분에 붙입니다. LLM이 답변을 만들기 전에 구조를 먼저 이해한 상태가 되거든요. "결제팀에 장애가 나면 어디까지 영향이 가나?"라는 질문에 답할 때, 이미 머릿속에 결제팀-물류팀-정산팀의 의존 관계가 들어와 있는 상태인 거예요.

이 단계가 들어가니까 할루시네이션이 줄어드는 효과도 같이 나왔어요. 그래프에 명시된 관계가 컨텍스트에 들어 있으니까, LLM이 "잘 모르겠는데 그럴듯하게 답하기" 쪽으로 빠질 여지가 줄어듭니다.


4단계 — 타겟 검색은 서브그래프 노드 이름을 검색어로 씁니다

마지막이 타겟 검색이에요. 서브그래프에 등장한 엔티티 이름들을 검색어로 다시 써서 벡터 검색을 돌립니다.

# 기존 검색
knowledge_results = search_knowledge(query, ...)

# 서브그래프 기반 추가 검색
if state.get("subgraph"):
    for ent in subgraph["nodes"][:5]:
        per_entity = search_knowledge(ent["name"], ..., top_k=3)
        knowledge_results.extend(per_entity)

서브그래프 상위 5개 엔티티에 대해 각각 벡터 검색을 추가로 돌려서 결과에 병합하는 구조예요. "결제팀 장애 영향" 질문에서 그래프가 결제팀-물류팀-정산팀 노드를 뽑았으면, 결제팀 문서뿐 아니라 물류팀·정산팀 관련 문서까지 같이 들어옵니다.

원래 병렬 구조에서는 이 부분이 비어 있었어요. 그래프가 찾은 노드를 벡터가 모르니까, 사용자가 두 번 세 번 질문을 던지면서 정보를 채워야 했거든요. 타겟 검색이 들어가니까 한 번에 답변이 완성됩니다.

엔티티별로 각각 검색하니까 호출 비용이 늘어나는 트레이드오프가 있어서, 상위 5개로 제한하고 있어요. 더 늘릴 수도 있지만 비용 대비 품질 향상은 5개에서 거의 평탄해지더라고요.


데이터 쪽이 받쳐줘야 효과가 나옵니다

설계는 깔끔하게 잡혔는데, 효과가 드러나려면 그래프에 관계가 충분히 쌓여 있어야 합니다. 특히 영향 분석 의도에서 핵심인 DEPENDS_ON 관계가 부족하면 서브그래프가 비어 있는 상태로 나와요.

지금 우리 그래프 상태는 이래요.

관계

현재

목표

DEPENDS_ON

4건

20건+

INCLUDES_PROCESS

45건

유지

RUNS_ON

75건

유지

AFFECTS

4건

10건+

MANAGES

0건

추후

DEPENDS_ON이 4건인 상태에서는 영향 분석 의도가 사실상 동작을 안 합니다. 시작점에서 한두 홉 가다가 끝나거든요. 마침 담당자 엑셀에 시스템 의존 정보가 정리되어 있어서, 그걸 임포트하면서 DEPENDS_ON 관계를 추가로 채울 예정입니다. MANAGES는 직전 글에서 이메일 데이터로 44건을 추론해서 채워뒀어요.

설계와 데이터, 둘 다 받쳐줘야 효과가 드러나는 작업이에요. 둘 중 하나만 있으면 "구조는 잘 만들었는데 막상 결과가 비어 있는" 상황이나 "데이터는 많은데 활용이 안 되는" 상황이 됩니다.


마무리

병렬 구조는 빠르지만 두 검색이 서로를 모릅니다. 순차 구조는 한 단계가 다음 단계의 입력이 되어서 답변의 일관성이 올라가는 대신 응답 시간이 늘어나요. 우리는 엔티티 링킹이 성공한 경우에만 순차 흐름을 타고, 실패하면 기존 병렬로 폴백하는 하이브리드로 갔습니다.

엔티티 링킹 → 의도별 서브그래프 추출 → LLM 컨텍스트 주입 → 타겟 벡터 검색 4단계로 정리했어요. 노드 폭발은 의도 기반 관계 제한과 허브 방어로 막았고요. 효과는 DEPENDS_ON 관계 데이터가 더 채워진 뒤에 본격적으로 측정할 예정입니다.

벡터·그래프 병렬 RAG를 운영 중이고 답변에서 "관계는 잡았는데 디테일이 비어 있는" 패턴이 보인다면, 두 검색을 순차로 묶는 구조를 검토해보시는 걸 추천합니다.