元のドキュメント (HTML、PDF、Markdown など) からテキストを抽出します。
ドキュメントの構造とセマンティクスに基づいて、テキストを特定のサイズに分割します。
チャンクの埋め込みをキーとして、チャンクをベクトル データベースに保存します。
回答を生成する際のコンテキストとして使用するために、質問に関連するチャンクを取得します。
ただし、ベクトル類似性に基づく RAG にはいくつかの弱点があります。質問に類似した情報に焦点を当てているため、たとえば、複数のトピックを含む質問や複数のホップを必要とする質問に答えるのが難しくなります。さらに、取得されるチャンクの数も制限されます。
各チャンクは個別のソースから取得されるため、ほぼ同様の情報が複数の場所に存在する場合は、情報の複数のコピーを取得するか(他の情報が失われる可能性あり)、より多くの異なるチャンクを取得するために 1 つのコピーのみを選択する必要があり、その場合、他のソースのニュアンスが失われます。
このアプローチには、類似性ベースのアプローチに比べていくつかの利点があります。
多くの事実を単一のソースから抽出し、ナレッジ グラフ内のさまざまなエンティティに関連付けることができます。これにより、無関係な情報を含むチャンク全体ではなく、特定のソースから関連する事実のみを取得できます。
複数のソースが同じことを言っている場合、同じノードまたはエッジが生成されます。これらを別個の事実として扱う (および複数のコピーを取得する) 代わりに、同じノードまたはエッジとして扱い、1 回だけ取得することができます。これにより、より多様な事実を取得したり、複数のソースに現れる事実のみに焦点を当てたりすることができます。
グラフは複数のステップで走査される可能性があり、質問内のエンティティに直接関連する情報を取得するだけでなく、2 ステップまたは 3 ステップ離れたものも取得します。従来の RAG アプローチでは、これには複数回のクエリが必要になります。
RAG にナレッジ グラフを使用する利点に加えて、LLM によってナレッジ グラフの作成も容易になりました。ナレッジ グラフを慎重に作成するために専門家を必要とするのではなく、LLM とプロンプトを使用してドキュメントから情報を抽出できます。
この投稿では、RAGのナレッジグラフの使用について説明します。
次に、質問からエンティティを抽出し、関連するサブグラフを取得するための LangChain ランナブルを作成します。ナレッジ グラフを使用して RAG を実装するために必要な操作には、グラフ データベースやグラフ クエリ言語は必要なく、既に使用している可能性のある一般的なデータ ストアを使用してこのアプローチを適用できることがわかります。
前述のように、ナレッジ グラフは個別のエンティティをノードとして表します。たとえば、ノードは人物の「マリー キュリー」や言語の「フランス語」を表すことができます。LangChain では、各ノードに名前とタイプがあります。ノードを一意に識別するときには、言語の「フランス語」と国籍の「フランス語」を区別するために、両方を考慮します。
エンティティ間の関係は、グラフのエッジに対応します。各エッジには、ソース (たとえば、マリー キュリーという人物)、ターゲット (ノーベル賞という賞)、およびソースとターゲットの関係を示すタイプ (たとえば、「受賞」) が含まれます。
LangChain を使用して Marie Curie に関する段落から抽出されたナレッジ グラフの例を以下に示します。
目的に応じて、ノードとエッジにプロパティを追加することもできます。たとえば、プロパティを使用して、ノーベル賞がいつ受賞されたか、およびカテゴリを識別することができます。これらは、取得中にグラフをトラバースするときに、エッジとノードをフィルター処理するのに役立ちます。
ナレッジ グラフを構成するエンティティと関係は、直接作成することも、既存の既知の適切なデータ ソースからインポートすることもできます。これは、ナレッジを慎重にキュレートしたい場合に便利ですが、新しい情報をすばやく組み込んだり、大量の情報を処理したりすることが難しくなります。
幸いなことに、LLM を使用するとコンテンツから情報を簡単に抽出できるため、ナレッジ グラフの抽出に使用できます。
以下では、
LangChainは次のような他のオプションもサポートしています
from langchain_experimental.graph_transformers import LLMGraphTransformer from langchain_openai import ChatOpenAI from langchain_core.documents import Document # Prompt used by LLMGraphTransformer is tuned for Gpt4. llm = ChatOpenAI(temperature=0, model_name="gpt-4") llm_transformer = LLMGraphTransformer(llm=llm) text = """ Marie Curie, was a Polish and naturalised-French physicist and chemist who conducted pioneering research on radioactivity. She was the first woman to win a Nobel Prize, the first person to win a Nobel Prize twice, and the only person to win a Nobel Prize in two scientific fields. Her husband, Pierre Curie, was a co-winner of her first Nobel Prize, making them the first-ever married couple to win the Nobel Prize and launching the Curie family legacy of five Nobel Prizes. She was, in 1906, the first woman to become a professor at the University of Paris. """ documents = [Document(page_content=text)] graph_documents = llm_transformer.convert_to_graph_documents(documents) print(f"Nodes:{graph_documents[0].nodes}") print(f"Relationships:{graph_documents[0].relationships}")
これは、LangChain のLLMGraphTransformer
を使用してナレッジ グラフを抽出する方法を示しています。リポジトリにあるrender_graph_document
を使用して、視覚的な検査用に LangChain GraphDocument
をレンダリングできます。
今後の記事では、ナレッジ グラフ全体と各ドキュメントから抽出されたサブグラフの両方を調べる方法と、プロンプト エンジニアリングとナレッジ エンジニアリングを適用して自動抽出を改善する方法について説明します。
ナレッジ グラフを使用して質問に答えるには、いくつかの手順が必要です。まず、ナレッジ グラフのトラバースを開始する場所を特定します。この例では、LLM に質問からエンティティを抽出するように指示します。次に、ナレッジ グラフをトラバースして、開始点から指定された距離内のすべての関係を取得します。デフォルトのトラバース深度は 3 です。取得した関係と元の質問を使用して、LLM が質問に答えるためのプロンプトとコンテキストが作成されます。
ナレッジ グラフの抽出と同様に、質問内のエンティティの抽出は、特別なモデルまたは特定のプロンプトを持つ LLM を使用して実行できます。簡単にするために、質問と抽出する形式に関する情報の両方を含む次のプロンプトを持つ LLM を使用します。適切な構造を取得するために、名前とタイプを持つ Pydantic モデルを使用します。
QUERY_ENTITY_EXTRACT_PROMPT = ( "A question is provided below. Given the question, extract up to 5 " "entity names and types from the text. Focus on extracting the key entities " "that we can use to best lookup answers to the question. Avoid stopwords.\n" "---------------------\n" "{question}\n" "---------------------\n" "{format_instructions}\n" ) def extract_entities(llm): prompt = ChatPromptTemplate.from_messages([keyword_extraction_prompt]) class SimpleNode(BaseModel): """Represents a node in a graph with associated properties.""" id: str = Field(description="Name or human-readable unique identifier.") type: str = optional_enum_field(node_types, description="The type or label of the node.") class SimpleNodeList(BaseModel): """Represents a list of simple nodes.""" nodes: List[SimpleNode] output_parser = JsonOutputParser(pydantic_object=SimpleNodeList) return ( RunnablePassthrough.assign( format_instructions=lambda _: output_parser.get_format_instructions(), ) | ChatPromptTemplate.from_messages([QUERY_ENTITY_EXTRACT_PROMPT]) | llm | output_parser | RunnableLambda( lambda node_list: [(n["id"], n["type"]) for n in node_list["nodes"]]) )
上記の例を実行すると、抽出されたエンティティを確認できます。
# Example showing extracted entities (nodes) extract_entities(llm).invoke({ "question": "Who is Marie Curie?"}) # Output: [Marie Curie(Person)]
もちろん、LangChain Runnable をチェーン内で使用して、質問からエンティティを抽出することもできます。
今後は、ノード プロパティを考慮したり、ベクトル埋め込みや類似性検索を使用して関連する開始点を特定したりするなど、エンティティ抽出を改善する方法について説明します。この最初の投稿をシンプルにするために、上記のプロンプトを使用し、ナレッジ グラフをトラバースしてknowledge-subgraph
を取得し、それをプロンプトのコンテキストとして含めることに進みましょう。
前のチェーンにより、問題のノードが提供されます。これらのエンティティとグラフ ストアを使用して、関連する知識トリプルを取得できます。RAG と同様に、それらをコンテキストの一部としてプロンプトにドロップし、回答を生成します。
def _combine_relations(relations): return "\n".join(map(repr, relations)) ANSWER_PROMPT = ( "The original question is given below." "This question has been used to retrieve information from a knowledge graph." "The matching triples are shown below." "Use the information in the triples to answer the original question.\n\n" "Original Question: {question}\n\n" "Knowledge Graph Triples:\n{context}\n\n" "Response:" ) chain = ( { "question": RunnablePassthrough() } # extract_entities is provided by the Cassandra knowledge graph library # and extracts entitise as shown above. | RunnablePassthrough.assign(entities = extract_entities(llm)) | RunnablePassthrough.assign( # graph_store.as_runnable() is provided by the CassandraGraphStore # and takes one or more entities and retrieves the relevant sub-graph(s). triples = itemgetter("entities") | graph_store.as_runnable()) | RunnablePassthrough.assign( context = itemgetter("triples") | RunnableLambda(_combine_relations)) | ChatPromptTemplate.from_messages([ANSWER_PROMPT]) | llm )
上記のチェーンを実行して質問に答えることができます。例:
chain.invoke("Who is Marie Curie?") # Output AIMessage( content="Marie Curie is a Polish and French chemist, physicist, and professor who " "researched radioactivity. She was married to Pierre Curie and has worked at " "the University of Paris. She is also a recipient of the Nobel Prize.", response_metadata={ 'token_usage': {'completion_tokens': 47, 'prompt_tokens': 213, 'total_tokens': 260}, 'model_name': 'gpt-4', ... } )
ナレッジ グラフの保存にグラフ DB を使用するのは直感的に思えるかもしれませんが、実際には必要ありません。いくつかのノードの周囲のサブナレッジ グラフを取得するのは単純なグラフ トラバーサルですが、グラフ DB は、特定のプロパティのシーケンスを持つパスを検索する、はるかに複雑なクエリ用に設計されています。さらに、トラバーサルは多くの場合、深さ 2 または 3 までしか行われません。これは、離れたノードはすぐに質問に無関係になるためです。これは、単純なクエリを数回実行すること (各ステップに 1 つ) または SQL 結合として表現できます。
個別のグラフ データベースが不要になると、ナレッジ グラフの使用が容易になります。さらに、Astra DB または Apache Cassandra を使用すると、グラフと同じ場所に保存されている他のデータへのトランザクション書き込みが簡素化され、スケーラビリティが向上する可能性があります。このオーバーヘッドは、Gremlin や Cypher などの類似のものを使用してグラフ クエリを生成および実行することを計画している場合にのみ価値があります。
しかし、これはサブ知識グラフを取得するには過剰であり、パフォーマンスの面で軌道から外れるクエリなど、他の多くの問題を引き起こす原因となります。
このトラバーサルはPythonで簡単に実装できます。CQLとCassandraドライバーを使用してこれを実装する完全なコード(同期と非同期の両方)は、
def fetch_relation(tg: asyncio.TaskGroup, depth: int, source: Node) -> AsyncPagedQuery: paged_query = AsyncPagedQuery( depth, session.execute_async(query, (source.name, source.type)) ) return tg.create_task(paged_query.next()) results = set() async with asyncio.TaskGroup() as tg: if isinstance(start, Node): start = [start] discovered = {t: 0 for t in start} pending = {fetch_relation(tg, 1, source) for source in start} while pending: done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED) for future in done: depth, relations, more = future.result() for relation in relations: results.add(relation) # Schedule the future for more results from the same query. if more is not None: pending.add(tg.create_task(more.next())) # Schedule futures for the next step. if depth < steps: # We've found a path of length `depth` to each of the targets. # We need to update `discovered` to include the shortest path. # And build `to_visit` to be all of the targets for which this is # the new shortest path. to_visit = set() for r in relations: previous = discovered.get(r.target, steps + 1) if depth < previous: discovered[r.target] = depth to_visit.add(r.target) for source in to_visit: pending.add(fetch_relation(tg, depth + 1, source)) return results
この記事では、質問への回答のためにナレッジ グラフの抽出と取得を構築して使用する方法を説明しました。重要な点は、現在これを行うために、Gremlin や Cypher などのグラフ クエリ言語を備えたグラフ データベースは必要ないということです。多くのクエリを効率的に並列処理する Astra などの優れたデータベースは、すでにこれを処理できます。
実際、特定のクエリに答えるために必要なサブ知識グラフを取得するには、単純なクエリシーケンスを書くだけで済みます。これにより、アーキテクチャがシンプルになり(依存関係が追加されず)、
私たちは、同じアイデアを使用して Cassandra と Astra DB の GraphRAG パターンを実装しました。私たちはこれを LangChain に提供し、将来的には LLM によるナレッジ グラフの使用に他の改善をもたらすことに取り組んでいきます。
ベン・チェンバース、DataStax