GraphRAGとは何か:RAGと知識グラフを組み合わせる最小デモから考える
はじめに
LLMを業務で使う場合、社内文書や業務マニュアル、日々更新される情報をどう参照させるかがよく問題になります。そこでよく使われるのが RAG(Retrieval-Augmented Generation) です。
RAGは、ユーザーの質問に対して関連文書を検索し、その検索結果をLLMに渡して回答を生成する仕組みです。LLM単体では参照できない外部知識を使えるため、社内文書検索やFAQ、業務支援チャットボットなどで使いやすい構成です。
一方で、通常のRAGには苦手なケースもあります。たとえば、答えの根拠が1つの文書にまとまっておらず、複数の文書に分散している場合です。さらに、それらの文書が「同じキーワード」で直接つながっていない場合、単純なベクトル検索だけでは必要な文書を取りこぼすことがあります。
そこで出てくる考え方が GraphRAG です。
この記事では、GraphRAGの考え方を、架空の事故調査シナリオを使った最小デモを題材に整理します。なお、この記事ではデモプロジェクト自体は公開せず、構成・データ例・検索ロジックの要点を文章と簡略コードで説明します。
この記事で扱うこと
この記事で扱う内容は以下です。
- RAGの基本的な流れ
- 通常のRAGが苦手なケース
- 知識グラフの考え方
- GraphRAGの代表的な使い方
- Graph-assisted Retrievalとしての最小デモ設計
- 通常RAGとGraphRAGで取得できる情報がどう変わるか
- 実装する場合の注意点
逆に、以下は扱いません。
- Microsoft GraphRAGの公式実装の詳細
- 本番運用向けのGraphRAGアーキテクチャ
- Neo4jのセットアップ手順
- デモプロジェクト全体のコード公開
ここで説明するGraphRAGは、厳密には「Graph-assisted Retrieval」に近い小さな実装です。Microsoft Researchが公開しているGraphRAGのように、コミュニティ検出や階層的な要約まで含む大規模な構成とは分けて考えます。
RAGの基本
まず、通常のRAGの流れを確認します。
ユーザー質問
↓
質問をEmbedding化
↓
ベクトル検索で関連チャンクを取得
↓
取得したチャンクをLLMに渡す
↓
LLMが回答を生成
検索部分だけを見ると、非常に単純化すれば次のような形です。
def retrieve_by_rag(question: str, vector_store, k: int = 5):
"""通常RAGの検索部分を単純化した例。"""
chunks = vector_store.similarity_search(question, k=k)
return chunks
実際には、以下のような処理も必要になります。
- 文書の分割
- Embeddingの作成
- ベクトルDBへの登録
- メタデータ管理
- プロンプト設計
- 回答時の出典表示
ただし、基本構造は「質問に近い文書チャンクを探し、それをLLMに渡す」というものです。
通常RAGの弱点
RAGは、特定の文書や類似した文書を探すことには強いです。たとえば、ユーザーが「F-17の事故原因は何か」と質問した場合、「F-17」や「事故原因」に近い文書は取得しやすくなります。
しかし、次のようなケースでは取りこぼしが起きやすくなります。
事故報告書には「F-17」という設備名が書かれている。
安全手順書には「フォークリフト」という一般名だけが書かれている。
保守点検記録には「バックブザーの音量低下」が書かれている。
ただし、安全手順書には「F-17」という文字列は出てこない。
この場合、「F-17」で検索すると事故報告書にはたどり着けますが、安全手順書にはたどり着けない可能性があります。
人間であれば、次のように考えられます。
F-17 はフォークリフトである
↓
フォークリフトには安全手順書が適用される
↓
安全手順書には、死角がある場合は誘導者が必要と書かれている
↓
バックブザーに異常がある場合は使用制限が必要かもしれない
つまり、必要なのは単なる類似検索ではなく、対象同士の関係をたどることです。
知識グラフとは
知識グラフでは、情報を 実体(Entity) と 関係(Relationship) で表現します。
たとえば、今回のような事故調査シナリオでは、次のような実体があります。
- F-17
- フォークリフト
- バックブザー
- 作業員
- 事故報告書
- 保守点検記録
- 安全手順書
- 使用制限
これらの実体を、関係でつなぎます。
F-17 --is_a--> フォークリフト
F-17 --has_part--> バックブザー
バックブザー --has_issue--> 音量低下
安全手順書 --applies_to--> フォークリフト
安全手順書 --requires--> 誘導者
警報装置異常 --leads_to--> 使用制限
このように表現すると、「F-17」というキーワードだけでは直接ヒットしない文書でも、グラフ上の関係をたどって検索候補に含めることができます。
GraphRAGとは
GraphRAGは、RAGにグラフ構造を組み合わせる考え方です。
通常のRAGでは、質問文から直接ベクトル検索を行います。一方、GraphRAGでは、質問に含まれる実体や検索結果に出てきた実体を手がかりに、知識グラフ上の隣接ノードや関係をたどり、追加の検索候補を作ります。
単純化すると、次のような流れになります。
ユーザー質問
↓
質問から実体を抽出
↓
知識グラフで関連実体を探索
↓
元の質問 + 関連実体で検索クエリを拡張
↓
ベクトル検索で関連チャンクを取得
↓
LLMが回答を生成
GraphRAGの価値は、単に「検索結果を増やすこと」だけではありません。どの実体からどの実体へたどったのかを記録すれば、調査経路や証拠チェーンも説明しやすくなります。
GraphRAGにはいくつかのタイプがある
GraphRAGといっても、実装方針は1つではありません。
ここでは説明のため、便宜的に次の3つの方向性に分けて考えます。
| タイプ | 概要 | 例 |
|---|---|---|
| Graph-assisted Retrieval | グラフを使って検索クエリや検索範囲を拡張する | F-17 → フォークリフト → 安全手順書 |
| Graph-based Reasoning | グラフ上のパスやルールを使って推論する | 設備異常 → 使用制限 → 事故原因候補 |
| Graph-based Summarization | 大規模文書群をグラフ化し、クラスタやコミュニティ単位で要約する | 大量の社内文書からテーマを抽出 |
今回の最小デモは、この中では Graph-assisted Retrieval に近い構成です。
重要なのは、GraphRAGを使ったからといって、LLMの推論能力そのものが自動的に大幅に上がるわけではないという点です。少なくとも今回のデモで確認した主な効果は、検索段階で取得できる証拠の範囲が広がることでした。
最小デモのシナリオ
今回のデモでは、架空の事故調査シナリオを用意しました。
対象文書は、たとえば次のようなMarkdownファイルです。
data/docs/
accident_report.md
maintenance_record.md
worker_interview.md
safety_manual.md
morning_meeting_memo.md
recurrence_prevention_plan.md
equipment_master.md
質問は次の1つに固定しました。
F-17の事故原因は何か。
ポイントは、事故原因に関する情報が1つの文書にまとまっていないことです。
| 文書 | 含まれる情報の例 |
|---|---|
| 事故報告書 | F-17が後退中に事故を起こした。通路幅が狭い。後方確認が不足していた。 |
| 保守点検記録 | F-17のバックブザーに音量低下が確認されていた。 |
| 作業員ヒアリング | 出荷締切が近く、作業に急ぎがあった。 |
| 安全手順書 | フォークリフト操作時に死角がある場合は誘導者が必要。警報装置に異常がある場合は使用制限が必要。 |
| 設備台帳 | F-17はフォークリフトである。 |
通常のRAGでは、「F-17」や「事故」に近い文書が取得されやすくなります。一方、安全手順書には「F-17」という名前が直接出てこないため、検索結果に入らない可能性があります。
GraphRAGでは、次のように関係をたどります。
F-17
↓ is_a
フォークリフト
↓ applies_to
安全手順書
その結果、「F-17」という文字列を含まない安全手順書も、検索対象として拾いやすくなります。
最小実装イメージ
ここでは、デモ全体ではなく、検索拡張部分だけを簡略化して示します。
まず、文書から抽出した関係をエッジとして持ちます。
from dataclasses import dataclass
from collections import defaultdict, deque
@dataclass(frozen=True)
class Edge:
source: str
relation: str
target: str
edges = [
Edge("F-17", "is_a", "フォークリフト"),
Edge("F-17", "has_part", "バックブザー"),
Edge("バックブザー", "has_issue", "音量低下"),
Edge("バックブザー", "is_a", "警報装置"),
Edge("警報装置", "can_have_issue", "警報装置異常"),
Edge("安全手順書", "mentions", "使用制限"),
Edge("安全手順書", "applies_to", "フォークリフト"),
Edge("安全手順書", "requires", "誘導者"),
Edge("警報装置異常", "leads_to", "使用制限"),
]
次に、指定した実体から数ホップ分だけ関連実体を取得します。
def build_undirected_graph(edges: list[Edge]) -> dict[str, list[tuple[str, str]]]:
"""説明用に無向グラフとして扱う。実運用では方向や関係種別を使い分ける。"""
graph = defaultdict(list)
for edge in edges:
graph[edge.source].append((edge.target, edge.relation))
graph[edge.target].append((edge.source, edge.relation))
return graph
def expand_entities(seed: str, graph: dict[str, list[tuple[str, str]]], max_hops: int = 2) -> set[str]:
"""seedからmax_hops以内の関連実体を取得する。"""
visited = {seed}
queue = deque([(seed, 0)])
while queue:
current, depth = queue.popleft()
if depth >= max_hops:
continue
for neighbor, relation in graph.get(current, []):
if neighbor not in visited:
visited.add(neighbor)
queue.append((neighbor, depth + 1))
return visited
F-17を起点にすると、次のような関連語が得られます。
graph = build_undirected_graph(edges)
terms = expand_entities("F-17", graph, max_hops=2)
print(sorted(terms))
出力例です。
{'F-17', 'フォークリフト', 'バックブザー', '安全手順書', '音量低下'}
この関連語を使って、ベクトル検索のクエリを拡張します。
def retrieve_by_graph_rag(question: str, vector_store, graph, seed_entity: str, k_per_query: int = 3):
"""Graph-assisted Retrievalの簡略例。"""
expanded_terms = expand_entities(seed_entity, graph, max_hops=2)
queries = [question]
for term in expanded_terms:
queries.append(f"{question} {term}")
results = []
seen_chunk_ids = set()
for query in queries:
chunks = vector_store.similarity_search(query, k=k_per_query)
for chunk in chunks:
if chunk.id not in seen_chunk_ids:
results.append(chunk)
seen_chunk_ids.add(chunk.id)
return results
このコードは、あくまで考え方を示すための簡略版です。実際には、以下のような工夫が必要になります。
- 実体抽出をどう行うか
- 同義語や表記揺れをどう正規化するか
- どの関係を何ホップまでたどるか
- グラフ由来の検索語をどの程度重視するか
- 取得した文書をどう重複排除するか
- 回答生成時に出典と探索経路をどう表示するか
Neo4jを使う場合のイメージ
デモでは、知識グラフの格納と確認にNeo4jを使いました。
たとえば、次のようなCypherで関係を登録できます。
MERGE (f:Equipment {name: "F-17"})
MERGE (forklift:EquipmentType {name: "フォークリフト"})
MERGE (buzzer:Part {name: "バックブザー"})
MERGE (manual:Document {name: "安全手順書"})
MERGE (restriction:Rule {name: "使用制限"})
MERGE (f)-[:IS_A]->(forklift)
MERGE (f)-[:HAS_PART]->(buzzer)
MERGE (manual)-[:APPLIES_TO]->(forklift)
MERGE (manual)-[:MENTIONS]->(restriction);
F-17から2ホップ以内の関連ノードを取得するなら、次のようなクエリになります。
MATCH path = (start {name: "F-17"})-[*1..2]-(neighbor)
RETURN path;
この記事では画像を使いませんが、実際にグラフDBで確認すると、F-17を中心にフォークリフト、バックブザー、安全手順書、使用制限などへ関係が広がっていることが分かります。
ただし、簡易実装ではグラフの品質に問題も出ます。たとえば、F-17 と フォークリフトF-17 が別ノードとして残る、関係の向きが不正確になる、一部の関係が抽出できない、といった問題です。
GraphRAGでは、グラフを可視化するだけでは不十分です。重要なのは、グラフを 追加検索の手がかりとして使うことです。
通常RAGとGraphRAGの比較
同じ質問を、通常RAGとGraphRAGに投げた場合の違いを整理します。
質問は次です。
F-17の事故原因は何か。
結果の傾向は次のようになりました。
| 観点 | 通常RAG | GraphRAG |
|---|---|---|
| 取得されやすい文書 | 事故報告書、作業員ヒアリング | 事故報告書、保守点検記録、安全手順書など |
| 見つかる原因 | 通路幅の狭さ、後方確認不足、作業圧力 | 上記に加えて、バックブザー異常、警報装置異常時の使用制限、誘導者の必要性 |
| 強み | 直接関連する文書を素早く取得できる | 関係をたどって間接的な根拠を取得できる |
| 弱み | キーワードが直接含まれない文書を取りこぼす可能性がある | グラフ構築コスト、検索コスト、ノイズが増える |
今回のデモでは、GraphRAGによって「事故報告書に近い情報」だけでなく、「設備異常」や「運用ルール」に関する文書も取得できました。
一方で、注意点もあります。
GraphRAGが安全手順書を取得できたとしても、そこから自動的に次のような結論まで組み立てられるとは限りません。
警報装置に異常がある場合は使用制限が必要
↓
F-17のバックブザーには音量低下があった
↓
したがってF-17は本来使用すべきではなかった可能性がある
このような推論を安定して行うには、Graph-assisted Retrievalだけでなく、Graph-based Reasoning、ルールベースの検証、より厳密なプロンプト設計、評価データの整備などが必要になります。
GraphRAGで得られたこと
今回の最小デモで確認できたことは、次の1点に集約できます。
GraphRAGは、LLMの推論能力そのものを魔法のように強化するというより、検索段階で取得できる証拠の範囲を広げる。
通常RAGでは、質問に直接近い文書が中心になります。GraphRAGでは、グラフ上の関係を使うことで、質問文に直接出てこない関連文書も検索対象に含められます。
この違いは、特に以下のような業務データで効いてきます。
- 事故調査
- 障害調査
- 顧客対応履歴の分析
- 設備保全
- 契約・規程・手順書の横断検索
- ナレッジベースの原因分析
これらの領域では、1つの文書だけで答えが完結しないことが多いです。複数の文書をまたいで、実体と関係をたどる必要があります。
実装時の注意点
GraphRAGを実装する場合、特に重要だと感じた点を整理します。
1. 実体の正規化が重要
簡易実装では、F-17 と フォークリフトF-17 が別ノードとして残るような問題が起きました。
これは検索品質に直結します。同じ対象が複数ノードに分裂すると、本来たどれるはずの関係が途切れます。
対策としては、次のようなものが考えられます。
- 実体辞書を作る
- 同義語辞書を作る
- 型情報を付与する
- LLM抽出後に正規化ステップを入れる
- 人手レビューを組み込む
2. 関係抽出の精度が検索品質を左右する
GraphRAGでは、グラフの関係を検索拡張に使います。つまり、誤った関係があると、検索範囲が誤った方向に広がります。
関係抽出では、少なくとも次を管理した方がよいです。
- 関係の種類
- 関係の向き
- 関係の根拠文書
- 抽出信頼度
- 更新日時
3. 検索コストは増える
GraphRAGでは、通常RAGよりも検索クエリが増えます。
たとえば、通常RAGでは1回の質問に対して1回検索すればよいところ、GraphRAGでは関連実体ごとに複数回検索する可能性があります。
通常RAG:
search("F-17の事故原因は何か")
GraphRAG:
search("F-17の事故原因は何か")
search("F-17の事故原因は何か フォークリフト")
search("F-17の事故原因は何か バックブザー")
search("F-17の事故原因は何か 安全手順書")
...
そのため、実運用では次のような制御が必要になります。
- 最大ホップ数を制限する
- 関係タイプでフィルタする
- ノードの重要度で絞る
- 検索結果を再ランキングする
- 取得チャンク数を制御する
4. 「検索」と「推論」は分けて考える
今回のデモで重要だったのは、回答生成部分ではなく検索部分です。
通常RAGとGraphRAGで同じLLM、同じ回答生成プロンプトを使い、検索段階で取得できる文書がどう変わるかを比較しました。
GraphRAGを評価する場合も、次を分けて見ると整理しやすいです。
- 必要な文書を取得できたか
- 不要な文書が増えすぎていないか
- 取得した文書から正しい回答を生成できたか
- 回答の根拠を説明できるか
- 探索経路を再現できるか
まとめ
この記事では、RAGと知識グラフを組み合わせるGraphRAGの考え方を、架空の事故調査シナリオを使った最小デモベースで整理しました。
要点は次の通りです。
- RAGは、外部文書を検索してLLMに渡す実用的な構成である
- ただし、複数文書にまたがる関係や因果関係の扱いは苦手な場合がある
- 知識グラフを使うと、実体と関係をたどって検索範囲を広げられる
- 今回のデモは、GraphRAGの中でもGraph-assisted Retrievalに近い
- GraphRAGの主な効果は、推論能力の自動強化ではなく、取得できる証拠範囲の拡大だった
- 実運用では、実体正規化、関係抽出、検索コスト、出典管理が重要になる
GraphRAGは、まだ発展途上の領域です。ただ、業務データのように情報が複数文書に分散している場合、通常RAGだけでは届きにくい文書に到達するための有効な選択肢になります。
参考
- Microsoft Research: Project GraphRAG
https://www.microsoft.com/en-us/research/project/graphrag/ - Microsoft GraphRAG Documentation
https://microsoft.github.io/graphrag/ - microsoft/graphrag GitHub Repository
https://github.com/microsoft/graphrag - Microsoft Research Blog: GraphRAG: Unlocking LLM discovery on narrative private data
https://www.microsoft.com/en-us/research/blog/graphrag-unlocking-llm-discovery-on-narrative-private-data/