2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

RAGとGraphRAGの比較

Posted at

はじめに

最近注目を集めているGraphRAGを学んでいこうと思います!従来のRAGではできない事でもGraphRAGだとできる事もあるようなので、今回は実験していこうと思います!!

そもそも、RAGとGraphRAGの違いは?

一言で言うと、RAG は文章の断片(チャンク)」を検索して LLM に渡す手法であり、近いチャンクを返却する。
GraphRAG は知識を構造化(グラフ化)して関係性をたどりながら回答させる手法で、関連ノードのネットワーク全体を辿る方式。
→ここだけ聞くといまいちわからないですね💦💦💦なぜ近いチャンクだけ返却するとダメなのかを説明していきます!!

RAG が不十分と言われる理由(1〜6の一般化まとめ)

1. 「近いテキスト」が本当に必要な情報とは限らない

RAG は embedding 類似度で “似ている文” を検索する手法。
しかし「単語が似ている=質問の答えに必要な情報がある」とは限らない。

実際には、

  • 必要な内容が複数の場所に散らばっている
  • 関係する情報が文章上では離れている
    ことが多いため、検索だけでは答えにたどり着けない

2. 長い流れ・複合的な文脈を扱えない

多くの知識は「A → B → C」と連続した流れで理解される。

しかし RAG は、

  • 1〜数チャンク分の“局所的な近さ”しか見ない
  • そのため途中のステップを取りこぼす
  • 文脈の長距離依存を扱えない
    という制約がある。

全体像が必要な質問に弱い

3. チャンク分割に大きく依存する

どこで文章を切るか(チャンク化)は人間が決める必要がある。

  • 切り方が悪いと必要な情報が別チャンクに飛ぶ
  • どんなサイズでも「完璧なチャンク化」は存在しない
  • チャンクの境界は「知識の境界」ではない

つまり RAG はチャンク設計の影響を強く受ける脆い方式

4. 知識の「関係性」を理解できない

実際の知識は、以下のような構造で成り立っている:

  • 原因 → 結果
  • 前提 → 結論
  • 上位概念 → 下位概念
  • 類似関係
  • 時系列
  • 参照関係
  • 相互依存性

しかし RAG はテキストの類似度のみを扱うため、
これらの 関係性そのものを理解したり辿ったりできない

5. 検索結果に依存するため回答が不安定

RAG の精度は検索に強く左右される。

  • 類似度のわずかな差
  • 質問文の表現ゆれ
  • テキストの位置関係

これらの条件が変わると、取得されるチャンクが変わり、
同じ質問でも回答がブレる

6. チャンクという形式自体が知識表現として粗い

チャンクは単なる「文章の塊」であり、
知識の最小単位とは限らない。

実際の知識は、

  • 概念
  • 属性
  • ルール
  • 関係性
  • 階層構造
  • 意図
  • 背景
    など、文章を超えた多層構造で成り立つ。

チャンクはこの構造を保持できないため、
複雑な質問・理由説明・背景理解に弱い


🔍 総括(一般形)

RAG が不十分なのは、
テキストの“位置の近さ”だけで判断するため、

  • 長い文脈
  • 因果
  • 関係性
  • 複合的な質問
  • 抽象的な問い

の理解が困難になるから。

つまり、RAG は「検索」。
構造や関係性を扱うには **GraphRAG(構造化された知識探索)」が必要になる。

検証

RAGはチャンクを拾ってくるのは得意です。一般的に類似度が高いチャンクを3つ取得し、それを元に変換していきます。ただ、全体の概要を踏まえた上で回答するのが難しい事を確認していきます!
題材はわかりやすいように日本昔話の「浦島太郎」を題材にしていきます!

LLMの学習内容

LLMに浦島太郎の内容を学習させます。チャンクは一文毎で区切っていきます。

浦島太郎の物語
ある日、若い漁師の浦島太郎は、浜辺で子どもたちにいじめられている一匹の亀を見つけた。
太郎は子どもたちを止めて、亀を助け、そっと海へ返してやった。

数日後、太郎が船で漁に出ていると、あの時の亀が姿を現し、「助けてくれたお礼に竜宮城へご案内します」と告げた。
太郎が亀の背中に乗ると、海の底にある美しい宮殿、竜宮城にたどり着いた。
竜宮城では、乙姫さまが太郎を迎え、踊りやごちそうでもてなした。

太郎は竜宮城で楽しい日々を過ごしたが、やがて「そろそろ村の家族のことが心配になってきた」と感じ、地上に帰りたいと乙姫さまに伝えた。
乙姫さまは少し寂しそうな顔をしながらも、「これはお土産の玉手箱です。決して決して開けてはいけません」と言って、箱を太郎に渡した。

太郎が地上に戻ると、知っているはずの村はすっかり様子が変わっていた。
家の並びも人々の顔ぶれも、自分が知っているものとは違っていた。
太郎が周りの人に尋ねると、自分の名前も家族の名前も、もう誰も知らなかった。
どうやら太郎が竜宮城で過ごしたあいだに、地上では何百年もの時が流れてしまっていたのだった。

途方に暮れた太郎は、乙姫さまから渡された玉手箱を思い出した。
「開けてはいけない」と言われていたが、「中を見れば何か分かるかもしれない」と考え、ついに箱を開けてしまった。
すると中から白い煙が立ちのぼり、たちまち太郎の体はしわだらけの老人の姿になってしまった。

RAGのコードと実行結果

# urashima_rag.py
# ------------------------------------------------------------
# 題材:昔話「浦島太郎」を使ったシンプルRAG実験
# ※ バージョン:参照チャンク(Top-k)を表示する
# ------------------------------------------------------------

from __future__ import annotations
import os
import numpy as np
from dotenv import load_dotenv
from openai import OpenAI

# =========================
# 0. OpenRouter クライアント設定
# =========================

load_dotenv()

api_key = os.getenv("OPENROUTER_API_KEY")
if not api_key:
    raise RuntimeError("OPENROUTER_API_KEY が設定されていません")

client = OpenAI(
    api_key=api_key,
    base_url="https://openrouter.ai/api/v1",
)

EMBED_MODEL = "openai/text-embedding-3-small"
CHAT_MODEL = "openai/gpt-4.1-mini"


# =========================
# 1. 題材(浦島太郎)
# =========================

URASHIMA_TEXT = """
ある日、若い漁師の浦島太郎は、浜辺で子どもたちにいじめられている一匹の亀を見つけた。
太郎は子どもたちを止めて、亀を助け、そっと海へ返してやった。

数日後、太郎が船で漁に出ていると、あの時の亀が姿を現し、「助けてくれたお礼に竜宮城へご案内します」と告げた。
太郎が亀の背中に乗ると、海の底にある美しい宮殿、竜宮城にたどり着いた。
竜宮城では、乙姫さまが太郎を迎え、踊りやごちそうでもてなした。

太郎は竜宮城で楽しい日々を過ごしたが、やがて「そろそろ村の家族のことが心配になってきた」と感じ、地上に帰りたいと乙姫さまに伝えた。
乙姫さまは少し寂しそうな顔をしながらも、「これはお土産の玉手箱です。決して決して開けてはいけません」と言って、箱を太郎に渡した。

太郎が地上に戻ると、知っているはずの村はすっかり様子が変わっていた。
家の並びも人々の顔ぶれも、自分が知っているものとは違っていた。
太郎が周りの人に尋ねると、自分の名前も家族の名前も、もう誰も知らなかった。
どうやら太郎が竜宮城で過ごしたあいだに、地上では何百年もの時が流れてしまっていたのだった。

途方に暮れた太郎は、乙姫さまから渡された玉手箱を思い出した。
「開けてはいけない」と言われていたが、「中を見れば何か分かるかもしれない」と考え、ついに箱を開けてしまった。
すると中から白い煙が立ちのぼり、たちまち太郎の体はしわだらけの老人の姿になってしまった。
"""


# =========================
# 2. 文ごとのチャンク分割
# =========================

def split_into_sentences(text: str):
    sentences = []
    for raw in text.split(""):
        s = raw.strip()
        if not s:
            continue
        if not s.endswith(""):
            s += ""
        sentences.append(s)
    return sentences


# =========================
# 3. Embedding & 類似検索
# =========================

def embed_list(texts):
    resp = client.embeddings.create(
        model=EMBED_MODEL,
        input=texts,
    )
    return [d.embedding for d in resp.data]


class MemoryVectorStore:
    def __init__(self, sentences):
        self.sentences = sentences
        emb = embed_list(sentences)
        self.vecs = np.array(emb, dtype="float32")
        self.vecs = self.vecs / (np.linalg.norm(self.vecs, axis=1, keepdims=True) + 1e-8)

    def search(self, query: str, k: int = 3):
        q = np.array(embed_list([query])[0], dtype="float32")
        q = q / (np.linalg.norm(q) + 1e-8)
        scores = np.dot(self.vecs, q)
        idx = np.argsort(-scores)[:k]
        return [(i, float(scores[i]), self.sentences[i]) for i in idx]


# =========================
# 4. LLM で回答(RAG)
# =========================

def answer_rag(question: str, store: MemoryVectorStore, k: int = 3):
    """
    RAG の回答と「参照チャンク(Top-k)」を両方返す
    """
    results = store.search(question, k=k)

    # 表示用
    context_parts = []
    for i, score, sent in results:
        context_parts.append(f"[Sentence {i} / score={score:.3f}] {sent}")
    context = "\n".join(context_parts)

    # LLM 用(RAG回答)
    system = (
        "あなたは昔話『浦島太郎』の質問に答えるAIです。"
        "与えられた文だけを使って回答してください。"
        "答えは1語/1行。分からなければ『不明』。"
    )

    prompt = f"""
以下が参照文です:

{context}

質問:
{question}

出力: 答えのみ(例: 亀)/分からない場合は「不明」
"""

    res = client.chat.completions.create(
        model=CHAT_MODEL,
        temperature=0.0,
        messages=[
            {"role": "system", "content": system},
            {"role": "user", "content": prompt},
        ],
    )

    answer = res.choices[0].message.content.strip()

    return answer, results   # ← 参照チャンクも返す


# =========================
# 5. 質問セット
# =========================

RAG_OK_QUESTIONS = [
    "浦島太郎が浜辺で助けた生き物は何ですか?",
    "浦島太郎が招かれた海の底の宮殿の名前は何ですか?",
    "乙姫さまがお土産として渡した箱の名前は何ですか?",
]

GRAPH_RAG_QUESTIONS = [
    "浦島太郎が竜宮城に行くことになったきっかけは何ですか?",
    "浦島太郎が老人になってしまった原因は何ですか?",
]


# =========================
# 6. 実行
# =========================

def main():
    print("📚 文分割中...")
    sentences = split_into_sentences(URASHIMA_TEXT)
    print(f"文数: {len(sentences)}\n")

    print("🧮 Embedding 計算中...")
    store = MemoryVectorStore(sentences)
    print("完了\n")

    print("====== 🔵 RAG向き(3問) ======")
    for q in RAG_OK_QUESTIONS:
        print(f"\nQ: {q}")
        ans, refs = answer_rag(q, store, k=3)
        print(f"A: {ans}\n")
        print("📌 参照したチャンク:")
        for i, score, sent in refs:
            print(f"  - [#{i} | {score:.3f}] {sent}")
        print("")

    print("\n====== 🔴 因果系(GraphRAG向き)2問 ======")
    for q in GRAPH_RAG_QUESTIONS:
        print(f"\nQ: {q}")
        ans, refs = answer_rag(q, store, k=3)
        print(f"A: {ans}\n")
        print("📌 参照したチャンク:")
        for i, score, sent in refs:
            print(f"  - [#{i} | {score:.3f}] {sent}")
        print("")


if __name__ == "__main__":
    main()

urashima_taro.gif

(venv) ~/develop/langchain_study  (main)$ python3 graph_rag/urashimataro_rag.py 
📚 文分割中...
文数: 15

🧮 Embedding 計算中...
完了

====== 🔵 RAG向き(3問) ======

Q: 浦島太郎が浜辺で助けた生き物は何ですか?
A: 亀

📌 参照したチャンク:
  - [#0 | 0.595] ある日、若い漁師の浦島太郎は、浜辺で子どもたちにいじめられている一匹の亀を見つけた。
  - [#1 | 0.569] 太郎は子どもたちを止めて、亀を助け、そっと海へ返してやった。
  - [#2 | 0.541] 数日後、太郎が船で漁に出ていると、あの時の亀が姿を現し、「助けてくれたお礼に竜宮城へご案内します」と告げた。


Q: 浦島太郎が招かれた海の底の宮殿の名前は何ですか?
A: 竜宮城

📌 参照したチャンク:
  - [#3 | 0.610] 太郎が亀の背中に乗ると、海の底にある美しい宮殿、竜宮城にたどり着いた。
  - [#2 | 0.426] 数日後、太郎が船で漁に出ていると、あの時の亀が姿を現し、「助けてくれたお礼に竜宮城へご案内します」と告げた。
  - [#5 | 0.404] 太郎は竜宮城で楽しい日々を過ごしたが、やがて「そろそろ村の家族のことが心配になってきた」と感じ、地上に帰りたいと乙姫さまに伝えた。


Q: 乙姫さまがお土産として渡した箱の名前は何ですか?
A: 玉手箱

📌 参照したチャンク:
  - [#6 | 0.698] 乙姫さまは少し寂しそうな顔をしながらも、「これはお土産の玉手箱です。
  - [#12 | 0.554] 途方に暮れた太郎は、乙姫さまから渡された玉手箱を思い出した。
  - [#7 | 0.424] 決して決して開けてはいけません」と言って、箱を太郎に渡した。


====== 🔴 因果系(GraphRAG向き)2問 ======

Q: 浦島太郎が竜宮城に行くことになったきっかけは何ですか?
A: 亀

📌 参照したチャンク:
  - [#3 | 0.636] 太郎が亀の背中に乗ると、海の底にある美しい宮殿、竜宮城にたどり着いた。
  - [#5 | 0.609] 太郎は竜宮城で楽しい日々を過ごしたが、やがて「そろそろ村の家族のことが心配になってきた」と感じ、地上に帰りたいと乙姫さまに伝えた。
  - [#2 | 0.581] 数日後、太郎が船で漁に出ていると、あの時の亀が姿を現し、「助けてくれたお礼に竜宮城へご案内します」と告げた。


Q: 浦島太郎が老人になってしまった原因は何ですか?
A: 白い煙

📌 参照したチャンク:
  - [#0 | 0.498] ある日、若い漁師の浦島太郎は、浜辺で子どもたちにいじめられている一匹の亀を見つけた。
  - [#14 | 0.452] すると中から白い煙が立ちのぼり、たちまち太郎の体はしわだらけの老人の姿になってしまった。
  - [#1 | 0.434] 太郎は子どもたちを止めて、亀を助け、そっと海へ返してやった。

RAGは取得した上位3つ情報を元に要約し回答を生成します。実際にやってみると、最初の3つはちゃんと答えられていますが、後半の2つはちょっと正確ではないですね。(きっかけは「亀」、老人になった原因は「白い煙」、ばつではないですが、これじゃない感の答えですね💦💦💦)

GraphRAGのコードと実行結果

GraphRAGでナレッジグラフを作成した上で、回答をしていきます!

# urashimataro_graphrag.py
# ------------------------------------------------------------
# 昔話「浦島太郎」からナレッジグラフを自動生成し、
# そのグラフを使って GraphRAG 風に QA するスクリプト。
# 最後にナレッジグラフを PNG としてプロットもする。
#
# 依存:
#   pip install openai python-dotenv numpy networkx matplotlib
#   .env に OPENROUTER_API_KEY を設定
# ------------------------------------------------------------

from __future__ import annotations
import os
import json
from typing import List, Dict, Any, Tuple

import numpy as np
from dotenv import load_dotenv
from openai import OpenAI

from matplotlib import font_manager
from matplotlib.font_manager import FontProperties

# グラフ可視化用
import networkx as nx
import matplotlib.pyplot as plt
from matplotlib import font_manager

# =========================
# 0. OpenRouter クライアント設定
# =========================

load_dotenv()

api_key = os.getenv("OPENROUTER_API_KEY")
if not api_key:
    raise RuntimeError("OPENROUTER_API_KEY が設定されていません")

client = OpenAI(
    api_key=api_key,
    base_url="https://openrouter.ai/api/v1",
)

EMBED_MODEL = "openai/text-embedding-3-small"
CHAT_MODEL = "openai/gpt-4.1-mini"


# =========================
# 1. 題材テキスト(浦島太郎)
# =========================

URASHIMA_TEXT = """
ある日、若い漁師の浦島太郎は、浜辺で子どもたちにいじめられている一匹の亀を見つけた。
太郎は子どもたちを止めて、亀を助け、そっと海へ返してやった。

数日後、太郎が船で漁に出ていると、あの時の亀が姿を現し、「助けてくれたお礼に竜宮城へご案内します」と告げた。
太郎が亀の背中に乗ると、海の底にある美しい宮殿、竜宮城にたどり着いた。
竜宮城では、乙姫さまが太郎を迎え、踊りやごちそうでもてなした。

太郎は竜宮城で楽しい日々を過ごしたが、やがて「そろそろ村の家族のことが心配になってきた」と感じ、地上に帰りたいと乙姫さまに伝えた。
乙姫さまは少し寂しそうな顔をしながらも、「これはお土産の玉手箱です。決して決して開けてはいけません」と言って、箱を太郎に渡した。

太郎が地上に戻ると、知っているはずの村はすっかり様子が変わっていた。
家の並びも人々の顔ぶれも、自分が知っているものとは違っていた。
太郎が周りの人に尋ねると、自分の名前も家族の名前も、もう誰も知らなかった。
どうやら太郎が竜宮城で過ごしたあいだに、地上では何百年もの時が流れてしまっていたのだった。

途方に暮れた太郎は、乙姫さまから渡された玉手箱を思い出した。
「開けてはいけない」と言われていたが、「中を見れば何か分かるかもしれない」と考え、ついに箱を開けてしまった。
すると中から白い煙が立ちのぼり、たちまち太郎の体はしわだらけの老人の姿になってしまった。
"""


# =========================
# 2. 文分割ユーティリティ
# =========================

def split_into_sentences(text: str) -> List[str]:
    sentences: List[str] = []
    for raw in text.split(""):
        s = raw.strip()
        if not s:
            continue
        if not s.endswith(""):
            s += ""
        sentences.append(s)
    return sentences


# =========================
# 3. Embedding ユーティリティ
# =========================

def embed_list(texts: List[str]) -> List[List[float]]:
    resp = client.embeddings.create(
        model=EMBED_MODEL,
        input=texts,
    )
    return [d.embedding for d in resp.data]


# =========================
# 4. ナレッジグラフ構築 (triple抽出 → nodes/edges)
# =========================

def extract_triples(sentence: str, idx: int) -> List[Dict[str, Any]]:
    system = (
        "あなたは日本語テキストからナレッジグラフ用の情報を抽出するアシスタントです。"
        "与えられた1文について、登場人物・物・場所・出来事などの関係を、"
        "subject, relation, object の三つ組で抽出してください。"
        "出力は必ず JSON 配列形式で、各要素は次のキーを持つオブジェクトにしてください:\n"
        "- subject: 主語となる名前(例: 浦島太郎, 亀, 乙姫さま, 玉手箱, 竜宮城 など)\n"
        "- subject_type: Person / Creature / Place / Object / Event など\n"
        "- relation: 日本語の短い動詞・フレーズ(例: 助けた, 案内した, 渡した, 開けた, 原因となった など)\n"
        "- object: 対象となる名前\n"
        "- object_type: Person / Creature / Place / Object / Event など\n"
        "- evidence: この関係が読み取れる元の文(そのまま)\n"
        "抽出できる関係が特にない場合は、空の配列 [] を返してください。"
    )

    user = f"文:\n{sentence}\n\nこの文から抽出できる triple を JSON 配列で出力してください。"

    res = client.chat.completions.create(
        model=CHAT_MODEL,
        temperature=0.0,
        messages=[
            {"role": "system", "content": system},
            {"role": "user", "content": user},
        ],
    )

    content = res.choices[0].message.content
    try:
        triples = json.loads(content)
        if not isinstance(triples, list):
            return []
        for t in triples:
            t.setdefault("evidence", sentence)
            t["sentence_index"] = idx
        return triples
    except json.JSONDecodeError:
        print(f"[WARN] JSON parse error at sentence #{idx}: {content}")
        return []


def build_graph_from_text(text: str) -> Dict[str, Any]:
    sentences = split_into_sentences(text)
    print(f"📚 文数: {len(sentences)}")

    all_triples: List[Dict[str, Any]] = []
    for idx, sent in enumerate(sentences):
        print(f"⛏ triple 抽出中... sentence #{idx}: {sent}")
        triples = extract_triples(sent, idx)
        all_triples.extend(triples)

    node_map: Dict[str, Dict[str, Any]] = {}
    edges: List[Dict[str, Any]] = []

    def node_key(name: str, ntype: str) -> str:
        return f"{name}::{ntype}"

    for t in all_triples:
        s_name = t.get("subject")
        s_type = t.get("subject_type", "Unknown")
        o_name = t.get("object")
        o_type = t.get("object_type", "Unknown")
        rel = t.get("relation")
        evidence = t.get("evidence")
        sent_idx = t.get("sentence_index")

        if not s_name or not o_name or not rel:
            continue

        sk = node_key(s_name, s_type)
        ok = node_key(o_name, o_type)

        if sk not in node_map:
            node_map[sk] = {"id": len(node_map), "name": s_name, "type": s_type}
        if ok not in node_map:
            node_map[ok] = {"id": len(node_map), "name": o_name, "type": o_type}

        edges.append(
            {
                "source": node_map[sk]["id"],
                "target": node_map[ok]["id"],
                "relation": rel,
                "evidence": evidence,
                "sentence_index": sent_idx,
            }
        )

    nodes = list(node_map.values())

    graph = {
        "nodes": nodes,
        "edges": edges,
        "sentences": sentences,
        "triples": all_triples,
    }
    return graph


# =========================
# 5. GraphRAG用:ノード埋め込み & QA
# =========================

class GraphRAG:
    def __init__(self, graph: Dict[str, Any]):
        self.graph = graph
        self.nodes = graph["nodes"]
        self.edges = graph["edges"]

        node_texts = [f'{n["name"]} ({n["type"]})' for n in self.nodes]
        vecs = np.array(embed_list(node_texts), dtype="float32")
        self.node_vecs = vecs / (np.linalg.norm(vecs, axis=1, keepdims=True) + 1e-8)

    def search_nodes(self, query: str, k: int = 3) -> List[Tuple[int, float]]:
        q_vec = np.array(embed_list([query])[0], dtype="float32")
        q_vec = q_vec / (np.linalg.norm(q_vec) + 1e-8)
        scores = np.dot(self.node_vecs, q_vec)
        idx = np.argsort(-scores)[:k]
        return [(int(i), float(scores[i])) for i in idx]

    def gather_facts_around_nodes(self, node_ids: List[int]) -> List[Dict[str, Any]]:
        node_id_set = set(node_ids)
        facts: List[Dict[str, Any]] = []
        for e in self.edges:
            if e["source"] in node_id_set or e["target"] in node_id_set:
                facts.append(e)
        return facts

    def answer(
        self,
        question: str,
        top_k_nodes: int = 3,
    ) -> Tuple[str, List[Tuple[int, float]], List[Dict[str, Any]]]:
        node_hits = self.search_nodes(question, k=top_k_nodes)
        hit_node_ids = [nid for nid, _ in node_hits]

        fact_edges = self.gather_facts_around_nodes(hit_node_ids)

        node_hit_infos = []
        for nid, score in node_hits:
            n = self.nodes[nid]
            node_hit_infos.append(
                f'[Node {nid} | score={score:.3f}] {n["name"]} ({n["type"]})'
            )

        edge_infos = []
        for e in fact_edges:
            src = self.nodes[e["source"]]
            tgt = self.nodes[e["target"]]
            edge_infos.append(
                f'{src["name"]} --{e["relation"]}--> {tgt["name"]} '
                f'(from sentence #{e["sentence_index"]}: {e["evidence"]})'
            )

        context = "【関連ノード】\n" + "\n".join(node_hit_infos) + "\n\n" \
                  + "【関連する関係(エッジ)】\n" + "\n".join(edge_infos)

        system = (
            "あなたは昔話『浦島太郎』の内容を、ナレッジグラフとして与えられています。"
            "ナレッジグラフは、「誰が」「何をした」「何に」という関係(エッジ)の集合です。"
            "以下に示すノードとエッジに含まれる事実だけを使って、質問に答えてください。"
            "一般的な常識や、グラフに含まれていない情報で補ってはいけません。"
            "答えはできるだけ短く、日本語で1行で答えてください。"
            "もしグラフの事実だけでは答えが決められない場合は、『不明』と答えてください。"
        )

        prompt = f"""
ナレッジグラフから抽出した事実は次の通りです:

{context}

質問:
{question}

出力形式:
- 答えを短く1行で(例: 亀, 亀を助けたこと, 玉手箱を開けたこと など)
- 分からない場合は『不明』
"""

        res = client.chat.completions.create(
            model=CHAT_MODEL,
            temperature=0.0,
            messages=[
                {"role": "system", "content": system},
                {"role": "user", "content": prompt},
            ],
        )
        answer = res.choices[0].message.content.strip()

        return answer, node_hits, fact_edges


# =========================
# 6. ナレッジグラフをPNGでプロットする(日本語フォント対応)
# =========================

def plot_graph(graph: Dict[str, Any], output_path: str = "urashimataro_graph.png") -> None:
    """
    graph["nodes"], graph["edges"] から簡易ナレッジグラフを PNG に描画する
    (日本語フォントをファイルから強制指定する版)
    """
    # ---- 日本語フォント設定(パス候補を順番に探す)----
    font_candidates = [
        # macOS (ヒラギノ)
        "/System/Library/Fonts/ヒラギノ角ゴシック W6.ttc",
        "/System/Library/Fonts/ヒラギノ角ゴ ProN W6.otf",
        "/System/Library/Fonts/ヒラギノ角ゴシック W3.ttc",
        # Noto 系(Homebrew や Linux, Colab など)
        "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc",
        "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.otf",
    ]

    jp_font_path = None
    for p in font_candidates:
        if os.path.exists(p):
            jp_font_path = p
            break

    font_prop: FontProperties | None = None
    if jp_font_path is not None:
        font_prop = FontProperties(fname=jp_font_path)
        font_manager.fontManager.addfont(jp_font_path)

        family = font_prop.get_name()
        # ★ ここで family / sans-serif 両方を上書きする
        plt.rcParams["font.family"] = family
        plt.rcParams["font.sans-serif"] = [family]

        print(f"✅ Using JP font: {jp_font_path} (family={family})")
    else:
        print("⚠ 日本語フォントが見つからなかったので、DejaVu のまま描画します(文字化けの可能性あり)")

    # ---- ここからグラフ描画 ----
    G = nx.DiGraph()

    id_to_name = {}
    for n in graph["nodes"]:
        nid = n["id"]
        name = n["name"]
        id_to_name[nid] = name
        G.add_node(nid, label=name)

    for e in graph["edges"]:
        src = e["source"]
        tgt = e["target"]
        rel = e["relation"]
        G.add_edge(src, tgt, label=rel)

    plt.figure(figsize=(10, 8))

    pos = nx.spring_layout(G, k=0.7, iterations=200, seed=42)

    nx.draw_networkx_nodes(G, pos)

    labels = {nid: id_to_name[nid] for nid in G.nodes()}
    # 日本語フォントを rcParams に登録しているので、ここは普通に描画で OK
    nx.draw_networkx_labels(G, pos, labels=labels, font_size=9)

    nx.draw_networkx_edges(G, pos, arrows=True)

    edge_labels = {(u, v): d["label"] for u, v, d in G.edges(data=True)}
    nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels, font_size=8)

    plt.axis("off")
    plt.tight_layout()
    plt.savefig(output_path, dpi=200)
    plt.close()

    print(f"\n🖼 ナレッジグラフを {output_path} として保存しました。")

# =========================
# 7. 質問セット
# =========================

RAG_OK_QUESTIONS = [
    "浦島太郎が浜辺で助けた生き物は何ですか?",
    "浦島太郎が招かれた海の底の宮殿の名前は何ですか?",
    "乙姫さまがお土産として渡した箱の名前は何ですか?",
]

GRAPH_RAG_QUESTIONS = [
    "浦島太郎が竜宮城に行くことになったきっかけは何ですか?",
    "浦島太郎が老人になってしまった原因は何ですか?",
]


# =========================
# 8. メイン実行
# =========================

def main():
    print("📖 浦島太郎テキストからグラフを構築します...")
    graph = build_graph_from_text(URASHIMA_TEXT)

    print("\n===== グラフ概要 =====")
    print(f"ノード数: {len(graph['nodes'])}")
    print(f"エッジ数: {len(graph['edges'])}")

    print("\nノード一覧:")
    for n in graph["nodes"]:
        print(f'  [Node {n["id"]}] {n["name"]} ({n["type"]})')

    print("\n代表的なエッジ例(最大10件):")
    for e in graph["edges"][:10]:
        src = graph["nodes"][e["source"]]
        tgt = graph["nodes"][e["target"]]
        print(
            f'  {src["name"]} --{e["relation"]}--> {tgt["name"]} '
            f'(sent #{e["sentence_index"]})'
        )

    # GraphRAGインスタンス生成
    rag = GraphRAG(graph)

    print("\n==============================")
    print("🔵 GraphRAGで答える(単純な3問)")
    print("==============================")
    for q in RAG_OK_QUESTIONS:
        print(f"\nQ: {q}")
        ans, node_hits, fact_edges = rag.answer(q, top_k_nodes=3)
        print(f"A: {ans}")

        print("📌 ヒットしたノード:")
        for nid, score in node_hits:
            n = graph["nodes"][nid]
            print(f'  - [Node {nid} | {score:.3f}] {n["name"]} ({n["type"]})')

        print("📌 参照されたエッジ:")
        for e in fact_edges:
            src = graph["nodes"][e["source"]]
            tgt = graph["nodes"][e["target"]]
            print(
                f'  - {src["name"]} --{e["relation"]}--> {tgt["name"]} '
                f'(from sentence #{e["sentence_index"]})'
            )

    print("\n==============================")
    print("🔴 GraphRAG向き(因果・多段推論)2問")
    print("==============================")
    for q in GRAPH_RAG_QUESTIONS:
        print(f"\nQ: {q}")
        ans, node_hits, fact_edges = rag.answer(q, top_k_nodes=3)
        print(f"A: {ans}")

        print("📌 ヒットしたノード:")
        for nid, score in node_hits:
            n = graph["nodes"][nid]
            print(f'  - [Node {nid} | {score:.3f}] {n["name"]} ({n["type"]})')

        print("📌 参照されたエッジ:")
        for e in fact_edges:
            src = graph["nodes"][e["source"]]
            tgt = graph["nodes"][e["target"]]
            print(
                f'  - {src["name"]} --{e["relation"]}--> {tgt["name"]} '
                f'(from sentence #{e["sentence_index"]})'
            )

    # ===== グラフのPNG出力 =====
    plot_graph(graph, output_path="urashimataro_graph.png")


if __name__ == "__main__":
    main()
実行結果
(venv) ~/develop/langchain_study  (main)$ python3 graph_rag/urashimataro_graphrag.py

📖 浦島太郎テキストからグラフを構築します...
📚 文数: 15
⛏ triple 抽出中... sentence #0: ある日、若い漁師の浦島太郎は、浜辺で子どもたちにいじめられている一匹の亀を見つけた。
⛏ triple 抽出中... sentence #1: 太郎は子どもたちを止めて、亀を助け、そっと海へ返してやった。
⛏ triple 抽出中... sentence #2: 数日後、太郎が船で漁に出ていると、あの時の亀が姿を現し、「助けてくれたお礼に竜宮城へご案内します」と告げた。
⛏ triple 抽出中... sentence #3: 太郎が亀の背中に乗ると、海の底にある美しい宮殿、竜宮城にたどり着いた。
⛏ triple 抽出中... sentence #4: 竜宮城では、乙姫さまが太郎を迎え、踊りやごちそうでもてなした。
⛏ triple 抽出中... sentence #5: 太郎は竜宮城で楽しい日々を過ごしたが、やがて「そろそろ村の家族のことが心配になってきた」と感じ、地上に帰りたいと乙姫さまに伝えた。
⛏ triple 抽出中... sentence #6: 乙姫さまは少し寂しそうな顔をしながらも、「これはお土産の玉手箱です。
⛏ triple 抽出中... sentence #7: 決して決して開けてはいけません」と言って、箱を太郎に渡した。
⛏ triple 抽出中... sentence #8: 太郎が地上に戻ると、知っているはずの村はすっかり様子が変わっていた。
⛏ triple 抽出中... sentence #9: 家の並びも人々の顔ぶれも、自分が知っているものとは違っていた。
⛏ triple 抽出中... sentence #10: 太郎が周りの人に尋ねると、自分の名前も家族の名前も、もう誰も知らなかった。
⛏ triple 抽出中... sentence #11: どうやら太郎が竜宮城で過ごしたあいだに、地上では何百年もの時が流れてしまっていたのだった。
⛏ triple 抽出中... sentence #12: 途方に暮れた太郎は、乙姫さまから渡された玉手箱を思い出した。
⛏ triple 抽出中... sentence #13: 「開けてはいけない」と言われていたが、「中を見れば何か分かるかもしれない」と考え、ついに箱を開けてしまった。
⛏ triple 抽出中... sentence #14: すると中から白い煙が立ちのぼり、たちまち太郎の体はしわだらけの老人の姿になってしまった。

===== グラフ概要 =====
ノード数: 34
エッジ数: 38

ノード一覧:
  [Node 0] 浦島太郎 (Person)
  [Node 1] 亀 (Creature)
  [Node 2] 子どもたち (Person)
  [Node 3] 太郎 (Person)
  [Node 4] 海 (Place)
  [Node 5] 船 (Object)
  [Node 6] 太郎の前 (Person)
  [Node 7] 竜宮城 (Place)
  [Node 8] 亀の背中 (Object)
  [Node 9] 海の底 (Place)
  [Node 10] 乙姫さま (Person)
  [Node 11] 踊り (Event)
  [Node 12] ごちそう (Object)
  [Node 13] 楽しい日々 (Event)
  [Node 14] そろそろ村の家族のことが心配になってきた (Event)
  [Node 15] 玉手箱 (Object)
  [Node 16] (話者) (Person)
  [Node 17] 決して決して開けてはいけません (Event)
  [Node 18] 箱 (Object)
  [Node 19] 地上 (Place)
  [Node 20] 村 (Place)
  [Node 21] 様子 (Event)
  [Node 22] 周りの人 (Person)
  [Node 23] 太郎の名前 (Object)
  [Node 24] 太郎の家族の名前 (Object)
  [Node 25] 何百年もの時 (Event)
  [Node 26] 自身 (Person)
  [Node 27] (誰か) (Person)
  [Node 28] 「開けてはいけない」 (Event)
  [Node 29] 「中を見れば何か分かるかもしれない」 (Event)
  [Node 30] 白い煙 (Object)
  [Node 31] 中 (Place)
  [Node 32] 太郎の体 (Object)
  [Node 33] しわだらけの老人の姿 (Object)

代表的なエッジ例(最大10件):
  浦島太郎 --見つけた--> 亀 (sent #0)
  亀 --いじめられている--> 子どもたち (sent #0)
  太郎 --止めた--> 子どもたち (sent #1)
  太郎 --助けた--> 亀 (sent #1)
  太郎 --返した--> 海 (sent #1)
  太郎 --漁に出ている--> 船 (sent #2)
  亀 --姿を現した--> 太郎の前 (sent #2)
  亀 --助けてくれたお礼に案内した--> 太郎 (sent #2)
  亀 --案内した--> 竜宮城 (sent #2)
  太郎 --乗った--> 亀の背中 (sent #3)

==============================
🔵 GraphRAGで答える(単純な3問)
==============================

Q: 浦島太郎が浜辺で助けた生き物は何ですか?
A: 亀
📌 ヒットしたノード:
  - [Node 0 | 0.611] 浦島太郎 (Person)
  - [Node 1 | 0.438] 亀 (Creature)
  - [Node 8 | 0.401] 亀の背中 (Object)
📌 参照されたエッジ:
  - 浦島太郎 --見つけた--> 亀 (from sentence #0)
  - 亀 --いじめられている--> 子どもたち (from sentence #0)
  - 太郎 --助けた--> 亀 (from sentence #1)
  - 亀 --姿を現した--> 太郎の前 (from sentence #2)
  - 亀 --助けてくれたお礼に案内した--> 太郎 (from sentence #2)
  - 亀 --案内した--> 竜宮城 (from sentence #2)
  - 太郎 --乗った--> 亀の背中 (from sentence #3)

Q: 浦島太郎が招かれた海の底の宮殿の名前は何ですか?
A: 竜宮城
📌 ヒットしたノード:
  - [Node 0 | 0.510] 浦島太郎 (Person)
  - [Node 9 | 0.434] 海の底 (Place)
  - [Node 7 | 0.385] 竜宮城 (Place)
📌 参照されたエッジ:
  - 浦島太郎 --見つけた--> 亀 (from sentence #0)
  - 亀 --案内した--> 竜宮城 (from sentence #2)
  - 太郎 --たどり着いた--> 竜宮城 (from sentence #3)
  - 竜宮城 --ある場所--> 海の底 (from sentence #3)
  - 太郎 --過ごした場所--> 竜宮城 (from sentence #5)
  - 太郎 --過ごした--> 竜宮城 (from sentence #11)

Q: 乙姫さまがお土産として渡した箱の名前は何ですか?
A: 玉手箱
📌 ヒットしたノード:
  - [Node 10 | 0.523] 乙姫さま (Person)
  - [Node 15 | 0.512] 玉手箱 (Object)
  - [Node 18 | 0.408] 箱 (Object)
📌 参照されたエッジ:
  - 乙姫さま --迎えた--> 太郎 (from sentence #4)
  - 乙姫さま --もてなした--> 踊り (from sentence #4)
  - 乙姫さま --もてなした--> ごちそう (from sentence #4)
  - 太郎 --帰りたいと伝えた--> 乙姫さま (from sentence #5)
  - 乙姫さま --持っている--> 玉手箱 (from sentence #6)
  - (話者) --渡した--> 箱 (from sentence #7)
  - 箱 --渡された相手--> 太郎 (from sentence #7)
  - 乙姫さま --渡した--> 玉手箱 (from sentence #12)
  - 太郎 --思い出した--> 玉手箱 (from sentence #12)
  - (誰か) --開けてしまった--> 箱 (from sentence #13)

==============================
🔴 GraphRAG向き(因果・多段推論)2問
==============================

Q: 浦島太郎が竜宮城に行くことになったきっかけは何ですか?
A: 亀を助けたこと
📌 ヒットしたノード:
  - [Node 0 | 0.568] 浦島太郎 (Person)
  - [Node 7 | 0.452] 竜宮城 (Place)
  - [Node 23 | 0.317] 太郎の名前 (Object)
📌 参照されたエッジ:
  - 浦島太郎 --見つけた--> 亀 (from sentence #0)
  - 亀 --案内した--> 竜宮城 (from sentence #2)
  - 太郎 --たどり着いた--> 竜宮城 (from sentence #3)
  - 竜宮城 --ある場所--> 海の底 (from sentence #3)
  - 太郎 --過ごした場所--> 竜宮城 (from sentence #5)
  - 周りの人 --知らなかった--> 太郎の名前 (from sentence #10)
  - 太郎 --過ごした--> 竜宮城 (from sentence #11)

Q: 浦島太郎が老人になってしまった原因は何ですか?
A: 玉手箱を開けたこと
📌 ヒットしたノード:
  - [Node 0 | 0.659] 浦島太郎 (Person)
  - [Node 33 | 0.445] しわだらけの老人の姿 (Object)
  - [Node 3 | 0.355] 太郎 (Person)
📌 参照されたエッジ:
  - 浦島太郎 --見つけた--> 亀 (from sentence #0)
  - 太郎 --止めた--> 子どもたち (from sentence #1)
  - 太郎 --助けた--> 亀 (from sentence #1)
  - 太郎 --返した--> 海 (from sentence #1)
  - 太郎 --漁に出ている--> 船 (from sentence #2)
  - 亀 --助けてくれたお礼に案内した--> 太郎 (from sentence #2)
  - 太郎 --乗った--> 亀の背中 (from sentence #3)
  - 太郎 --たどり着いた--> 竜宮城 (from sentence #3)
  - 乙姫さま --迎えた--> 太郎 (from sentence #4)
  - 太郎 --過ごした--> 楽しい日々 (from sentence #5)
  - 太郎 --過ごした場所--> 竜宮城 (from sentence #5)
  - 太郎 --感じた--> そろそろ村の家族のことが心配になってきた (from sentence #5)
  - 太郎 --帰りたいと伝えた--> 乙姫さま (from sentence #5)
  - 箱 --渡された相手--> 太郎 (from sentence #7)
  - 太郎 --戻った--> 地上 (from sentence #8)
  - 太郎 --尋ねた--> 周りの人 (from sentence #10)
  - 太郎 --過ごした--> 竜宮城 (from sentence #11)
  - 太郎 --途方に暮れた--> 自身 (from sentence #12)
  - 太郎 --思い出した--> 玉手箱 (from sentence #12)
  - 太郎の体 --なった--> しわだらけの老人の姿 (from sentence #14)
✅ Using JP font: /System/Library/Fonts/ヒラギノ角ゴシック W6.ttc (family=Hiragino Sans)

🖼 ナレッジグラフを urashimataro_graph.png として保存しました。

実際に作成されたナレッジグラフは下記です。
RAGで回答できなかった部分も正しく回答できています。
純粋なRAGだとチャンク間の関係性を考慮した回答ができませんでしたが、
GraphRAGだと学習時に作成しているため回答できています。
image.png

最後に

いかがでしたか?RAGで対応できない事もGraphRAGであれば回答できることがあります!!

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?