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.
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.
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 .
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.
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
Você pode inspecionar o gráfico gerado no navegador Neo4j.
Observe que esta imagem representa apenas uma parte do gráfico gerado.
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.
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.
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.
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.
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...
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.
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.
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