Extraire le texte des documents originaux (HTML, PDF, Markdown, etc.).
Regrouper le texte à des tailles spécifiques en fonction de la structure et de la sémantique du document.
Stockage de morceaux dans une base de données vectorielle saisie par une intégration du morceau.
Récupérer les morceaux pertinents pour une question à utiliser comme contexte lors de la génération de la réponse.
Cependant, le RAG basé sur la similarité des vecteurs présente quelques faiblesses. Puisqu'il se concentre sur des informations similaires à la question, il est plus difficile de répondre à des questions impliquant plusieurs sujets et/ou nécessitant plusieurs sauts, par exemple. De plus, cela limite le nombre de morceaux récupérés.
Chaque morceau provient d'une source distincte, donc dans les cas où des informations largement similaires existent à plusieurs endroits, il doit choisir entre récupérer plusieurs copies de l'information (et éventuellement manquer d'autres informations) ou en choisir une seule copie afin d'en obtenir plus. différents morceaux, qui passent alors à côté des nuances des autres sources.
Cette approche présente plusieurs avantages par rapport à l'approche basée sur la similarité :
De nombreux faits peuvent être extraits d’une source unique et associés à diverses entités au sein du graphe de connaissances. Cela permet de récupérer uniquement les faits pertinents à partir d’une source donnée plutôt que l’intégralité, y compris les informations non pertinentes.
Si plusieurs sources disent la même chose, elles produisent le même nœud ou bord. Au lieu de les traiter comme des faits distincts (et d’en récupérer plusieurs copies), ils peuvent être traités comme le même nœud ou bord et récupérés une seule fois. Cela permet de récupérer une plus grande variété de faits et/ou de se concentrer uniquement sur les faits qui apparaissent dans plusieurs sources.
Le graphique peut être parcouru en plusieurs étapes – non seulement pour récupérer des informations directement liées aux entités de la question, mais également pour extraire des éléments qui se trouvent à 2 ou 3 étapes. Dans une approche RAG conventionnelle, cela nécessiterait plusieurs cycles d’interrogation.
En plus des avantages de l'utilisation d'un graphique de connaissances pour RAG, les LLM ont également facilité la création de graphiques de connaissances. Plutôt que d'exiger que des experts en la matière élaborent soigneusement le graphique de connaissances, un LLM et une invite peuvent être utilisés pour extraire des informations à partir de documents.
Cet article explore l'utilisation des graphes de connaissances pour RAG, en utilisant
Nous créerons ensuite des exécutables LangChain pour extraire les entités de la question et récupérer les sous-graphiques pertinents. Nous verrons que les opérations nécessaires pour implémenter RAG à l'aide de graphes de connaissances ne nécessitent pas de bases de données de graphes ni de langages de requête de graphes, ce qui permet d'appliquer l'approche à l'aide d'un magasin de données typique que vous utilisez peut-être déjà.
Comme mentionné précédemment, un graphe de connaissances représente des entités distinctes sous forme de nœuds. Par exemple, un nœud peut représenter « Marie Curie » la personne, ou « Français » la langue. Dans LangChain, chaque nœud a un nom et un type. Nous considérerons les deux lors de l'identification unique d'un nœud, pour distinguer « Français » la langue de « Français » la nationalité.
Les relations entre entités correspondent aux arêtes du graphique. Chaque bord comprend la source (par exemple, Marie Curie la personne), la cible (le prix Nobel, le prix) et un type, indiquant le lien entre la source et la cible (par exemple, « gagné »).
Un exemple de graphique de connaissances extrait d'un paragraphe sur Marie Curie utilisant LangChain est présenté ci-dessous :
En fonction de vos objectifs, vous pouvez choisir d'ajouter des propriétés aux nœuds et aux arêtes. Par exemple, vous pouvez utiliser une propriété pour identifier la date d'obtention du prix Nobel et la catégorie. Ceux-ci peuvent être utiles pour filtrer les arêtes et les nœuds lors du parcours du graphique lors de la récupération.
Les entités et les relations composant le graphe de connaissances peuvent être créées directement ou importées à partir de sources de données existantes connues. Ceci est utile lorsque vous souhaitez organiser soigneusement les connaissances, mais cela rend difficile l'incorporation rapide de nouvelles informations ou la gestion de grandes quantités d'informations.
Heureusement, les LLM facilitent l'extraction d'informations du contenu, nous pouvons donc les utiliser pour extraire le graphe de connaissances.
Ci-dessous, j'utilise le
LangChain prend en charge d'autres options telles que
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}")
Cela montre comment extraire un graphe de connaissances à l'aide du LLMGraphTransformer
de LangChain. Vous pouvez utiliser le render_graph_document
trouvé dans le référentiel pour restituer un LangChain GraphDocument
pour une inspection visuelle.
Dans un prochain article, nous expliquerons comment vous pouvez examiner le graphique de connaissances dans son intégralité ainsi que le sous-graphique extrait de chaque document et comment vous pouvez appliquer l'ingénierie rapide et l'ingénierie des connaissances pour améliorer l'extraction automatisée.
Répondre aux questions à l’aide du graphe de connaissances nécessite plusieurs étapes. Nous identifions d’abord par où commencer notre parcours du graphe de connaissances. Pour cet exemple, je demanderai à un LLM d'extraire les entités de la question. Ensuite, le graphe de connaissances est parcouru pour récupérer toutes les relations situées à une distance donnée de ces points de départ. La profondeur de parcours par défaut est de 3. Les relations récupérées et la question d'origine sont utilisées pour créer une invite et un contexte permettant au LLM de répondre à la question.
Comme pour l'extraction du graphe de connaissances, l'extraction des entités d'une question peut être effectuée à l'aide d'un modèle spécial ou d'un LLM avec une invite spécifique. Pour plus de simplicité, nous utiliserons un LLM avec l'invite suivante qui inclut à la fois la question et les informations sur le format à extraire. Nous utilisons un modèle Pydantic avec le nom et le type pour obtenir la structure appropriée.
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"]]) )
En exécutant l'exemple ci-dessus, nous pouvons voir les entités extraites :
# Example showing extracted entities (nodes) extract_entities(llm).invoke({ "question": "Who is Marie Curie?"}) # Output: [Marie Curie(Person)]
Bien entendu, un LangChain Runnable peut être utilisé dans une chaîne pour extraire les entités d'une question.
À l'avenir, nous discuterons des moyens d'améliorer l'extraction d'entités, par exemple en prenant en compte les propriétés des nœuds ou en utilisant des représentations vectorielles vectorielles et une recherche de similarité pour identifier les points de départ pertinents. Pour que ce premier article reste simple, nous nous en tiendrons à l'invite ci-dessus et passerons à la traversée du graphe de connaissances pour récupérer le knowledge-subgraph
et l'inclure comme contexte dans l'invite.
La chaîne précédente nous donne les nœuds en question. Nous pouvons utiliser ces entités et le magasin de graphiques pour récupérer les triples de connaissances pertinents. Comme avec RAG, nous les déposons dans l'invite dans le cadre du contexte et générons des réponses.
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 )
La chaîne ci-dessus peut être exécutée pour répondre à une question. Par exemple:
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', ... } )
Bien qu'il puisse sembler intuitif d'utiliser une base de données graphique pour stocker le graphe de connaissances, ce n'est pas réellement nécessaire. Récupérer le graphe de sous-connaissances autour de quelques nœuds est un simple parcours de graphe, tandis que les bases de données de graphe sont conçues pour des requêtes beaucoup plus complexes recherchant des chemins avec des séquences spécifiques de propriétés. De plus, la traversée ne se fait souvent que jusqu'à une profondeur de 2 ou 3, car les nœuds les plus éloignés deviennent assez rapidement hors de propos pour la question. Cela peut être exprimé sous la forme de quelques séries de requêtes simples (une pour chaque étape) ou d'une jointure SQL.
L'élimination du besoin d'une base de données de graphiques distincte facilite l'utilisation des graphiques de connaissances. De plus, l'utilisation d'Astra DB ou d'Apache Cassandra simplifie les écritures transactionnelles sur le graphique et sur d'autres données stockées au même endroit, et évolue probablement mieux. Cette surcharge ne serait utile que si vous envisagez de générer et d'exécuter des requêtes graphiques, en utilisant Gremlin ou Cypher ou quelque chose de similaire.
Mais c’est tout simplement exagéré pour récupérer le graphe de sous-connaissances, et cela ouvre la porte à une foule d’autres problèmes, tels que des requêtes qui déraillent en termes de performances.
Cette traversée est facile à implémenter en Python. Le code complet pour implémenter cela (à la fois de manière synchrone et asynchrone) à l'aide de CQL et du pilote Cassandra peut être trouvé dans le
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
Cet article a montré comment créer et utiliser l'extraction et la récupération de graphiques de connaissances pour répondre aux questions. L'essentiel à retenir est que vous n'avez pas besoin d'une base de données graphique avec un langage de requête graphique comme Gremlin ou Cypher pour ce faire aujourd'hui. Une excellente base de données comme Astra, qui gère efficacement de nombreuses requêtes en parallèle, peut déjà gérer cela.
En fait, vous pouvez simplement écrire une simple séquence de requêtes pour récupérer le graphe de sous-connaissances nécessaire pour répondre à une requête spécifique. Cela maintient votre architecture simple (pas de dépendances ajoutées) et vous permet
Nous avons utilisé ces mêmes idées pour implémenter des modèles GraphRAG pour Cassandra et Astra DB. Nous allons les apporter à LangChain et travailler à apporter d'autres améliorations à l'utilisation des graphes de connaissances avec les LLM à l'avenir !
Par Ben Chambers, DataStax