paint-brush
Desbloqueando a precisão em aplicações RAG: aproveitando gráficos de conhecimento com Neo4j e LangChainpor@neo4j
220 leituras

Desbloqueando a precisão em aplicações RAG: aproveitando gráficos de conhecimento com Neo4j e LangChain

por Neo4j9m2024/10/21
Read on Terminal Reader

Muito longo; Para ler

Esta postagem do blog mostra como criar um gráfico de conhecimento usando LangChain. O código está disponível no GitHub. Você precisa configurar uma instância do Neo4j. Para esta demonstração, usaremos a página da Wikipedia [Elizabeth I]. Podemos usar [carregadores LangChain] para buscar e dividir os documentos da Wikipedia.
featured image - Desbloqueando a precisão em aplicações RAG: aproveitando gráficos de conhecimento com Neo4j e LangChain
Neo4j HackerNoon profile picture
0-item
1-item


A geração aumentada de recuperação de grafos ( GraphRAG ) está ganhando força e se tornando uma poderosa adição aos métodos tradicionais de recuperação de pesquisa vetorial. Essa abordagem aproveita a natureza estruturada dos bancos de dados de grafos, que organizam dados como nós e relacionamentos, para aumentar a profundidade e a contextualidade das informações recuperadas.



Exemplo de um gráfico de conhecimento.

Os gráficos são ótimos para representar e armazenar informações heterogêneas e interconectadas de forma estruturada, capturando sem esforço relacionamentos e atributos complexos em diversos tipos de dados. Em contraste, bancos de dados vetoriais geralmente têm dificuldades com essas informações estruturadas, pois sua força está em lidar com dados não estruturados por meio de vetores de alta dimensão. Em seu aplicativo RAG, você pode combinar dados de gráfico estruturados com pesquisa vetorial por meio de texto não estruturado para obter o melhor dos dois mundos. É isso que demonstraremos nesta postagem do blog.

Gráficos de conhecimento são ótimos, mas como criar um?

Construir um gráfico de conhecimento é tipicamente o passo mais desafiador. Ele envolve reunir e estruturar os dados, o que requer um profundo entendimento tanto do domínio quanto da modelagem do gráfico.


Para simplificar esse processo, temos experimentado LLMs. Com sua profunda compreensão da linguagem e do contexto, os LLMs podem automatizar partes significativas do processo de criação de gráficos de conhecimento. Ao analisar dados de texto, esses modelos podem identificar entidades, entender seus relacionamentos e sugerir como eles podem ser melhor representados em uma estrutura de gráfico.


Como resultado desses experimentos, adicionamos a primeira versão do módulo de construção de gráficos ao LangChain, que demonstraremos nesta postagem do blog.


O código está disponível no GitHub .

Configuração do ambiente Neo4j

Você precisa configurar uma instância do Neo4j. Siga os exemplos neste post do blog. A maneira mais fácil é iniciar uma instância gratuita no Neo4j Aura , que oferece instâncias em nuvem do banco de dados Neo4j. Como alternativa, você também pode configurar uma instância local do banco de dados Neo4j baixando o aplicativo Neo4j Desktop e criando uma instância de banco de dados 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()


Além disso, você deve fornecer uma chave OpenAI , pois usaremos os modelos deles nesta postagem do blog.

Ingestão de dados

Para esta demonstração, usaremos a página da Wikipédia de Elizabeth I. Podemos usar carregadores LangChain para buscar e dividir os documentos da Wikipédia perfeitamente.


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


É hora de construir um gráfico com base nos documentos recuperados. Para esse propósito, implementamos um módulo LLMGraphTransformer que simplifica significativamente a construção e o armazenamento de um gráfico de conhecimento em um banco de dados 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 )


Você pode definir qual LLM deseja que a cadeia de geração de gráficos de conhecimento use. Atualmente, oferecemos suporte apenas a modelos de chamada de função do OpenAI e Mistral. No entanto, planejamos expandir a seleção de LLM no futuro. Neste exemplo, estamos usando o GPT-4 mais recente. Observe que a qualidade do gráfico gerado depende significativamente do modelo que você está usando. Em teoria, você sempre deseja usar o mais capaz. Os transformadores de gráfico LLM retornam documentos de gráfico, que podem ser importados para o Neo4j por meio do método add_graph_documents. O parâmetro baseEntityLabel atribui um adicional Entidade rótulo para cada nó, melhorando a indexação e o desempenho da consulta. O parâmetro include_source vincula nós aos seus documentos de origem, facilitando a rastreabilidade de dados e a compreensão do contexto.


Você pode inspecionar o gráfico gerado no navegador Neo4j.


Parte do gráfico gerado.


Observe que esta imagem representa apenas uma parte do gráfico gerado.


Recuperação Híbrida para RAG

Após a geração do gráfico, usaremos uma abordagem de recuperação híbrida que combina índices vetoriais e de palavras-chave com recuperação de gráficos para aplicações RAG.


Combinando métodos de recuperação híbridos (vetor + palavra-chave) e de grafos. Imagem do autor.


O diagrama ilustra um processo de recuperação começando com um usuário fazendo uma pergunta, que é então direcionada para um recuperador RAG. Este recuperador emprega pesquisas de palavras-chave e vetores para pesquisar dados de texto não estruturados e os combina com as informações que coleta do gráfico de conhecimento. Como o Neo4j apresenta índices de palavras-chave e vetores, você pode implementar todas as três opções de recuperação com um único sistema de banco de dados. Os dados coletados dessas fontes são alimentados em um LLM para gerar e entregar a resposta final.

Recuperador de Dados Não Estruturados

Você pode usar o método Neo4jVector.from_existing_graph para adicionar recuperação de palavra-chave e vetor a documentos. Este método configura índices de pesquisa de palavra-chave e vetor para uma abordagem de pesquisa híbrida, direcionando nós rotulados como Documento. Além disso, ele calcula valores de incorporação de texto se estiverem faltando.


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


O índice vetorial pode então ser chamado com o método similarity_search.

Recuperador de gráfico

Por outro lado, configurar uma recuperação de gráfico é mais envolvente, mas oferece mais liberdade. Este exemplo usará um índice de texto completo para identificar nós relevantes e retornar sua vizinhança direta.


Recuperador de gráficos. Imagem do autor.



O recuperador de gráfico começa identificando entidades relevantes na entrada. Para simplificar, instruímos o LLM a identificar pessoas, organizações e locais. Para conseguir isso, usaremos LCEL com o método with_structured_output recém-adicionado para conseguir isso.


 # 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 testar:


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


Ótimo, agora que podemos detectar entidades na pergunta, vamos usar um índice de texto completo para mapeá-las para o gráfico de conhecimento. Primeiro, precisamos definir um índice de texto completo e uma função que gerará consultas de texto completo que permitam um pouco de erro de ortografia, o que não entraremos em muitos detalhes aqui.


 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 juntar tudo agora.


 # 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



A função structural_retriever começa detectando entidades na pergunta do usuário. Em seguida, ela itera sobre as entidades detectadas e usa um modelo Cypher para recuperar a vizinhança de nós relevantes. Vamos testar!


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


Último Retriever

Conforme mencionado no início, combinaremos o recuperador de gráfico e o não estruturado para criar o contexto final passado para um 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 lidando com Python, podemos simplesmente concatenar as saídas usando a f-string.

Definindo a Cadeia RAG

Implementamos com sucesso o componente de recuperação do RAG. Em seguida, introduzimos um prompt que aproveita o contexto fornecido pelo recuperador híbrido integrado para produzir a resposta, completando a implementação da cadeia 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 prosseguir e testar nossa implementação RAG híbrida.


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


Também incorporei um recurso de reescrita de consulta, permitindo que a cadeia RAG se adapte a configurações de conversação que permitem perguntas de acompanhamento. Dado que usamos métodos de pesquisa de vetor e palavra-chave, precisamos reescrever perguntas de acompanhamento para otimizar nosso processo de pesquisa.


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


Você pode observar que When was she born? foi reescrito pela primeira vez para When was Elizabeth I born? . A consulta reescrita foi então usada para recuperar o contexto relevante e responder à pergunta.

Gráficos de conhecimento simplificados

Com a introdução do LLMGraphTransformer, o processo de geração de gráficos de conhecimento deve ser agora mais suave e acessível, tornando mais fácil para qualquer um que queira aprimorar seus aplicativos RAG com a profundidade e o contexto que os gráficos de conhecimento fornecem. Isso é apenas o começo, pois temos muitas melhorias planejadas.


Se você tiver ideias, sugestões ou dúvidas sobre como gerar gráficos com LLMs, não hesite em entrar em contato.


O código está disponível em GitHub .