paint-brush
So nutzen Sie Wissensgraphen zur abruferweiterten Generierung – ohne Graph-DBvon@datastax
2,896 Lesungen
2,896 Lesungen

So nutzen Sie Wissensgraphen zur abruferweiterten Generierung – ohne Graph-DB

von DataStax12m2024/04/23
Read on Terminal Reader

Zu lang; Lesen

Dieser Beitrag untersucht die Verwendung von Wissensgraphen für RAG unter Verwendung von DataStax Astra DB zur Speicherung. Der Code für die Beispiele befindet sich in diesem Notebook und verwendet einen Prototypcode zum Speichern und Abrufen von Wissensgraphen mit Astra DB
featured image - So nutzen Sie Wissensgraphen zur abruferweiterten Generierung – ohne Graph-DB
DataStax HackerNoon profile picture

Abrufgestützte Generierung (RAG) bezieht sich auf eine Vielzahl von Techniken zum Abrufen von Informationen und deren Verwendung zur Bereitstellung von Kontextinformationen für generative KI. Die häufigste Form arbeitet mit Textblöcken und umfasst:


  1. Extrahieren des Textes aus den Originaldokumenten (HTML, PDF, Markdown usw.).


  2. Aufteilen des Textes in bestimmte Größen basierend auf der Dokumentstruktur und Semantik.


  3. Speichern von Chunks in einer Vektordatenbank mit Schlüsselwert durch die Einbettung des Chunks.


  4. Abrufen der für eine Frage relevanten Blöcke zur Verwendung als Kontext beim Generieren der Antwort.


Allerdings hat RAG auf Grundlage von Vektorähnlichkeit einige Schwächen. Da es sich auf Informationen konzentriert, die der Frage ähnlich sind, ist es beispielsweise schwieriger, Fragen zu beantworten, die mehrere Themen betreffen und/oder mehrere Hops erfordern. Darüber hinaus begrenzt es die Anzahl der abgerufenen Chunks.


Jeder Block stammt aus einer bestimmten Quelle. Wenn also weitgehend ähnliche Informationen an mehreren Stellen vorhanden sind, muss entschieden werden, ob mehrere Kopien der Informationen abgerufen werden sollen (wobei möglicherweise andere Informationen verloren gehen) oder ob nur eine Kopie ausgewählt wird, um mehr unterschiedliche Blöcke zu erhalten, wodurch die Nuancen der anderen Quellen verloren gehen.


Wissensgraphen kann als Alternative oder Ergänzung zur vektorbasierten Chunk-Abfrage verwendet werden. In einem Wissensgraphen entsprechen Knoten bestimmten Entitäten und Kanten zeigen Beziehungen zwischen den Entitäten an. Bei Verwendung für RAG werden für die Frage relevante Entitäten extrahiert und dann der Wissens-Subgraph abgerufen, der diese Entitäten und die Informationen über sie enthält.


Dieser Ansatz bietet gegenüber dem ähnlichkeitsbasierten Ansatz mehrere Vorteile:

  1. Viele Fakten können aus einer einzigen Quelle extrahiert und mit einer Vielzahl von Entitäten innerhalb des Wissensgraphen verknüpft werden. Dadurch können nur die relevanten Fakten aus einer bestimmten Quelle abgerufen werden, statt der gesamten Datenmenge, einschließlich irrelevanter Informationen.


  2. Wenn mehrere Quellen dasselbe aussagen, erzeugen sie denselben Knoten oder dieselbe Kante. Anstatt diese als unterschiedliche Fakten zu behandeln (und mehrere Kopien abzurufen), können sie als derselbe Knoten oder die gleiche Kante behandelt und nur einmal abgerufen werden. Dadurch können eine größere Vielfalt an Fakten abgerufen und/oder nur auf Fakten konzentriert werden, die in mehreren Quellen vorkommen.


  3. Der Graph kann in mehreren Schritten durchlaufen werden – dabei werden nicht nur Informationen abgerufen, die direkt mit den Entitäten in der Frage in Zusammenhang stehen, sondern auch Dinge, die 2 oder 3 Schritte entfernt sind. Bei einem herkömmlichen RAG-Ansatz würde dies mehrere Abfragerunden erfordern.


Zusätzlich zu den Vorteilen der Verwendung eines Wissensgraphen für RAG haben LLMs auch die Erstellung von Wissensgraphen vereinfacht. Anstatt Fachexperten zu benötigen, um den Wissensgraphen sorgfältig zu erstellen, können ein LLM und eine Eingabeaufforderung verwendet werden, um Informationen aus Dokumenten zu extrahieren.


Dieser Beitrag untersucht die Verwendung von Wissensgraphen für RAG unter Verwendung DataStax Astra DB zur Speicherung. Der Code für die Beispiele befindet sich hier Notizbuch Verwendung von Prototyp-Code zum Speichern und Abrufen von Wissensgraphen mit Astra DB von dieses Repository . Wir werden den „LLMGraphTransformer“ von LangChain nutzen, um Wissensgraphen aus Dokumenten zu extrahieren, sie in Astra zu schreiben und Techniken zum Optimieren der Eingabeaufforderung zu besprechen, die für die Wissensextraktion verwendet wird.


Anschließend erstellen wir LangChain-Runnables, um Entitäten aus der Frage zu extrahieren und die relevanten Untergraphen abzurufen. Wir werden sehen, dass die zur Implementierung von RAG mithilfe von Wissensgraphen erforderlichen Vorgänge keine Graphdatenbanken oder Graphabfragesprachen erfordern, sodass der Ansatz mithilfe eines typischen Datenspeichers angewendet werden kann, den Sie möglicherweise bereits verwenden.

Wissensgraph

Wie bereits erwähnt, stellt ein Wissensgraph unterschiedliche Entitäten als Knoten dar. Ein Knoten kann beispielsweise die Person „Marie Curie“ oder die Sprache „Französisch“ darstellen. In LangChain hat jeder Knoten einen Namen und einen Typ. Wir berücksichtigen beides bei der eindeutigen Identifizierung eines Knotens, um zwischen der Sprache „Französisch“ und der Nationalität „Französisch“ zu unterscheiden.


Beziehungen zwischen Entitäten entsprechen den Kanten im Diagramm. Jede Kante enthält die Quelle (z. B. die Person Marie Curie), das Ziel (die Auszeichnung Nobelpreis) und einen Typ, der angibt, in welcher Beziehung die Quelle zum Ziel steht (z. B. „gewonnen“).


Unten sehen Sie ein Beispiel für einen Wissensgraphen, der mit LangChain aus einem Absatz über Marie Curie extrahiert wurde:


Abhängig von Ihren Zielen können Sie Knoten und Kanten Eigenschaften hinzufügen. Sie können beispielsweise eine Eigenschaft verwenden, um anzugeben, wann der Nobelpreis verliehen wurde und in welcher Kategorie. Diese können beim Durchlaufen des Graphen während des Abrufs nützlich sein, um Kanten und Knoten herauszufiltern.

Extraktion: Erstellen des Wissensgraphen

Die Entitäten und Beziehungen, aus denen sich der Wissensgraph zusammensetzt, können direkt erstellt oder aus vorhandenen, als gut bekannten Datenquellen importiert werden. Dies ist nützlich, wenn Sie das Wissen sorgfältig kuratieren möchten, es erschwert jedoch die schnelle Einbindung neuer Informationen oder die Verarbeitung großer Informationsmengen.


Glücklicherweise erleichtern LLMs das Extrahieren von Informationen aus Inhalten, sodass wir sie zum Extrahieren des Wissensgraphen verwenden können.


Im Folgenden verwende ich die LLMGraphTransformer von LangChain, um aus einigen Informationen über Marie Curie ein Diagramm zu extrahieren. Dabei wird eine Eingabeaufforderung verwendet, um ein LLM anzuweisen, Knoten und Kanten aus einem Dokument zu extrahieren. Es kann mit jedem Dokument verwendet werden, das LangChain laden kann, und lässt sich daher leicht zu vorhandenen LangChain-Projekten hinzufügen.


LangChain unterstützt weitere Optionen wie DiffBot , und Sie können sich auch einige der verfügbaren Modelle zur Wissensextraktion ansehen, wie Rebell .


 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}")


Hier wird gezeigt, wie Sie mit dem LLMGraphTransformer von LangChain einen Wissensgraphen extrahieren. Sie können das im Repository enthaltene render_graph_document verwenden, um ein LangChain GraphDocument zur visuellen Überprüfung zu rendern.


In einem zukünftigen Beitrag besprechen wir, wie Sie den Wissensgraphen sowohl in seiner Gesamtheit als auch den aus jedem Dokument extrahierten Teilgraphen untersuchen können und wie Sie Prompt Engineering und Knowledge Engineering anwenden können, um die automatische Extraktion zu verbessern.

Abruf: Antworten mit dem Sub-Knowledge Graph

Das Beantworten von Fragen mithilfe des Wissensgraphen erfordert mehrere Schritte. Zunächst legen wir fest, wo wir mit der Durchquerung des Wissensgraphen beginnen. In diesem Beispiel fordere ich ein LLM auf, Entitäten aus der Frage zu extrahieren. Anschließend wird das Wissensgraph durchlaufen, um alle Beziehungen innerhalb einer bestimmten Entfernung von diesen Startpunkten abzurufen. Die Standarddurchquerungstiefe beträgt 3. Die abgerufenen Beziehungen und die ursprüngliche Frage werden verwendet, um eine Eingabeaufforderung und einen Kontext für das LLM zum Beantworten der Frage zu erstellen.

Extrahieren von Entitäten aus der Frage

Wie bei der Extraktion des Wissensgraphen kann das Extrahieren der Entitäten in einer Frage mithilfe eines speziellen Modells oder eines LLM mit einer bestimmten Eingabeaufforderung erfolgen. Der Einfachheit halber verwenden wir ein LLM mit der folgenden Eingabeaufforderung, die sowohl die Frage als auch Informationen zum zu extrahierenden Format enthält. Wir verwenden ein Pydantic-Modell mit dem Namen und Typ, um die richtige Struktur zu erhalten.

 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"]]) )


Wenn wir das obige Beispiel ausführen, können wir die extrahierten Entitäten sehen:

 # Example showing extracted entities (nodes) extract_entities(llm).invoke({ "question": "Who is Marie Curie?"}) # Output: [Marie Curie(Person)]


Natürlich kann ein LangChain Runnable in einer Kette verwendet werden, um die Entitäten aus einer Frage zu extrahieren.


In Zukunft werden wir Möglichkeiten zur Verbesserung der Entitätsextraktion diskutieren, z. B. die Berücksichtigung von Knoteneigenschaften oder die Verwendung von Vektoreinbettungen und Ähnlichkeitssuche zur Identifizierung relevanter Startpunkte. Um diesen ersten Beitrag einfach zu halten, bleiben wir bei der obigen Eingabeaufforderung und fahren mit der Durchquerung des Wissensgraphen fort, um den knowledge-subgraph abzurufen und diesen als Kontext in die Eingabeaufforderung aufzunehmen.

Abrufen des Sub-Knowledge-Graphen

Die vorherige Kette liefert uns die betreffenden Knoten. Wir können diese Entitäten und den Graphenspeicher verwenden, um die relevanten Wissenstripel abzurufen. Wie bei RAG fügen wir sie als Teil des Kontexts in die Eingabeaufforderung ein und generieren Antworten.

 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 )


Die obige Kette kann ausgeführt werden, um eine Frage zu beantworten. Beispiel:

 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', ... } )

Durchlaufen, nicht abfragen

Obwohl es intuitiv erscheinen mag, eine Graph-Datenbank zum Speichern des Wissensgraphen zu verwenden, ist dies eigentlich nicht notwendig. Das Abrufen des Teilwissensgraphen um einige Knoten herum ist eine einfache Graph-Traversierung, während Graph-Datenbanken für viel komplexere Abfragen konzipiert sind, die nach Pfaden mit bestimmten Eigenschaftssequenzen suchen. Außerdem erfolgt die Traversierung oft nur bis zu einer Tiefe von 2 oder 3, da weiter entfernte Knoten für die Frage ziemlich schnell irrelevant werden. Dies kann als einige Runden einfacher Abfragen (eine für jeden Schritt) oder als SQL-Join ausgedrückt werden.


Der Wegfall einer separaten Graphdatenbank erleichtert die Verwendung von Wissensgraphen. Darüber hinaus vereinfacht die Verwendung von Astra DB oder Apache Cassandra das transaktionale Schreiben sowohl in den Graphen als auch in andere am selben Ort gespeicherte Daten und lässt sich wahrscheinlich besser skalieren. Dieser Mehraufwand würde sich nur lohnen, wenn Sie vorhaben, Graphabfragen mit Gremlin, Cypher oder etwas Ähnlichem zu generieren und auszuführen.


Dies ist jedoch für den Abruf des Unterwissensgraphen schlicht übertrieben und öffnet die Tür für eine Reihe anderer Probleme, wie beispielsweise Abfragen, deren Leistung aus dem Ruder läuft.


Diese Durchquerung ist in Python einfach zu implementieren. Den vollständigen Code zur Implementierung (sowohl synchron als auch asynchron) mit CQL und dem Cassandra-Treiber finden Sie im Repository Der Kern der asynchronen Durchquerung ist unten zur Veranschaulichung dargestellt:


 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


Abschluss

In diesem Artikel wurde gezeigt, wie Sie die Extraktion und Abfrage von Wissensgraphen zum Beantworten von Fragen erstellen und verwenden. Die wichtigste Erkenntnis ist, dass Sie hierfür heute keine Graphdatenbank mit einer Graphabfragesprache wie Gremlin oder Cypher benötigen. Eine großartige Datenbank wie Astra, die viele Abfragen effizient parallel verarbeitet, kann dies bereits bewältigen.


Tatsächlich könnten Sie einfach eine einfache Abfragesequenz schreiben, um den Unterwissensgraphen abzurufen, der zur Beantwortung einer bestimmten Abfrage erforderlich ist. Dadurch bleibt Ihre Architektur einfach (keine zusätzlichen Abhängigkeiten) und Sie können sofort loslegen !


Wir haben dieselben Ideen verwendet, um GraphRAG-Muster für Cassandra und Astra DB zu implementieren. Wir werden sie zu LangChain beitragen und daran arbeiten, in Zukunft weitere Verbesserungen für die Verwendung von Wissensgraphen mit LLMs zu erzielen!


Von Ben Chambers, DataStax