Extrair o texto dos documentos originais (HTML, PDF, Markdown, etc.).
Segmentar o texto em tamanhos específicos com base na estrutura e na semântica do documento.
Armazenar pedaços em um banco de dados vetorial codificado por uma incorporação do pedaço.
Recuperar os pedaços relevantes para uma pergunta para usar como contexto ao gerar a resposta.
No entanto, o RAG baseado na similaridade vetorial apresenta alguns pontos fracos. Como se concentra em informações semelhantes à pergunta, é mais difícil responder a perguntas que envolvem vários tópicos e/ou que exigem vários saltos – por exemplo. Além disso, limita o número de pedaços recuperados.
Cada pedaço vem de uma fonte distinta, portanto, nos casos em que existem informações muito semelhantes em vários lugares, é necessário escolher entre recuperar múltiplas cópias da informação (e possivelmente perder outras informações) ou escolher apenas uma cópia para obter mais. pedaços diferentes, que então perdem as nuances das outras fontes.
Esta abordagem tem vários benefícios em relação à abordagem baseada em similaridade:
Muitos fatos podem ser extraídos de uma única fonte e associados a uma variedade de entidades no gráfico de conhecimento. Isto permite a recuperação apenas dos factos relevantes de uma determinada fonte, em vez de todo o bloco, incluindo informações irrelevantes.
Se múltiplas fontes disserem a mesma coisa, elas produzirão o mesmo nó ou aresta. Em vez de tratá-los como fatos distintos (e recuperar múltiplas cópias), eles podem ser tratados como o mesmo nó ou aresta e recuperados apenas uma vez. Isso permite recuperar uma variedade maior de fatos e/ou focar apenas em fatos que aparecem em múltiplas fontes.
O gráfico pode ser percorrido através de vários passos – não apenas recuperando informações diretamente relacionadas às entidades em questão, mas também recuperando coisas que estão a 2 ou 3 passos de distância. Numa abordagem RAG convencional, isto exigiria múltiplas rondas de consulta.
Além dos benefícios de usar um gráfico de conhecimento para RAG, os LLMs também facilitaram a criação de gráficos de conhecimento. Em vez de exigir que especialistas no assunto elaborem cuidadosamente o gráfico de conhecimento, um LLM e um prompt podem ser usados para extrair informações de documentos.
Esta postagem explora o uso de gráficos de conhecimento para RAG, usando
Em seguida, criaremos executáveis LangChain para extrair entidades da questão e recuperar os subgráficos relevantes. Veremos que as operações necessárias para implementar RAG usando gráficos de conhecimento não requerem bancos de dados de gráficos ou linguagens de consulta de gráficos, permitindo que a abordagem seja aplicada usando um armazenamento de dados típico que você já esteja usando.
Conforme mencionado anteriormente, um gráfico de conhecimento representa entidades distintas como nós. Por exemplo, um nó pode representar “Marie Curie” a pessoa ou “Francês” o idioma. No LangChain, cada nó possui um nome e um tipo. Consideraremos ambos ao identificar exclusivamente um nó, para distinguir “francês” a língua de “francês” a nacionalidade.
Os relacionamentos entre entidades correspondem às arestas do gráfico. Cada aresta inclui a fonte (por exemplo, Marie Curie, a pessoa), o alvo (Prêmio Nobel, o prêmio) e um tipo, indicando como a fonte se relaciona com o alvo (por exemplo, “ganhou”).
Um exemplo de gráfico de conhecimento extraído de um parágrafo sobre Marie Curie usando LangChain é mostrado abaixo:
Dependendo de seus objetivos, você pode optar por adicionar propriedades a nós e arestas. Por exemplo, você poderia usar uma propriedade para identificar quando o Prêmio Nobel foi ganho e a categoria. Eles podem ser úteis para filtrar arestas e nós ao percorrer o gráfico durante a recuperação.
As entidades e relacionamentos que compõem o gráfico de conhecimento podem ser criados diretamente ou importados de fontes de dados existentes e conhecidas. Isto é útil quando você deseja selecionar o conhecimento cuidadosamente, mas torna difícil incorporar novas informações rapidamente ou lidar com grandes quantidades de informações.
Felizmente, os LLMs facilitam a extração de informações do conteúdo, para que possamos usá-los para extrair o gráfico de conhecimento.
Abaixo, eu uso o
LangChain oferece suporte a outras opções, como
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}")
Isso mostra como extrair um gráfico de conhecimento usando LLMGraphTransformer
da LangChain. Você pode usar o render_graph_document
encontrado no repositório para renderizar um LangChain GraphDocument
para inspeção visual.
Em uma postagem futura, discutiremos como você pode examinar o gráfico de conhecimento em sua totalidade, bem como o subgráfico extraído de cada documento e como você pode aplicar engenharia imediata e engenharia de conhecimento para melhorar a extração automatizada.
Responder a perguntas usando o gráfico de conhecimento requer várias etapas. Primeiro identificamos onde começar nossa travessia do gráfico de conhecimento. Para este exemplo, solicitarei que um LLM extraia entidades da pergunta. Em seguida, o gráfico de conhecimento é percorrido para recuperar todos os relacionamentos dentro de uma determinada distância desses pontos iniciais. A profundidade de passagem padrão é 3. Os relacionamentos recuperados e a pergunta original são usados para criar um prompt e um contexto para o LLM responder à pergunta.
Assim como na extração do gráfico de conhecimento, a extração das entidades de uma questão pode ser feita por meio de um modelo especial ou de um LLM com prompt específico. Para simplificar, usaremos um LLM com o seguinte prompt que inclui a pergunta e as informações sobre o formato a ser extraído. Usamos um modelo Pydantic com o nome e tipo para obter a estrutura adequada.
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"]]) )
Executando o exemplo acima podemos ver as entidades extraídas:
# Example showing extracted entities (nodes) extract_entities(llm).invoke({ "question": "Who is Marie Curie?"}) # Output: [Marie Curie(Person)]
Claro, um LangChain Runnable pode ser usado em uma cadeia para extrair as entidades de uma pergunta.
No futuro, discutiremos maneiras de melhorar a extração de entidades, como considerar propriedades de nós ou usar incorporações de vetores e pesquisa por similaridade para identificar pontos de partida relevantes. Para manter esta primeira postagem simples, seguiremos o prompt acima e passaremos a percorrer o gráfico de conhecimento para recuperar o knowledge-subgraph
e incluí-lo como o contexto no prompt.
A cadeia anterior nos dá os nós em questão. Podemos usar essas entidades e o armazenamento gráfico para recuperar os triplos de conhecimento relevantes. Assim como no RAG, nós os colocamos no prompt como parte do contexto e geramos respostas.
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 )
A cadeia acima pode ser executada para responder a uma pergunta. Por exemplo:
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', ... } )
Embora possa parecer intuitivo usar um banco de dados gráfico para armazenar o gráfico de conhecimento, na verdade não é necessário. Recuperar o gráfico de subconhecimento em torno de alguns nós é uma simples travessia do gráfico, enquanto os bancos de dados gráficos são projetados para consultas muito mais complexas, procurando caminhos com sequências específicas de propriedades. Além disso, a travessia geralmente ocorre apenas até uma profundidade de 2 ou 3, uma vez que os nós que estão mais distantes tornam-se irrelevantes para a questão muito rapidamente. Isso pode ser expresso como algumas rodadas de consultas simples (uma para cada etapa) ou uma junção SQL.
Eliminar a necessidade de um banco de dados gráfico separado facilita o uso de gráficos de conhecimento. Além disso, o uso do Astra DB ou do Apache Cassandra simplifica as gravações transacionais no gráfico e em outros dados armazenados no mesmo local e provavelmente tem melhor escalabilidade. Essa sobrecarga só valeria a pena se você estivesse planejando gerar e executar consultas gráficas, usando Gremlin ou Cypher ou algo semelhante.
Mas isso é simplesmente um exagero para recuperar o gráfico de subconhecimento e abre a porta para uma série de outros problemas, como consultas que saem dos trilhos em termos de desempenho.
Essa travessia é fácil de implementar em Python. O código completo para implementar isso (tanto de forma síncrona quanto assíncrona) usando CQL e o driver Cassandra pode ser encontrado no
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
Este artigo mostrou como construir e usar a extração e recuperação de gráficos de conhecimento para responder perguntas. A principal conclusão é que você não precisa de um banco de dados gráfico com uma linguagem de consulta gráfica como Gremlin ou Cypher para fazer isso hoje. Um ótimo banco de dados como o Astra, que lida com eficiência com muitas consultas em paralelo, já pode lidar com isso.
Na verdade, você poderia simplesmente escrever uma sequência simples de consultas para recuperar o gráfico de subconhecimento necessário para responder a uma consulta específica. Isso mantém sua arquitetura simples (sem dependências adicionais) e permite que você
Usamos essas mesmas ideias para implementar padrões GraphRAG para Cassandra e Astra DB. Vamos contribuir com eles para o LangChain e trabalhar para trazer outras melhorias no uso de gráficos de conhecimento com LLMs no futuro!
Por Ben Chambers, DataStax