paint-brush
Cómo desbloquear la precisión en aplicaciones RAG: aprovechar los gráficos de conocimiento con Neo4j y LangChainpor@neo4j
220 lecturas

Cómo desbloquear la precisión en aplicaciones RAG: aprovechar los gráficos de conocimiento con Neo4j y LangChain

por Neo4j9m2024/10/21
Read on Terminal Reader

Demasiado Largo; Para Leer

Esta publicación del blog muestra cómo crear un gráfico de conocimiento usando LangChain. El código está disponible en GitHub. Debe configurar una instancia de Neo4j. Para esta demostración, usaremos la página de Wikipedia de [Elizabeth I]. Podemos usar [cargadores de LangChain] para obtener y dividir los documentos de Wikipedia.
featured image - Cómo desbloquear la precisión en aplicaciones RAG: aprovechar los gráficos de conocimiento con Neo4j y LangChain
Neo4j HackerNoon profile picture
0-item
1-item


La generación aumentada de recuperación de grafos ( GraphRAG ) está ganando impulso y se está convirtiendo en una poderosa incorporación a los métodos tradicionales de recuperación de búsqueda vectorial. Este enfoque aprovecha la naturaleza estructurada de las bases de datos de grafos, que organizan los datos como nodos y relaciones, para mejorar la profundidad y la contextualidad de la información recuperada.



Ejemplo de un gráfico de conocimiento.

Los gráficos son excelentes para representar y almacenar información heterogénea e interconectada de manera estructurada, capturando sin esfuerzo relaciones y atributos complejos en diversos tipos de datos. Por el contrario, las bases de datos vectoriales suelen tener dificultades con este tipo de información estructurada, ya que su fortaleza radica en el manejo de datos no estructurados a través de vectores de alta dimensión. En su aplicación RAG, puede combinar datos de gráficos estructurados con búsquedas vectoriales a través de texto no estructurado para lograr lo mejor de ambos mundos. Eso es lo que demostraremos en esta publicación del blog.

Los gráficos de conocimiento son geniales, pero ¿cómo crearlos?

La construcción de un gráfico de conocimiento suele ser el paso más complicado. Implica la recopilación y estructuración de los datos, lo que requiere un conocimiento profundo tanto del dominio como del modelado de gráficos.


Para simplificar este proceso, hemos estado experimentando con modelos LLM. Con su profundo conocimiento del lenguaje y el contexto, los modelos LLM pueden automatizar partes significativas del proceso de creación de gráficos de conocimiento. Al analizar datos de texto, estos modelos pueden identificar entidades, comprender sus relaciones y sugerir cómo podrían representarse mejor en una estructura gráfica.


Como resultado de estos experimentos, hemos agregado la primera versión del módulo de construcción de gráficos a LangChain, que demostraremos en esta publicación del blog.


El código está disponible en GitHub .

Configuración del entorno Neo4j

Debe configurar una instancia de Neo4j. Siga los ejemplos de esta publicación del blog. La forma más sencilla es iniciar una instancia gratuita en Neo4j Aura , que ofrece instancias en la nube de la base de datos de Neo4j. Como alternativa, también puede configurar una instancia local de la base de datos de Neo4j descargando la aplicación Neo4j Desktop y creando una instancia de base de datos local.


 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()


Además, debes proporcionar una clave OpenAI , ya que usaremos sus modelos en esta publicación de blog.

Ingestión de datos

Para esta demostración, utilizaremos la página de Wikipedia de Isabel I. Podemos utilizar los cargadores LangChain para obtener y dividir los documentos de Wikipedia sin problemas.


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


Es hora de construir un gráfico basado en los documentos recuperados. Para ello, hemos implementado un módulo LLMGraphTransformer que simplifica significativamente la construcción y el almacenamiento de un gráfico de conocimiento en una base de datos de gráficos.


 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 )


Puede definir qué LLM desea que utilice la cadena de generación de gráficos de conocimiento. Actualmente, solo admitimos modelos de llamada de funciones de OpenAI y Mistral. Sin embargo, planeamos ampliar la selección de LLM en el futuro. En este ejemplo, estamos utilizando el último GPT-4. Tenga en cuenta que la calidad del gráfico generado depende significativamente del modelo que esté utilizando. En teoría, siempre desea utilizar el más capaz. Los transformadores de gráficos LLM devuelven documentos de gráficos, que se pueden importar a Neo4j a través del método add_graph_documents. El parámetro baseEntityLabel asigna un valor adicional Entidad etiqueta a cada nodo, lo que mejora la indexación y el rendimiento de las consultas. El parámetro include_source vincula los nodos a sus documentos de origen, lo que facilita la trazabilidad de los datos y la comprensión del contexto.


Puede inspeccionar el gráfico generado en el navegador Neo4j.


Parte del gráfico generado.


Tenga en cuenta que esta imagen representa solo una parte del gráfico generado.


Recuperación híbrida para RAG

Después de la generación del gráfico, utilizaremos un enfoque de recuperación híbrido que combina índices vectoriales y de palabras clave con la recuperación de gráficos para aplicaciones RAG.


Combinación de métodos de recuperación híbridos (vector + palabra clave) y de grafos. Imagen del autor.


El diagrama ilustra un proceso de recuperación que comienza con un usuario que plantea una pregunta, que luego se dirige a un recuperador RAG. Este recuperador emplea búsquedas de palabras clave y vectores para buscar en datos de texto no estructurados y los combina con la información que recopila del gráfico de conocimiento. Dado que Neo4j cuenta con índices de palabras clave y vectores, puede implementar las tres opciones de recuperación con un único sistema de base de datos. Los datos recopilados de estas fuentes se introducen en un LLM para generar y entregar la respuesta final.

Recuperador de datos no estructurados

Puede utilizar el método Neo4jVector.from_existing_graph para agregar la recuperación de palabras clave y vectores a los documentos. Este método configura los índices de búsqueda de palabras clave y vectores para un enfoque de búsqueda híbrido, que apunta a los nodos etiquetados como Documento. Además, calcula los valores de incrustación de texto si faltan.


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


Luego se puede llamar al índice vectorial con el método similarity_search.

Recuperador de gráficos

Por otro lado, configurar la recuperación de un gráfico es más complejo, pero ofrece más libertad. En este ejemplo, se utilizará un índice de texto completo para identificar los nodos relevantes y devolver su vecindad directa.


Recuperador de gráficos. Imagen del autor.



El recuperador de gráficos comienza identificando entidades relevantes en la entrada. Para simplificar, le indicamos al LLM que identifique personas, organizaciones y ubicaciones. Para lograr esto, utilizaremos LCEL con el método with_structured_output recientemente agregado.


 # 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)


Vamos a probarlo:


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


Genial, ahora que podemos detectar entidades en la pregunta, usemos un índice de texto completo para mapearlas al gráfico de conocimiento. Primero, necesitamos definir un índice de texto completo y una función que genere consultas de texto completo que permitan corregir algunos errores ortográficos, algo que no explicaremos en detalle aquí.


 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()



Vamos a ponerlo todo junto ahora.


 # 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



La función structured_retriever comienza detectando entidades en la pregunta del usuario. A continuación, itera sobre las entidades detectadas y utiliza una plantilla Cypher para recuperar el vecindario de los nodos relevantes. ¡Vamos a probarla!


 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...


Recuperador final

Como se mencionó al principio, combinaremos el recuperador no estructurado y el recuperador gráfico para crear el contexto final que se pasa a un 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


Como estamos tratando con Python, podemos simplemente concatenar las salidas usando la cadena f.

Definición de la cadena RAG

Hemos implementado con éxito el componente de recuperación del RAG. A continuación, introducimos un mensaje que aprovecha el contexto proporcionado por el recuperador híbrido integrado para producir la respuesta, completando así la implementación de la cadena 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() )


Finalmente, podemos seguir adelante y probar nuestra implementación híbrida de 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.'


También he incorporado una función de reescritura de consultas, lo que permite que la cadena RAG se adapte a configuraciones conversacionales que permiten preguntas de seguimiento. Dado que utilizamos métodos de búsqueda de palabras clave y vectores, debemos reescribir las preguntas de seguimiento para optimizar nuestro proceso de búsqueda.


 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.'


Se puede observar que ¿Cuándo nació ella? se reescribió primero como ¿Cuándo nació Isabel I?. La consulta reescrita se utilizó luego para recuperar el contexto relevante y responder la pregunta.

Gráficos de conocimiento simplificados

Con la introducción de LLMGraphTransformer, el proceso de generación de gráficos de conocimiento ahora debería ser más sencillo y accesible, lo que facilitará la tarea a quienes deseen mejorar sus aplicaciones RAG con la profundidad y el contexto que brindan los gráficos de conocimiento. Esto es solo el comienzo, ya que tenemos muchas mejoras planeadas.


Si tiene ideas, sugerencias o preguntas sobre nuestra generación de gráficos con LLM, no dude en contactarnos.


El código está disponible en GitHub .