グラフRAGでNeo4jを使った時につまづいたところ

# Neo4jでナレッジグラフ推論をしてみた

こんにちは、都内の大学に通う情報系の大学生です。最近、LLMを用いたRAGという単語やさらに先へ行ったグラフRAGという事もよく聞きます。このページではneo4jでナレッジグラフを構成し、RAGを動かしてみた場合につまづいた非本質（？）的なことをまとめようと思います。

## そもそもRAGとは

RAGというのは、LLMに専門知識を持たせて推論を行わせる手法の1つです。専門知識を何らかの方法で「参考文献」として作成し、プロンプトと一緒に与え、それを元に問題などを解いてもらうというものです。参考文献を作る手法として、オーソドックスなものはベクトル検索を用いるものがあります。手順は以下の通りです。

1. 参考文献のもととなるテキストを一定サイズのチャンクに分割する
2. LLMを用いてチャンクごとにエンべディングを計算しあるサイズのベクトルに変換する。（ベクトルサイズ）* （チャンク数）のデータを得る
3. 解きたい問題文など、知識を与えたい文章に対して、その文章のエンべディングを計算する
4. ２で計算したエンべディングから類似度（コサイン類似度など）が高いものを複数選び、参考文献を作成する
5. プロンプトに参考文献を与え、LLMに推論をさせる

細かい工夫を考えると際限がないですが、ざっくりまとめるとこんな感じです。この手法では４の検索においてチャンクごとに似ているかどうかの判定をしています。このベクトル検索の場合、以下の懸念が考えられます。

-　チャンクに分割して検索するので情報が分散している可能性がある。
-　類似度が高くても重要な情報を含んでいるとは限らない

これらの欠点を解消する手法としてナレッジグラフを用いるというものがあります。ナレッジグラフの定義はいろいろとあるのですが、基本的に知識（ナレッジ）をグラフの形で表現したものです。ノードに(固有)名詞などを配置し、それらの関係性の記述としてエッジ(リレーションシップ)で表現します。いろいろな説明がありますが、この記事がわかりやすいと思います。

https://www.rd.ntt/se/media/article/0025.html#:~:text=%E3%83%8A%E3%83%AC%E3%83%83%E3%82%B8%E3%82%B0%E3%83%A9%E3%83%95%E3%81%A8%E3%81%AF%E3%80%81%E3%81%95%E3%81%BE%E3%81%96%E3%81%BE,%E3%82%88%E3%81%86%E3%81%AB%E3%81%AA%E3%81%A3%E3%81%A6%E3%81%84%E3%81%BE%E3%81%99%E3%80%82

### グラフRAG
普通のRAGではベクトル検索によってデータベースから類似度の高いテキストを取得しそれを参考文献としてLLMに与えタスクを解かせます。検索手法としてベクトルに変換したものを対象とするのではなく、全文を検索できたらハッピーだと思いませんか！？グラフを使って検索を行い、RAGの精度を向上させる試みは数おおく行われており、MicrodoftはサービスとしてグラフRAGを開始しました。

https://microsoft.github.io/graphrag/


また、自分の手で構築してみたい場合、neo4jを使うと楽にグラフRAGが体感できます。チュートリアルはgithub上にあります。

https://github.com/tomasonjo/blogs/blob/master/llm/enhancing_rag_with_graph.ipynb

↑は英語文献を使ったものですが、日本語文献でも試した方もいらっしゃいます。

https://qiita.com/MaTTA_RUNTEQ50B/items/8a0370e04bc5376ca24c


英語文献を使うとこのコードは環境にもよりますが、おそらく動きます。しかし、日本語文献を扱う際に何個か躓いた点がありました。

### 非本質的なつまづき①：ノードにある単語なのに検索できない
neo4jではcypherを用いてクエリを作成し、ノードに存在する単語を検索して、それにつながるノードも一緒に取ってきます。このクエリを作成する際に、以下の手順を踏みます。
1. 質問文をLLMに投げ、エンティティを抽出する
2. エンティティをまとめる 

このエンティティをまとめる関数としてチュートリアルでは下の関数が用いられています。
```
def generate_full_text_query(input: str) -> str:
    """
    Generate a full-text search query for a given input string.

    This function constructs a query string suitable for a full-text search.
    It processes the input string by splitting it into words and appending a
    similarity threshold (~2 changed characters) to each word, then combines
    them using the AND operator. Useful for mapping entities from user questions
    to database values, and allows for some misspelings.
    """
    full_text_query = ""
    words = [el for el in remove_lucene_chars(input).split() if el]
    for word in words[:-1]:
        full_text_query += f" {word}~2 AND"
    full_text_query += f" {words[-1]}~2"
    return full_text_query.strip()
```
この関数では"~2"という文字列がエンティティに追加されています。cypherの仕様に詳しくないのではっきりとしたことは言えませんが、部分一致の程度を示しているのではと思います。この文字列があっても英文のエンティティは検索できるのですが、日本語文献をグラフに変換し、日本語の単語を検索した場合検索できないことがありました。"~2"を削除すると解決しました。

### 非本質的なつまづき②：パーサーがついていて、うまく動かない
日本語文献を使った記事の方では、LLMの出力にパーサーをつけていて、これが型の不整合を起こして動きませんでした。
```
parser = PydanticOutputParser(pydantic_object=Entities)
entity_chain = prompt | llm | parser
```
parserは文字列(string)を受け付けるのですが、なぜかllmの出力が16進数表記（unicode）になっておりうまく動きませんでした。このparserを削除して、with_structured_outputとすることで解決しました。
```
entity_chain = prompt | llm.with_structured_output
```

以上が非本質的なつまづきでした。初めてRAGタスクをしてみたので手探り状態でしたがいろいろチュートリアルが多いので助かりました。↑のチュートリアルが万能であればいいのに、、、