[LLM][Python] キャラクター比較におけるGraphRAGの有用性を検証してみた

## はじめに
　LLMの精度向上にRAGがよく用いられているが、より精度が高いAdvanced RAGとしてGraphRAGが注目されている。
　通常株価変動や薬効など因果関係が明確なものに有効らしいが、今回は因果関係が明確でないものについてどのようになるか検証するため、尾田栄一郎氏の『ONE PIECE』におけるキャラクターの戦闘結果予想を行わせることを想定し、キャラクター名をキーワードにナレッジグラフから関係性を出力した。
　検証した際のプログラムと検証結果および所感について、備忘録として記していく。

## 注意
　今回作成したナレッジグラフについて、あくまでもGraphRAGへの技術的理解を深めるために作成した事例であり、『ONE PIECE』（尾田栄一郎／集英社）のキャラクター名・用語を参照しているが、記載内容は筆者による構成であり、公式とは一切関係ないものである。

## GraphRAGとは
　GraphRAGとはAdvanced RAGの一つで、3つのステップに分かれてLLMにリクエストを投げる手法である。
 　①各ノード間の関係性を示すナレッジグラフからキーワード（ノード）をもとに、キーワード間の関係性を抽出する。
　②①で抽出した関係性を、LLMに投げるための文章に変更する。
　③②の結果をリクエストに埋め込み、LLMに投げる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1391358/b8e3076a-bc24-43f3-a92d-2ba2ff3f9c8c.png)


 
## 環境情報
```requirements.txt
contourpy==1.3.2
cycler==0.12.1
fonttools==4.57.0
kiwisolver==1.4.8
matplotlib==3.10.1
networkx==3.4.2
numpy==2.2.5
packaging==25.0
pillow==11.2.1
psycopg2-binary==2.9.10
pyparsing==3.2.3
python-dateutil==2.9.0.post0
setuptools==75.8.0
six==1.17.0
wheel==0.45.1
```

## プログラム
```test.py
import networkx as nx

# フル知識グラフ
G = nx.DiGraph()

# ───── ルフィと技・能力 ─────
G.add_edge("モンキー・D・ルフィ", "四皇", relation="所属する")
G.add_edge("四皇", "海賊王に最も近い", relation="状態")
G.add_edge("モンキー・D・ルフィ", "ゾオン系ヒトヒトの実幻獣種モデルニカ", relation="所有")
G.add_edge("ゾオン系ヒトヒトの実幻獣種モデルニカ", "身体性質がゴムになる", relation="能力")
G.add_edge("モンキー・D・ルフィ", "ギア2nd", relation="所有")
G.add_edge("ギア2nd", "高速移動", relation="能力")
G.add_edge("モンキー・D・ルフィ", "ギア3rd", relation="所有")
G.add_edge("ギア3rd", "身体の巨人化", relation="能力")
G.add_edge("ギア3rd", "一定時間経過後の子供化", relation="弱点")
G.add_edge("モンキー・D・ルフィ", "ギア4th", relation="所有")
G.add_edge("ギア4th", "バウンドマン", relation="能力")
G.add_edge("ギア4th", "スネークマン", relation="能力")
G.add_edge("バウンドマン", "空中移動", relation="能力")
G.add_edge("バウンドマン", "超筋力", relation="能力")
G.add_edge("バウンドマン", "ゴムゴムの大猿王銃", relation="必殺技")
G.add_edge("バウンドマン", "覇気の大量消費", relation="弱点")
G.add_edge("スネークマン", "数秒後の未来予知", relation="能力")
G.add_edge("スネークマン", "蛇のような動きのパンチ", relation="能力")
G.add_edge("スネークマン", "覇気の大量消費", relation="弱点")
G.add_edge("モンキー・D・ルフィ", "ギア5th", relation="所有")
G.add_edge("ギア5th", "物理法則を無視した戦闘", relation="能力")
G.add_edge("ギア5th", "周囲をゴムのように変形させる", relation="能力")
G.add_edge("ギア5th", "カートゥーン調で戦う", relation="能力")
G.add_edge("ギア5th", "巨大化", relation="能力")
G.add_edge("ギア5th", "ゴムゴムの猿神銃", relation="必殺技")
G.add_edge("ギア5th", "覇気の大量消費", relation="弱点")

# ───── 覇気 ─────
G.add_edge("モンキー・D・ルフィ", "見聞色の覇気", relation="所有")
G.add_edge("モンキー・D・ルフィ", "武装色の覇気", relation="所有")
G.add_edge("モンキー・D・ルフィ", "覇王色の覇気", relation="所有")
G.add_edge("モンキー・D・ルフィ", "流桜", relation="所有")
G.add_edge("見聞色の覇気", "索敵", relation="能力")
G.add_edge("武装色の覇気", "武器・身体強化", relation="能力")
G.add_edge("覇王色の覇気", "武器・身体強化および弱者の制圧", relation="能力")
G.add_edge("覇気の大量消費", "身動きがとれない", relation="状態")
G.add_edge("流桜", "対象へ接触せずに内部破壊", relation="能力")

# ───── 青雉 ─────
G.add_edge("青雉", "黒ひげ海賊団", relation="所属する")
G.add_edge("黒ひげ海賊団", "四皇の海賊団", relation="状態")
G.add_edge("青雉", "ロギア系ヒエヒエの実", relation="所有")
G.add_edge("ロギア系ヒエヒエの実", "肉体性質を氷に変質させることで、周囲を凍らせる", relation="能力")
G.add_edge("ロギア系ヒエヒエの実", "覇気がない物理攻撃が効かない", relation="能力")
G.add_edge("ロギア系ヒエヒエの実", "自身に触れた相手を凍らせる", relation="能力")
G.add_edge("ロギア系ヒエヒエの実", "凍った対象の強度を脆くできる", relation="能力")
G.add_edge("青雉", "軍艦をサンドバッグにして鍛えた腕力", relation="所有")
G.add_edge("ロギア系ヒエヒエの実", "強力な覇気をまとった対象を凍らせることができない", relation="弱点")
G.add_edge("ロギア系ヒエヒエの実", "超高温の炎", relation="弱点")
G.add_edge("武装色の覇気", "青雉", relation="使用者")  
G.add_edge("見聞色の覇気", "青雉", relation="使用者")  

# ───── ナレーションテンプレート ─────
verb_templates = {
    "所属する": "{src}は{dst}に所属する",
    "状態": "{dst}は{src}である",
    "弱点": "{src}は{dst}に弱い",
    "必殺技": "{src}は{dst}が必殺技だ",
    "所有": "{src}は{dst}を持つ",
    "能力": "{src}は{dst}の能力を持つ",
    "使用者": "{src}は{dst}によって使用されている",
}

# ───── 経路探索 ─────
start_node = "モンキー・D・ルフィ"
end_node = "青雉"
paths = []

if nx.has_path(G, start_node, end_node):
    paths.extend(nx.all_simple_paths(G, source=start_node, target=end_node))

# ───── ナレーション生成関数 ─────
def explain_path(G, path):
    explanation = []
    for i in range(len(path) - 1):
        src = path[i]
        dst = path[i + 1]
        relation = G.edges[src, dst].get('relation', '関連')
        sentence = verb_templates.get(relation, f"{src}は{dst}と関連している")
        explanation.append(sentence.format(src=src, dst=dst))
    return " → ".join(explanation)

# ───── 出力 ─────
if not paths:
    print("⚠ モンキー・D・ルフィから青雉への経路は存在しません。")
else:
    for i, path in enumerate(paths):
        print(f"\n【経路{i+1}】:", " → ".join(path))
        print("説明:", explain_path(G, path))

```

## 結果
　ナレッジグラフについて、「モンキー・D・ルフィ」「青雉」のキーワードで検索をしたが、２人の共通点が所有能力のみだったため、２人が戦闘した場合の直接的な補足情報になりえるものは取得されず、LLMに与える参考情報として有用なものは取得できなかった。
 　キャラクター情報は明確な因果関係がなく、知識のリンク密度が低いもので、単純に情報を列挙しただけのものだったため、意義ある構造にならなかったからのようだ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1391358/fbc1c59d-8ba4-42b0-95c1-6f13de2dc85b.png)


　なお今回GraphRAGの検証を目的としていたため、本来はナレッジグラフをもとにしたLLMへのリクエスト結果まで確認すべきだが、ナレッジグラフの出力結果の時点で今回の命題に対して有効でなさそうだと判断したため、LLMへのリクエストは実施していない。
 
## 所感
　一般的にはAdvanced RAGは通常のRAGよりも優れていると言われているが、やはり向き不向きがあるので、明確な因果関係のような関係性があるものに対して利用しない限り、GraphRAGは有効とは言えない。まずはキーワードからどんなナレッジグラフになりそうか、矢印の方向はどうなりそうか、具体的な検証をしたうえでGraphRAGの利用有無を検証したほうが良さそうだ。
　また、通常のRAG同様に参考情報を取得してきているだけで大きな差異はないようで、COS類似度で紐づく情報を取得するか、キーワードから関連性を取得してきているかの違いだけのようだ。この関連性が明確かつ簡潔に与えられることでLLM側の精度向上に寄与していると思われるが、仮に通常のRAGで取得できる情報が明確かつ簡潔な関連性に関するものだった場合、GraphRAGと相違ないものだと考えられるので、ナレッジグラフの作成と通常のRAGで取得する文章内容の検討のどちらが簡易的か否かで選択することになりそうだ。ただし、ナレッジグラフではノード間の結びつきが視覚的に明らかなので、COS類似度なので結びつきを確認するよりも、人間が理解することは容易だろう。
 　
## 参考資料
[1]本橋 和貴, 『RAG の精度を向上させる Advanced RAG on AWS の道標』,https://aws.amazon.com/jp/blogs/news/a-practical-guide-to-improve-rag-systems-with-advanced-rag-on-aws/
[2] ksonoda(Kenichi Sonoda), 『GraphRAGをわかりやすく解説』,　https://qiita.com/ksonoda/items/98a6607f31d0bbb237ef#graphrag%E3%81%AE%E3%81%97%E3%81%8F%E3%81%BF
[3]尾田栄一郎（著）『ONE PIECE』集英社、1997年～連載中
[4] ONE PIECE 悪魔の実とかのINDEX,『ルフィの技一覧【ギア別】基本技から2、3、4、5』,https://onepiece-akumanomi.com/luffy-move-gear.html