paint-brush
RAG アプリケーションの精度を解き放つ: Neo4j と LangChain によるナレッジ グラフの活用@neo4j
220 測定値

RAG アプリケーションの精度を解き放つ: Neo4j と LangChain によるナレッジ グラフの活用

Neo4j9m2024/10/21
Read on Terminal Reader

長すぎる; 読むには

このブログ投稿では、LangChain を使用してナレッジ グラフを作成する方法を説明します。コードは GitHub で入手できます。Neo4j インスタンスをセットアップする必要があります。このデモでは、[エリザベス 1 世] の Wikipedia ページを使用します。[LangChain ローダー] を使用して、Wikipedia からドキュメントを取得して分割できます。
featured image - RAG アプリケーションの精度を解き放つ: Neo4j と LangChain によるナレッジ グラフの活用
Neo4j HackerNoon profile picture
0-item
1-item


グラフ検索拡張生成 ( GraphRAG ) は勢いを増しており、従来のベクトル検索検索方法に強力な追加機能として加わりつつあります。このアプローチは、データをノードと関係として整理するグラフ データベースの構造化された性質を活用して、取得された情報の深さとコンテキストを強化します。



ナレッジグラフの例。

グラフは、異種で相互接続された情報を構造化された方法で表現および保存するのに優れており、さまざまなデータ タイプにわたる複雑な関係や属性を簡単にキャプチャできます。対照的に、ベクター データベースは、高次元ベクターを介して非構造化データを処理することに強みがあるため、このような構造化情報を扱うのに苦労することがよくあります。RAG アプリケーションでは、構造化グラフ データと非構造化テキストによるベクター検索を組み合わせて、両方の長所を実現できます。このブログ投稿では、その点について説明します。

ナレッジグラフは素晴らしいですが、どうやって作成するのでしょうか?

ナレッジ グラフの構築は、通常、最も難しいステップです。データの収集と構造化が伴い、ドメインとグラフ モデリングの両方に対する深い理解が必要です。


このプロセスを簡素化するために、私たちは LLM の実験を行ってきました。言語とコンテキストを深く理解する LLM は、ナレッジ グラフ作成プロセスの重要な部分を自動化できます。テキスト データを分析することで、これらのモデルはエンティティを識別し、それらの関係を理解し、それらをグラフ構造で最適に表現する方法を提案できます。


これらの実験の結果、グラフ構築モジュールの最初のバージョンが LangChain に追加されました。このブログ投稿ではこれについて説明します。


コードはGitHubで入手できます。

Neo4j 環境のセットアップ

Neo4j インスタンスをセットアップする必要があります。このブログ投稿の例に従ってください。最も簡単な方法は、Neo4j データベースのクラウド インスタンスを提供するNeo4j Auraで無料インスタンスを開始することです。または、 Neo4j デスクトップアプリケーションをダウンロードしてローカル データベース インスタンスを作成し、Neo4j データベースのローカル インスタンスをセットアップすることもできます。


 os.environ["OPENAI_API_KEY"] = "sk-" os.environ["NEO4J_URI"] = "bolt://localhost:7687" os.environ["NEO4J_USERNAME"] = "neo4j" os.environ["NEO4J_PASSWORD"] = "password" graph = Neo4jGraph()


さらに、このブログ記事では OpenAI のモデルを使用するため、 OpenAI キーを提供する必要があります。

データの取り込み

このデモでは、エリザベス 1 世のWikipedia ページを使用します。LangChainローダーを使用して、Wikipedia からドキュメントをシームレスに取得および分割できます。


 # Read the wikipedia article raw_documents = WikipediaLoader(query="Elizabeth I").load() # Define chunking strategy text_splitter = TokenTextSplitter(chunk_size=512, chunk_overlap=24) documents = text_splitter.split_documents(raw_documents[:3])


取得したドキュメントに基づいてグラフを構築します。この目的のために、グラフ データベースでのナレッジ グラフの構築と保存を大幅に簡素化する LLMGraphTransformer モジュールを実装しました。


 llm=ChatOpenAI(temperature=0, model_name="gpt-4-0125-preview") llm_transformer = LLMGraphTransformer(llm=llm) # Extract graph data graph_documents = llm_transformer.convert_to_graph_documents(documents) # Store to neo4j graph.add_graph_documents( graph_documents, baseEntityLabel=True, include_source=True )


ナレッジグラフ生成チェーンで使用するLLMを定義できます。現在、OpenAIとMistralの関数呼び出しモデルのみをサポートしています。ただし、将来的にはLLMの選択範囲を拡張する予定です。この例では、最新のGPT-4を使用しています。生成されるグラフの品質は、使用しているモデルに大きく依存することに注意してください。理論的には、常に最も機能的なモデルを使用する必要があります。LLMグラフトランスフォーマーはグラフドキュメントを返します。これは、add_graph_documentsメソッドを介してNeo4jにインポートできます。baseEntityLabelパラメータは、追加のエンティティラベルを割り当てます。実在物各ノードにラベルを付けることで、インデックス作成とクエリのパフォーマンスが向上します。include_source パラメータはノードを元のドキュメントにリンクし、データの追跡可能性とコンテキストの理解を容易にします。


生成されたグラフは Neo4j ブラウザで検査できます。


生成されたグラフの一部。


この画像は生成されたグラフの一部のみを表していることに注意してください。


RAG のハイブリッド検索

グラフ生成後、RAG アプリケーションでは、ベクトル インデックスとキーワード インデックスをグラフ検索と組み合わせたハイブリッド検索アプローチを使用します。


ハイブリッド(ベクトル + キーワード)とグラフ検索方法の組み合わせ。画像は著者による。


この図は、ユーザーが質問を投げかけることから始まる検索プロセスを示しています。この質問は RAG リトリーバーに送られます。このリトリーバーは、キーワード検索とベクター検索を使用して非構造化テキスト データを検索し、それをナレッジ グラフから収集した情報と組み合わせます。Neo4j はキーワード インデックスとベクター インデックスの両方を備えているため、単一のデータベース システムで 3 つの検索オプションすべてを実装できます。これらのソースから収集されたデータは LLM に送られ、最終的な回答が生成されて提供されます。

非構造化データ取得ツール

Neo4jVector.from_existing_graph メソッドを使用して、ドキュメントにキーワードとベクターの両方の取得を追加できます。このメソッドは、Document というラベルの付いたノードをターゲットとするハイブリッド検索アプローチ用に、キーワードとベクターの検索インデックスを構成します。さらに、テキスト埋め込み値が欠落している場合はそれを計算します。


 vector_index = Neo4jVector.from_existing_graph( OpenAIEmbeddings(), search_type="hybrid", node_label="Document", text_node_properties=["text"], embedding_node_property="embedding" )


その後、ベクトル インデックスは similarity_search メソッドで呼び出すことができます。

グラフレトリーバー

一方、グラフ取得の構成はより複雑ですが、より自由度が高くなります。この例では、フルテキスト インデックスを使用して関連するノードを識別し、それらの直接の近傍を返します。


グラフリトリーバー。画像は著者によるものです。



グラフ リトリーバーは、入力内の関連エンティティを識別することから始まります。簡単にするために、LLM に人、組織、場所を識別するように指示します。これを実現するには、新しく追加された with_structured_output メソッドを備えたLCELを使用します。


 # Extract entities from text class Entities(BaseModel): """Identifying information about entities.""" names: List[str] = Field( ..., description="All the person, organization, or business entities that " "appear in the text", ) prompt = ChatPromptTemplate.from_messages( [ ( "system", "You are extracting organization and person entities from the text.", ), ( "human", "Use the given format to extract information from the following " "input: {question}", ), ] ) entity_chain = prompt | llm.with_structured_output(Entities)


テストしてみましょう:


 entity_chain.invoke({"question": "Where was Amelia Earhart born?"}).names # ['Amelia Earhart']


質問内のエンティティを検出できるようになったので、フルテキスト インデックスを使用してエンティティをナレッジ グラフにマッピングしてみましょう。まず、フルテキスト インデックスと、多少のスペルミスを許容するフルテキスト クエリを生成する関数を定義する必要がありますが、ここでは詳しく説明しません。


 graph.query( "CREATE FULLTEXT INDEX entity IF NOT EXISTS FOR (e:__Entity__) ON EACH [e.id]") 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()



では、すべてをまとめてみましょう。


 # Fulltext index query def structured_retriever(question: str) -> str: """ Collects the neighborhood of entities mentioned in the question """ result = "" entities = entity_chain.invoke({"question": question}) for entity in entities.names: response = graph.query( """CALL db.index.fulltext.queryNodes('entity', $query, {limit:2}) YIELD node,score CALL { MATCH (node)-[r:!MENTIONS]->(neighbor) RETURN node.id + ' - ' + type(r) + ' -> ' + neighbor.id AS output UNION MATCH (node)<-[r:!MENTIONS]-(neighbor) RETURN neighbor.id + ' - ' + type(r) + ' -> ' + node.id AS output } RETURN output LIMIT 50 """, {"query": generate_full_text_query(entity)}, ) result += "\n".join([el['output'] for el in response]) return result



structured_retriever 関数は、まずユーザーの質問内のエンティティを検出します。次に、検出されたエンティティを反復処理し、Cypher テンプレートを使用して関連するノードの近傍を取得します。テストしてみましょう。


 print(structured_retriever("Who is Elizabeth I?")) # Elizabeth I - BORN_ON -> 7 September 1533 # Elizabeth I - DIED_ON -> 24 March 1603 # Elizabeth I - TITLE_HELD_FROM -> Queen Of England And Ireland # Elizabeth I - TITLE_HELD_UNTIL -> 17 November 1558 # Elizabeth I - MEMBER_OF -> House Of Tudor # Elizabeth I - CHILD_OF -> Henry Viii # and more...


ファイナルレトリーバー

冒頭で述べたように、非構造化リトリーバーとグラフ リトリーバーを組み合わせて、LLM に渡される最終的なコンテキストを作成します。


 def retriever(question: str): print(f"Search query: {question}") structured_data = structured_retriever(question) unstructured_data = [el.page_content for el in vector_index.similarity_search(question)] final_data = f"""Structured data: {structured_data} Unstructured data: {"#Document ". join(unstructured_data)} """ return final_data


Python を扱っているので、f 文字列を使用して出力を連結するだけです。

RAGチェーンの定義

RAG の検索コンポーネントを正常に実装しました。次に、統合ハイブリッド リトリーバーによって提供されるコンテキストを活用して応答を生成するプロンプトを導入し、RAG チェーンの実装を完了します。


 template = """Answer the question based only on the following context: {context} Question: {question} """ prompt = ChatPromptTemplate.from_template(template) chain = ( RunnableParallel( { "context": _search_query | retriever, "question": RunnablePassthrough(), } ) | prompt | llm | StrOutputParser() )


最後に、ハイブリッド RAG 実装をテストします。


 chain.invoke({"question": "Which house did Elizabeth I belong to?"}) # Search query: Which house did Elizabeth I belong to? # 'Elizabeth I belonged to the House of Tudor.'


また、クエリ書き換え機能も組み込んでおり、RAG チェーンがフォローアップの質問を許可する会話設定に適応できるようにしています。ベクター検索とキーワード検索の方法を使用しているため、検索プロセスを最適化するにはフォローアップの質問を書き換える必要があります。


 chain.invoke( { "question": "When was she born?", "chat_history": [("Which house did Elizabeth I belong to?", "House Of Tudor")], } ) # Search query: When was Elizabeth I born? # 'Elizabeth I was born on 7 September 1533.'


「When was she born?」が最初に「When was Elizabeth I born?」に書き換えられたことがわかります。その後、書き換えられたクエリを使用して、関連するコンテキストを取得し、質問に答えます。

ナレッジグラフを簡単に作成

LLMGraphTransformer の導入により、ナレッジ グラフの生成プロセスがよりスムーズかつアクセスしやすくなり、ナレッジ グラフが提供する深みとコンテキストを使用して RAG アプリケーションを強化したい人にとって、より簡単に実行できるようになります。これはほんの始まりに過ぎず、今後も多くの改善が計画されています。


LLM を使用したグラフ生成についてのご意見、ご提案、ご質問がございましたら、お気軽にお問い合わせください。


コードは以下から入手可能ですGitHub