Extrahieren des Textes aus den Originaldokumenten (HTML, PDF, Markdown usw.).
Aufteilen des Textes in bestimmte Größen basierend auf der Dokumentstruktur und Semantik.
Speichern von Chunks in einer Vektordatenbank mit Schlüsselwert durch die Einbettung des Chunks.
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.
Dieser Ansatz bietet gegenüber dem ähnlichkeitsbasierten Ansatz mehrere Vorteile:
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.
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.
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
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.
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.
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
LangChain unterstützt weitere Optionen wie
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.
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.
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.
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', ... } )
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
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
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
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