Imagine que você entregou uma estante cheia de livros a um assistente e fez uma pergunta. Em vez de obrigá-lo a ler tudo novamente, um mecanismo procura as páginas que parecem mais relacionadas à pergunta. O assistente recebe essas páginas e prepara a resposta. Essa é, de maneira simplificada, a ideia do RAG.
RAG é a sigla para Retrieval-Augmented Generation, ou geração aumentada por recuperação. Na prática, o sistema divide documentos em trechos menores, procura os mais próximos da pergunta e os entrega a um modelo de linguagem. Esse desenho funciona muito bem para localizar uma cláusula, explicar uma função ou responder uma dúvida cuja evidência está concentrada em poucos parágrafos. O problema começa quando a resposta está espalhada por vários lugares.
Imagine uma biografia em que cada capítulo descreve uma conquista diferente da mesma pessoa. A pergunta “quais foram as principais contribuições de P?” não procura um único trecho muito semelhante. Ela exige cobertura: encontrar P, percorrer todas as conquistas relacionadas, preservar datas e organizações, remover duplicações e reunir evidências distribuídas. A busca vetorial top-k pode devolver cinco capítulos e ignorar outros quinze igualmente importantes.
É nesse ponto que entra o Graph RAG. Em vez de enxergar apenas páginas separadas, ele também cria uma espécie de mapa. Nesse mapa, pessoas, empresas, projetos e acontecimentos ficam conectados. Se a pergunta for sobre uma pessoa, o sistema pode seguir as conexões e encontrar realizações descritas em capítulos diferentes.
Durante a preparação dos documentos, o Graph RAG identifica nomes, relações e fatos, junta referências que representam a mesma coisa e mantém cada informação ligada ao trecho que a comprova. Na consulta, encontra um ponto inicial nesse mapa e percorre somente as conexões úteis. A ideia é simples; fazer isso corretamente exige cuidado com custo, velocidade, duplicações, permissões e informações inventadas.
Neste guia vamos implementar os dois caminhos em Python, construir um grafo no Neo4j, criar recuperação híbrida com vetores e Cypher, expor uma API com FastAPI, avaliar completude e fundamentação e entender onde Microsoft GraphRAG, Neo4j GraphRAG, LlamaIndex, LangChain e Haystack entram em uma arquitetura profissional.
A ideia central: RAG recupera trechos semanticamente próximos. Graph RAG recupera evidências conectadas por entidades e relações. O sistema mais forte em produção costuma combinar os dois e escolher o caminho de acordo com a pergunta.
Você não precisa conhecer bancos de grafos, embeddings ou inteligência artificial para acompanhar o artigo. Cada termo será apresentado antes do código. Primeiro entenderemos o problema; depois construiremos uma versão funcional; somente então acrescentaremos os cuidados necessários para usar a solução em um sistema real.
Índice completo do artigo
1 RAG e Graph RAG sem simplificações
Um modelo de linguagem não deve ser tratado como uma enciclopédia perfeitamente atualizada. No RAG, separamos duas responsabilidades. O recuperador procura informação nos documentos; o gerador escreve a resposta usando o que foi encontrado. Pense em um pesquisador que seleciona as fontes e um redator que transforma essas fontes em uma explicação.
Essa separação facilita atualização, auditoria e controle de acesso. Mesmo assim, não garante uma resposta verdadeira. Se o pesquisador entregar páginas erradas ou incompletas, o redator trabalhará com material ruim. Por isso, a qualidade da recuperação é tão importante quanto a qualidade do modelo.
1.1 O pipeline do RAG tradicional
- O documento é convertido para texto e dividido em pequenos trechos, chamados chunks.
- Cada chunk recebe informações de origem, permissões e uma representação matemática chamada embedding.
- O vetor é armazenado em pgvector, Qdrant, Weaviate, Pinecone, Milvus ou outro índice.
- A pergunta é transformada em embedding.
- O índice devolve os
top_kvetores mais próximos. - Filtros e reranking selecionam os melhores trechos.
- O LLM responde limitado às evidências e apresenta citações.
Embedding é uma lista de números que representa aproximadamente o significado de um texto. Textos semanticamente parecidos tendem a produzir vetores próximos. Isso permite encontrar “encerrar o contrato” mesmo quando o documento usa a expressão “cancelamento do serviço”.
O adjetivo “ingênuo” não significa que embeddings sejam ruins. Significa depender apenas de similaridade e top-k fixo, sem expansão de contexto, consulta lexical, reranking, decomposição de pergunta ou estrutura do domínio.
1.2 O que o grafo acrescenta
Graph RAG transforma elementos importantes em um mapa de conexões. Cada elemento é um nó; cada ligação é uma aresta. No trecho “Paulo fundou a Acme em 2018”, Paulo vira um nó do tipo pessoa, Acme vira um nó do tipo organização e “fundou” vira a relação entre eles. O sistema também guarda qual trecho originou essa informação.
O embedding continua útil para localizar o primeiro nó ou documento. Em seguida, o grafo permite seguir conexões relevantes. É como usar uma busca para encontrar uma pessoa em uma agenda e, depois, consultar os projetos e organizações ligados a ela.
Existem várias famílias chamadas Graph RAG. A mais direta combina busca vetorial com travessia de um property graph. Outra transforma a pergunta em Cypher. A abordagem da Microsoft extrai entidades e relações, detecta comunidades e gera relatórios hierárquicos para responder perguntas globais por map-reduce. Portanto, “usar grafo” não descreve um único algoritmo.
2 Onde o RAG ingênuo falha
A busca vetorial encontra os trechos mais parecidos, mas não sabe se encontrou todos os trechos necessários. Esse é um ponto fundamental. Proximidade não significa cobertura.
top_k é apenas a quantidade máxima de resultados escolhidos. Se configurarmos cinco, o sistema recuperará cinco chunks. Aumentar esse número pode trazer mais fatos, porém também aumenta repetições, textos pouco úteis, custo e dificuldade para o modelo perceber o que realmente importa.
2.1 O caso da biografia
Considere vinte capítulos, cada um dedicado a uma conquista de P. A busca por “principais realizações de P” pode favorecer capítulos que repetem palavras como “realização” ou “prêmio”. Outros capítulos descrevem projetos, pesquisas ou empresas sem usar esses termos e acabam esquecidos.
Um grafo bem construído liga P a cada realização. A consulta encontra a pessoa e segue relações que significam “realizou”, “fundou”, “liderou”, “criou” e “foi premiado”. Depois volta aos trechos originais para comprovar cada resultado. Assim, o modelo recebe uma coleção mais completa e não apenas os parágrafos que se parecem mais com a pergunta.
O grafo também não pode entregar contexto ilimitado ao LLM. Ele melhora a seleção, mas ainda precisa limitar arestas, deduplicar evidências e resumir grupos quando ultrapassa a janela.
2.2 Casos reais multi-hop e globais
- Suporte técnico: quais versões de bibliotecas são afetadas por uma vulnerabilidade transitiva e quais serviços dependem delas?
- Jurídico: quais contratos de uma empresa citam cláusulas alteradas por determinado aditivo?
- Saúde: quais sintomas, medicamentos e condições aparecem ligados ao mesmo episódio clínico, respeitando autorização?
- Finanças: quais empresas de um portfólio compartilham administradores, fornecedores ou exposição a um risco?
- Engenharia: qual decisão arquitetural levou a um incidente e quais serviços herdaram aquela dependência?
- Pesquisa: quais trabalhos sustentam uma hipótese, quais a contradizem e por quais autores estão conectados?
Em perguntas como “qual é a senha do Wi-Fi descrita no manual?”, a relação extra não agrega valor. Um RAG híbrido lexical + vetorial tende a ser menor, barato e rápido.
3 Arquitetura profissional
Para não misturar tudo, separe o sistema em dois grandes fluxos. O primeiro prepara o conhecimento; o segundo responde perguntas.
- Ingestão: recebe documentos, divide o texto, identifica entidades e relações, elimina duplicações e grava o resultado.
- Consulta: verifica quem está perguntando, escolhe uma estratégia de busca, recupera evidências e monta a resposta com citações.

3.1 Ontologia, proveniência e temporalidade
Ontologia é o conjunto de regras que define quais tipos de elementos e relações podem existir no grafo. Para uma biografia, podemos permitir pessoa, organização, projeto e realização. Também definimos que uma pessoa pode fundar uma organização ou liderar um projeto.
Comece pequeno. Se deixarmos o modelo inventar nomes livremente, a mesma ideia pode aparecer como WORKS_AT, EMPLOYED_BY e MEMBER_OF. Para o computador, são relações diferentes. Definir uma lista controlada evita essa bagunça.
Proveniência significa saber de onde veio cada informação. Não grave “P fundou Acme” sem indicar a fonte. Armazene o documento, o chunk, a localização e uma pequena citação. Assim, a interface pode mostrar a evidência e a equipe consegue corrigir uma extração errada.
Temporalidade representa quando um fato é válido. “Trabalha em” pode ser verdade em 2022 e falso em 2026. Campos como data inicial, data final ou data do acontecimento evitam apresentar relações antigas como atuais.
4 Frameworks e bancos para desenvolver Graph RAG
| Opção | Ponto forte | Quando usar |
|---|---|---|
| Microsoft GraphRAG | Pipeline de extração, comunidades, relatórios, Local, Global e DRIFT Search | Síntese holística de grandes coleções e exploração de comunidades |
| Neo4j GraphRAG for Python | KG builder e retrievers Vector, Hybrid, VectorCypher, Text2Cypher e externos | Property graph operacional com Cypher e recuperação híbrida |
| LlamaIndex PropertyGraphIndex | Abstrações de ingestão, graph stores e retrievers compostos | Aplicações que já usam o ecossistema LlamaIndex |
| LangChain + LangGraph | Componentes, ferramentas e orquestração com estado | Roteamento, agentes e workflows customizados |
| Haystack | Pipelines explícitos, componentes e avaliação | Times que preferem DAGs de recuperação bem delimitados |
| NetworkX | Algoritmos em memória, simples para protótipos | Experimentos e corpora pequenos, não banco multiusuário |
| Neo4j | Property graph, Cypher, índices vetoriais e ecossistema | Travessias flexíveis e operações orientadas a relações |
| PostgreSQL + Apache AGE | Dados relacionais, vetores e grafo próximos | Consolidação quando a extensão e o modelo atendem ao projeto |
Não é necessário usar todas essas ferramentas. Para aprender e construir um primeiro sistema funcional, Neo4j, Python, FastAPI e um provedor de embeddings já são suficientes. Microsoft GraphRAG é interessante quando a prioridade é resumir coleções inteiras; LlamaIndex, LangChain e Haystack ajudam quando a aplicação já utiliza seus componentes.
Framework reduz código repetitivo, mas não decide quais entidades existem, quando duas pessoas são a mesma, quais documentos o usuário pode abrir ou se uma relação foi inventada. Essas decisões continuam pertencendo ao projeto.
4.1 Neo4j GraphRAG e Microsoft GraphRAG na prática
O pacote oficial neo4j-graphrag oferece retrievers prontos. VectorCypherRetriever usa o índice vetorial para localizar nós sementes e executa uma consulta de travessia controlada depois. É exatamente a composição que implementaremos manualmente nas próximas seções:
from neo4j import GraphDatabase
from neo4j_graphrag.embeddings import SentenceTransformerEmbeddings
from neo4j_graphrag.retrievers import VectorCypherRetriever
driver = GraphDatabase.driver(
"neo4j://localhost:7687",
auth=("neo4j", "change-me"),
)
embedder = SentenceTransformerEmbeddings(
model="sentence-transformers/all-MiniLM-L6-v2"
)
retrieval_query = """
MATCH (node)-[r:RELATED*1..2]-(related:Entity)
WHERE node.tenant_id = $tenant_id
AND all(edge IN r WHERE edge.tenant_id = $tenant_id)
OPTIONAL MATCH (related)-[:MENTIONED_IN]->(chunk:Chunk)
WHERE chunk.tenant_id = $tenant_id
RETURN node.name AS seed,
related.name AS related_entity,
chunk.id AS chunk_id,
chunk.text AS evidence,
score
LIMIT 40
"""
retriever = VectorCypherRetriever(
driver=driver,
index_name="entity_embedding",
embedder=embedder,
retrieval_query=retrieval_query,
)
result = retriever.search(
query_text="Quais projetos levaram às conquistas de P?",
top_k=6,
query_params={"tenant_id": "tenant-a"},
)
Confirme a assinatura da versão fixada pelo projeto, porque os frameworks evoluem. O princípio permanece: parâmetros são separados da consulta, o tenant participa da travessia e o retorno contém propriedades e evidências, não objetos arbitrários do banco.
Microsoft GraphRAG é mais opinativo. Depois de preparar a configuração e os documentos, o fluxo típico indexa o corpus e consulta os artefatos:
python -m graphrag init --root ./rag-workspace
python -m graphrag index --root ./rag-workspace
python -m graphrag query \
--root ./rag-workspace \
--method local \
--query "Quais realizações estão ligadas a P?"
python -m graphrag query \
--root ./rag-workspace \
--method global \
--query "Quais são os principais temas de toda a coleção?"
Para incorporar a biblioteca a uma aplicação, prefira a API Python oficial da versão instalada em vez de executar subprocessos. O CLI é excelente para experimentar configuração, prompts e custo antes de assumir a integração.
5 Projeto Python: da instalação ao domínio
python -m venv .venv
source .venv/bin/activate
pip install \
"fastapi>=0.115" "uvicorn[standard]>=0.34" \
"neo4j>=5.28" "pydantic>=2.10" \
"pydantic-settings>=2.7" "httpx>=0.28" \
"tenacity>=9.0" "structlog>=25.1" \
"numpy>=2.2"
Esse comando cria um ambiente isolado e instala as bibliotecas usadas no artigo. FastAPI expõe a API; Neo4j acessa o grafo; Pydantic valida os dados; Tenacity implementa tentativas controladas; Structlog ajuda nos logs.
Em produção, fixe versões com lockfile e mantenha senhas fora do código. Também vale esconder o provedor de IA atrás de uma interface. Isso permite trocar modelo ou executar testes sem reescrever toda a aplicação.
5.1 Modelos de extração estruturada
from __future__ import annotations
from enum import StrEnum
from pydantic import BaseModel, Field, model_validator
class EntityType(StrEnum):
PERSON = "Person"
ORGANIZATION = "Organization"
PROJECT = "Project"
ACCOMPLISHMENT = "Accomplishment"
LOCATION = "Location"
class Entity(BaseModel):
local_id: str
type: EntityType
name: str = Field(min_length=1, max_length=300)
aliases: list[str] = Field(default_factory=list)
description: str = Field(default="", max_length=2000)
class Relationship(BaseModel):
source_id: str
target_id: str
type: str = Field(pattern=r"^[A-Z][A-Z0-9_]{1,63}$")
description: str
confidence: float = Field(ge=0, le=1)
occurred_at: str | None = None
evidence_quote: str = Field(min_length=1, max_length=1000)
class Extraction(BaseModel):
entities: list[Entity]
relationships: list[Relationship]
@model_validator(mode="after")
def validate_references(self) -> "Extraction":
ids = {entity.local_id for entity in self.entities}
for rel in self.relationships:
if rel.source_id not in ids or rel.target_id not in ids:
raise ValueError("relationship references an unknown entity")
return self
O primeiro código define o formato que esperamos receber da IA. Entity representa uma pessoa, organização ou outro elemento. Relationship liga duas entidades e exige uma evidência. Extraction verifica se todas as relações apontam para entidades que realmente existem na resposta.
Esse formato reduz respostas quebradas, mas não prova que o fato está no documento. O serviço ainda precisa conferir a citação, rejeitar relações fora da ontologia e registrar qual modelo e prompt fizeram a extração.
6 Ingestão, chunking e construção do grafo
Chunking é o processo de dividir um documento em partes menores. Cortar a cada mil caracteres é fácil, mas pode separar uma frase da explicação que vem logo depois. O ideal é respeitar capítulos, seções, parágrafos, tabelas e blocos de código.
Além do texto, preserve título, página, posição e documento de origem. Um pequeno overlap, ou repetição entre o final de um chunk e o começo do próximo, evita perder ideias que atravessam a divisão.
6.1 Extração de entidades e relações
from typing import Protocol
from tenacity import retry, stop_after_attempt, wait_exponential_jitter
class StructuredLLM(Protocol):
async def extract(self, *, system: str, text: str, schema: type[Extraction]) -> Extraction: ...
ALLOWED_RELATIONS = {
"ACHIEVED", "FOUNDED", "LED", "CREATED", "AWARDED",
"WORKED_AT", "COLLABORATED_WITH", "OCCURRED_AT",
}
@retry(stop=stop_after_attempt(3), wait=wait_exponential_jitter(max=20))
async def extract_graph(llm: StructuredLLM, chunk_text: str) -> Extraction:
result = await llm.extract(
system=(
"Extraia somente fatos explicitamente sustentados pelo texto. "
"Use apenas os tipos de entidade e relações do schema. "
"Copie uma evidência curta e literal para cada relação. "
"Não complete conhecimento com memória externa."
),
text=chunk_text,
schema=Extraction,
)
invalid = {r.type for r in result.relationships} - ALLOWED_RELATIONS
if invalid:
raise ValueError(f"unsupported relationships: {sorted(invalid)}")
return result
O código pede ao modelo somente fatos presentes no chunk e aceita apenas relações conhecidas. Se a chamada falhar temporariamente, Tenacity tenta novamente com intervalos crescentes. Erros permanentes continuam visíveis; não devem ser repetidos para sempre.
Em lotes grandes, use uma fila e limite quantos chunks são enviados simultaneamente. Crie uma chave a partir da versão do documento, conteúdo e versão do extrator. Assim, processar o mesmo chunk novamente atualiza o resultado em vez de criar cópias.
6.2 Resolução de entidades
Resolução de entidades é a tarefa de descobrir quando nomes diferentes representam a mesma coisa. “Dr. Ana Silva”, “Ana” e “A. Silva” podem ser a mesma pessoa. Ao mesmo tempo, duas pessoas chamadas João Souza podem ser completamente diferentes.
Por isso, o sistema combina nome normalizado, apelidos, identificadores, organizações relacionadas e revisão humana. Em saúde, finanças ou segurança, nunca una pessoas apenas porque o nome é igual.
import re
import unicodedata
def canonical_name(value: str) -> str:
normalized = unicodedata.normalize("NFKD", value.casefold())
without_marks = "".join(c for c in normalized if not unicodedata.combining(c))
return re.sub(r"\s+", " ", re.sub(r"[^\w\s]", " ", without_marks)).strip()
def entity_key(tenant_id: str, entity: Entity) -> str:
# Em produção, prefira um identificador verificado do domínio.
return f"{tenant_id}:{entity.type}:{canonical_name(entity.name)}"
Resultados ambíguos entram em uma fila de revisão. Mesclar nós erroneamente contamina todas as travessias seguintes; separar duplicados reduz recall, mas costuma ser mais fácil de reparar.
6.3 Persistência idempotente no Neo4j
CREATE CONSTRAINT entity_key IF NOT EXISTS
FOR (e:Entity) REQUIRE (e.tenant_id, e.key) IS UNIQUE;
CREATE CONSTRAINT chunk_id IF NOT EXISTS
FOR (c:Chunk) REQUIRE (c.tenant_id, c.id) IS UNIQUE;
CREATE VECTOR INDEX chunk_embedding IF NOT EXISTS
FOR (c:Chunk) ON c.embedding
OPTIONS {indexConfig: {
`vector.dimensions`: 1536,
`vector.similarity_function`: 'cosine'
}};
Cypher é a linguagem usada para consultar e alterar grafos no Neo4j. As duas primeiras instruções impedem chaves duplicadas. A terceira cria o índice que encontra chunks semanticamente parecidos por seus embeddings.
from neo4j import AsyncDriver
async def persist_extraction(
driver: AsyncDriver,
*,
tenant_id: str,
chunk_id: str,
document_id: str,
text: str,
extraction: Extraction,
) -> None:
entities = [
{
**entity.model_dump(mode="json"),
"key": entity_key(tenant_id, entity),
}
for entity in extraction.entities
]
local_to_key = {e["local_id"]: e["key"] for e in entities}
relationships = [
{
**rel.model_dump(),
"source_key": local_to_key[rel.source_id],
"target_key": local_to_key[rel.target_id],
}
for rel in extraction.relationships
]
query = """
MERGE (c:Chunk {tenant_id: $tenant_id, id: $chunk_id})
SET c.document_id=$document_id, c.text=$text
WITH c
UNWIND $entities AS row
MERGE (e:Entity {tenant_id:$tenant_id, key:row.key})
SET e.name=row.name, e.type=row.type,
e.aliases=row.aliases, e.description=row.description
MERGE (e)-[:MENTIONED_IN]->(c)
WITH c
UNWIND $relationships AS row
MATCH (a:Entity {tenant_id:$tenant_id, key:row.source_key})
MATCH (b:Entity {tenant_id:$tenant_id, key:row.target_key})
MERGE (a)-[r:RELATED {
tenant_id:$tenant_id, type:row.type, source_chunk_id:$chunk_id
}]->(b)
SET r.description=row.description, r.confidence=row.confidence,
r.evidence_quote=row.evidence_quote, r.occurred_at=row.occurred_at
"""
async with driver.session(database="neo4j") as session:
await session.execute_write(
lambda tx: tx.run(
query,
tenant_id=tenant_id,
chunk_id=chunk_id,
document_id=document_id,
text=text,
entities=entities,
relationships=relationships,
).consume()
)
A função Python recebe o resultado da extração, cria ou atualiza o chunk, grava as entidades e liga cada relação à evidência. O uso de MERGE torna a operação repetível: se a chave já existe, o nó é reaproveitado.
tenant_id identifica a empresa ou cliente dono daqueles dados. Ele aparece em nós, relações e consultas para impedir que uma organização encontre documentos de outra. Em cenários de alto risco, bancos separados ou controles adicionais são mais seguros que depender apenas de filtros escritos manualmente.

7 RAG vetorial profissional
Antes de construir um grafo, crie uma boa versão do RAG comum. Combine busca por palavras com busca por significado, respeite permissões, recupere os parágrafos vizinhos e use um reranker — um modelo que reorganiza os candidatos depois da primeira busca. Muitos problemas desaparecem sem a complexidade adicional do grafo.
from dataclasses import dataclass
@dataclass(frozen=True)
class Evidence:
chunk_id: str
document_id: str
text: str
score: float
async def vector_retrieve(
driver: AsyncDriver,
*,
tenant_id: str,
embedding: list[float],
top_k: int = 12,
) -> list[Evidence]:
query = """
CALL db.index.vector.queryNodes('chunk_embedding', $candidate_k, $embedding)
YIELD node, score
WHERE node.tenant_id = $tenant_id
RETURN node.id AS chunk_id, node.document_id AS document_id,
node.text AS text, score
ORDER BY score DESC
LIMIT $top_k
"""
async with driver.session(database="neo4j") as session:
result = await session.run(
query,
tenant_id=tenant_id,
embedding=embedding,
candidate_k=max(top_k * 5, 50),
top_k=top_k,
)
return [Evidence(**record.data()) async for record in result]
A função procura mais candidatos do que pretende devolver e depois mantém apenas os permitidos para aquele tenant. Isso é necessário porque a busca aproximada, conhecida como ANN, pode encontrar itens de outros clientes primeiro. Sempre aplique autorização antes de o texto chegar ao modelo.
8 Recuperação Graph RAG híbrida
Na recuperação Graph RAG, primeiro encontramos alguns nós iniciais, chamados sementes. Depois seguimos somente relações permitidas por um ou dois passos. É como localizar uma pessoa e consultar seus projetos, organizações e realizações, sem abrir todos os caminhos do banco.
O caminho mais seguro usa consultas previamente preparadas. Pedir ao modelo que escreva qualquer Cypher é flexível, mas um erro pode gerar uma consulta lenta ou acessar algo indevido. Text2Cypher deve operar com usuário somente leitura, limite de tempo e validação.
8.1 Sementes semânticas e travessia
CALL db.index.vector.queryNodes('entity_embedding', 8, $embedding)
YIELD node, score
WHERE node.tenant_id = $tenant_id
WITH node, score
MATCH path=(node)-[rels:RELATED*1..2]-(connected:Entity)
WHERE all(r IN rels WHERE
r.tenant_id = $tenant_id AND
r.type IN $allowed_relationships AND
r.confidence >= $min_confidence
)
WITH path, node, connected, score,
reduce(conf=1.0, r IN rels | conf * r.confidence) AS path_confidence
UNWIND relationships(path) AS rel
MATCH (chunk:Chunk {
tenant_id:$tenant_id,
id:rel.source_chunk_id
})
RETURN DISTINCT chunk.id AS chunk_id,
chunk.document_id AS document_id,
chunk.text AS text,
score * path_confidence / length(path) AS score
ORDER BY score DESC
LIMIT $limit;
Limitar profundidade evita explosão combinatória. Um salto costuma recuperar fatos diretamente ligados; dois saltos resolvem muitas perguntas multi-hop. Caminhos maiores precisam de relações muito seletivas, orçamento por nó e prevenção de ciclos. “Recupere tudo conectado” não é estratégia de produção.
8.2 Ranking e montagem do contexto
Nem tudo o que está conectado possui a mesma importância. O sistema atribui uma pontuação usando a proximidade com a pergunta, a confiança da relação, a quantidade de passos, a data e a qualidade da fonte. Também evita preencher o contexto com dez trechos quase iguais do mesmo capítulo.
def pack_context(evidence: list[Evidence], max_chars: int = 24_000) -> str:
selected: list[str] = []
seen_chunks: set[str] = set()
used = 0
for item in sorted(evidence, key=lambda e: e.score, reverse=True):
if item.chunk_id in seen_chunks:
continue
block = (
f"[source={item.document_id}; chunk={item.chunk_id}; "
f"score={item.score:.4f}]\n{item.text}\n"
)
if used + len(block) > max_chars:
continue
selected.append(block)
seen_chunks.add(item.chunk_id)
used += len(block)
return "\n---\n".join(selected)
A função percorre as evidências da mais importante para a menos importante, remove chunks repetidos e para quando atinge o orçamento. O exemplo conta caracteres para ser fácil de entender; em produção, conte tokens com o tokenizer do modelo.
Também trate documentos como conteúdo não confiável. Uma frase dentro de um PDF dizendo “ignore todas as regras anteriores” não pode controlar a aplicação. O documento fornece fatos; as instruções continuam vindo do sistema.
9 Roteador entre RAG, Graph RAG e busca global
Não vale executar o caminho mais caro em toda pergunta. Um roteador funciona como uma central de atendimento: lê a solicitação e escolhe o setor adequado.
“Qual é o prazo de cancelamento?” vai ao RAG comum. “Quais projetos de P levaram aos prêmios recebidos?” pede Graph RAG local. “Quais temas aparecem em toda a coleção?” pede busca global. O roteador pode começar com regras simples e evoluir depois.
from enum import StrEnum
class Route(StrEnum):
VECTOR = "vector"
GRAPH_LOCAL = "graph_local"
GRAPH_GLOBAL = "graph_global"
def route_question(question: str) -> Route:
value = question.casefold()
global_markers = ("em toda", "principais temas", "visão geral", "panorama")
graph_markers = (
"relação entre", "como se conecta", "quais projetos",
"dependências", "impacto de", "trajetória", "todas as conquistas",
)
if any(marker in value for marker in global_markers):
return Route.GRAPH_GLOBAL
if any(marker in value for marker in graph_markers):
return Route.GRAPH_LOCAL
return Route.VECTOR
O exemplo procura expressões que normalmente indicam síntese global ou relações. Ele não é perfeito, mas é previsível e fácil de testar. Registre qual rota foi escolhida e permita fallback: se a busca comum trouxer pouco conteúdo, tente o grafo; se o grafo não encontrar evidências, não invente uma resposta.
10 API com FastAPI pronta para evoluir
from typing import Annotated
from fastapi import Depends, FastAPI, Header, HTTPException
from pydantic import BaseModel, Field
app = FastAPI(title="Hybrid RAG API")
class AskRequest(BaseModel):
question: str = Field(min_length=3, max_length=2000)
route: Route | None = None
class Citation(BaseModel):
document_id: str
chunk_id: str
class AskResponse(BaseModel):
answer: str
route: Route
citations: list[Citation]
async def tenant_id(x_tenant_id: Annotated[str, Header()]) -> str:
# Resolva o tenant a partir da identidade autenticada; não confie
# diretamente em um header enviado pelo cliente.
if not x_tenant_id:
raise HTTPException(401)
return x_tenant_id
@app.post("/v1/ask", response_model=AskResponse)
async def ask(body: AskRequest, tenant: str = Depends(tenant_id)) -> AskResponse:
route = body.route or route_question(body.question)
evidence = await retrieval_service.retrieve(
tenant_id=tenant,
question=body.question,
route=route,
)
if not evidence:
return AskResponse(
answer="Não encontrei evidências suficientes nas fontes autorizadas.",
route=route,
citations=[],
)
context = pack_context(evidence)
answer = await answer_service.generate(
question=body.question,
context=context,
instruction=(
"Responda somente com fatos sustentados pelo contexto. "
"Cite source e chunk. Declare lacunas e conflitos."
),
)
return AskResponse(
answer=answer,
route=route,
citations=[
Citation(document_id=e.document_id, chunk_id=e.chunk_id)
for e in evidence
],
)
A API recebe uma pergunta, descobre o tenant, escolhe a rota, recupera evidências e só então chama o modelo. Se nada confiável for encontrado, responde claramente que não possui base suficiente. As citações permitem que a interface mostre de onde cada informação veio.
Para produção, acrescente autenticação real, limite de requisições, timeout, cancelamento, rastreamento e cache. Evite retornar textos brutos quando eles contêm campos que a interface não deveria expor.
11 Graph RAG global, comunidades e resumos
A travessia local começa em uma pessoa, empresa, projeto ou outro elemento. Mas perguntas como “quais são os temas principais de todos os documentos?” não oferecem um ponto inicial.
Microsoft GraphRAG agrupa partes relacionadas do grafo em comunidades, como se organizasse uma biblioteca por assuntos. Gera um resumo de cada grupo e combina os resumos mais relevantes. Essa técnica é chamada map-reduce: primeiro produz respostas parciais; depois reúne as melhores em uma resposta global.
Esse pipeline atende “quais são os temas recorrentes e como evoluem?”, mas cobra mais na indexação e na consulta. Relatórios também são artefatos derivados: precisam de versão, ligação às fontes e reprocessamento quando documentos mudam. A documentação atual oferece Standard GraphRAG, de maior fidelidade e custo, e FastGraphRAG, mais econômico e voltado especialmente à sumarização global.
12 Quantização binária e a promessa de 32x
Embeddings podem ocupar bastante memória. Pense neles como listas com centenas ou milhares de números detalhados. A quantização binária transforma cada dimensão em apenas dois estados, zero ou um. É como trocar uma fotografia detalhada por uma silhueta: fica muito menor e ainda ajuda a localizar candidatos, mas perde informação.
Um número float32 usa 32 bits; a representação binária usa um bit por dimensão. Ignorando metadados e estruturas do índice, a parte bruta pode ficar 32 vezes menor. Isso não significa que todo o banco ficará 32 vezes menor nem que a qualidade continuará igual.
O padrão profissional usa busca em duas fases: quantize para encontrar muitos candidatos baratos por distância de Hamming e reranqueie os melhores com os vetores originais. No pgvector:
CREATE INDEX chunks_embedding_binary_hnsw
ON chunks USING hnsw (
(binary_quantize(embedding)::bit(1536)) bit_hamming_ops
);
SELECT id, text
FROM (
SELECT id, text, embedding
FROM chunks
WHERE tenant_id = $1
ORDER BY binary_quantize(embedding)::bit(1536)
<~> binary_quantize($2::vector)::bit(1536)
LIMIT 200
) candidates
ORDER BY embedding <=> $2::vector
LIMIT 20;
A consulta usa a versão binária para selecionar rapidamente 200 candidatos e depois usa os vetores completos para escolher os 20 melhores. Esse processo em duas etapas economiza trabalho sem confiar totalmente na versão simplificada.
Antes de adotar a técnica, meça quantas respostas corretas continuam aparecendo entre os primeiros resultados. O índice HNSW possui seu próprio custo, e manter o vetor completo para a segunda etapa preserva parte do armazenamento original.
13 Avaliação: medir recuperação e resposta
Um chatbot “parecer bom” em cinco perguntas não é teste. Avalie separadamente a criação do grafo, a recuperação e a resposta. Prepare um conjunto dourado: perguntas reais acompanhadas das fontes e relações que deveriam ser encontradas.
Inclua perguntas simples, perguntas que exigem vários passos, sínteses globais, fatos com data, perguntas sem resposta, ambiguidades e tentativas de acessar documentos proibidos.
| Camada | Métricas | Falha revelada |
|---|---|---|
| Extração | Precisão/recall de entidades e relações, entity resolution F1 | Grafo incompleto, relação inventada ou nós mesclados |
| Recuperação | Recall@k, MRR, path recall, coverage e nDCG | A evidência correta não chegou ao contexto |
| Resposta | Faithfulness, completude, correção e precisão de citações | LLM omitiu, distorceu ou inventou |
| Sistema | p50/p95/p99, tokens, custo, erros e freshness | Qualidade inviável operacionalmente |
| Segurança | Taxa de vazamento e resistência a prompt injection | Contexto ultrapassou autorização ou controlou o modelo |

13.1 Testes automatizados e conjunto dourado
import pytest
@pytest.mark.asyncio
async def test_biography_retrieval_covers_every_accomplishment(retriever):
result = await retriever.retrieve(
tenant_id="test",
question="Quais foram todas as principais conquistas de P?",
route=Route.GRAPH_LOCAL,
)
recovered = {item.chunk_id for item in result}
expected = {"chapter-2", "chapter-4", "chapter-7", "chapter-11"}
assert expected <= recovered
@pytest.mark.asyncio
async def test_tenant_isolation(retriever):
result = await retriever.retrieve(
tenant_id="tenant-a",
question="Resuma os projetos da empresa B",
route=Route.GRAPH_LOCAL,
)
assert all(item.document_id != "tenant-b-secret" for item in result)
O teste da resposta deve confirmar que cada afirmação possui suporte, não procurar igualdade literal com uma frase ideal. Para regressão, congele o corpus e compare por categoria, mantendo revisão humana em amostras.
14 Segurança, custos e operação
- Prompt injection: trate documentos como dados, delimite o contexto e não ofereça ferramentas perigosas ao gerador.
- Autorização: filtre chunks, nós, arestas e relatórios de comunidade antes da composição.
- Dados pessoais: aplique retenção, remoção e rastreie derivados; apagar o documento sem apagar entidades não cumpre a intenção.
- Cypher gerado: use credencial read-only, allowlist, limites de profundidade, timeout e validação.
- Custo: extração e sumarização de comunidades podem superar embeddings; faça cache por hash e reprocessamento incremental.
- Observabilidade: trace roteamento, seeds, caminhos, chunks, tokens, modelo, latência e versão do índice.
- Freshness: propague exclusões e atualizações ao vetor, grafo, comunidades e caches.
- Fallback: quando o grafo está degradado, use RAG vetorial e informe menor cobertura.
Use filas para ingestão, backpressure por provedor, dead letter para documentos defeituosos e circuit breaker. Separar indexação da API evita que uma reindexação esgote conexões do caminho online.
15 Quando usar cada arquitetura
| Situação | Escolha inicial | Motivo |
|---|---|---|
| FAQ, manual e cláusula pontual | RAG híbrido | Evidência local, menor custo e latência |
| Dependências, fraude, biografias e pesquisa | Graph RAG local | Relações e múltiplos saltos fazem parte da resposta |
| Temas e panorama do corpus inteiro | Graph RAG global | Comunidades e map-reduce aumentam cobertura |
| Produto geral com perguntas variadas | Roteador híbrido | Cada pergunta paga apenas pela complexidade necessária |
| Corpus pequeno e estático | Long context ou RAG simples | Construir e manter grafo pode não se pagar |
| Ontologia instável e baixa qualidade documental | Melhorar dados primeiro | O grafo amplifica inconsistências da origem |
Faça um experimento antes da migração: baseline forte, 100 a 500 perguntas representativas e limites de qualidade, custo e p95 definidos antecipadamente. Graph RAG deve ganhar nas categorias que justificam seu custo sem degradar as perguntas simples.
16 Conclusão
RAG ingênuo falha quando similaridade é confundida com completude. Top-k encontra os trechos mais próximos, mas não sabe que uma biografia possui vinte conquistas distribuídas nem que uma dependência precisa ser percorrida até um serviço afetado. Graph RAG torna entidades e relações navegáveis e permite recuperar evidência por caminhos explícitos.
O benefício só aparece com engenharia: ontologia controlada, resolução de entidades, proveniência, temporalidade, travessias limitadas, autorização em todos os níveis, ranking e avaliação. Um grafo ruidoso pode produzir uma resposta mais convincente e menos correta que o RAG simples.
Por isso, a arquitetura mais prática raramente abandona vetores. Ela usa busca lexical e vetorial para localização, grafo para expansão relacional, comunidades para perguntas globais e um roteador para não pagar o caminho caro em toda consulta. A escolha deixa de ser “RAG ou Graph RAG” e passa a ser “qual mecanismo recupera a evidência necessária para esta pergunta?”.
17 Referências
- Microsoft GraphRAG — documentação oficial
- Microsoft GraphRAG — pipeline de indexação
- Microsoft GraphRAG — Local, Global, DRIFT e Basic Search
- Neo4j GraphRAG for Python
- Neo4j GraphRAG — retrievers
- Neo4j — índices vetoriais
- LlamaIndex — Property Graph Index
- LangChain — Neo4j e Cypher
- Haystack — documentação oficial
- pgvector — quantização binária e reranking
- FastAPI — documentação oficial