RAG vs. Graph RAG: guia completo com Python, frameworks, arquitetura e casos reais

Published on: 2026-07-26
Post image
pt rag graph-rag python neo4j llm inteligencia-artificial embeddings banco-vetorial knowledge-graph fastapi langchain llamaindex microsoft-graphrag pgvector

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

  1. O documento é convertido para texto e dividido em pequenos trechos, chamados chunks.
  2. Cada chunk recebe informações de origem, permissões e uma representação matemática chamada embedding.
  3. O vetor é armazenado em pgvector, Qdrant, Weaviate, Pinecone, Milvus ou outro índice.
  4. A pergunta é transformada em embedding.
  5. O índice devolve os top_k vetores mais próximos.
  6. Filtros e reranking selecionam os melhores trechos.
  7. 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 ; 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.
Pipeline visual de documentos, chunks, extração, resolução de entidades, grafo e evidências
O grafo útil não é apenas uma coleção de entidades: cada fato precisa voltar à fonte, e cada etapa precisa ser reprocessável.

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.

Diagrama da ontologia de uma biografia mostrando uma pessoa ligada a realizações, organizações e projetos, com afirmações e entidades conectadas aos chunks de evidência
A ontologia separa relações do domínio, como ACHIEVED e FOUNDED, das ligações de proveniência que levam cada afirmação de volta ao chunk original.

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çãoPonto forteQuando usar
Microsoft GraphRAGPipeline de extração, comunidades, relatórios, Local, Global e DRIFT SearchSíntese holística de grandes coleções e exploração de comunidades
Neo4j GraphRAG for PythonKG builder e retrievers Vector, Hybrid, VectorCypher, Text2Cypher e externosProperty graph operacional com Cypher e recuperação híbrida
LlamaIndex PropertyGraphIndexAbstrações de ingestão, graph stores e retrievers compostosAplicações que já usam o ecossistema LlamaIndex
LangChain + LangGraphComponentes, ferramentas e orquestração com estadoRoteamento, agentes e workflows customizados
HaystackPipelines explícitos, componentes e avaliaçãoTimes que preferem DAGs de recuperação bem delimitados
NetworkXAlgoritmos em memória, simples para protótiposExperimentos e corpora pequenos, não banco multiusuário
Neo4jProperty graph, Cypher, índices vetoriais e ecossistemaTravessias flexíveis e operações orientadas a relações
PostgreSQL + Apache AGEDados relacionais, vetores e grafo próximosConsolidaçã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.

Pergunta encontrando nós sementes, atravessando caminhos, ranqueando evidências e montando contexto
A busca vetorial encontra por significado; a travessia recupera relações; o ranking limita o que realmente entra na janela do modelo.

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.

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.

CamadaMétricasFalha revelada
ExtraçãoPrecisão/recall de entidades e relações, entity resolution F1Grafo incompleto, relação inventada ou nós mesclados
RecuperaçãoRecall@k, MRR, path recall, coverage e nDCGA evidência correta não chegou ao contexto
RespostaFaithfulness, completude, correção e precisão de citaçõesLLM omitiu, distorceu ou inventou
Sistemap50/p95/p99, tokens, custo, erros e freshnessQualidade inviável operacionalmente
SegurançaTaxa de vazamento e resistência a prompt injectionContexto ultrapassou autorização ou controlou o modelo
RAG e Graph RAG sendo comparados por completude, fundamentação, latência, custo e segurança
Compare arquiteturas sobre o mesmo corpus, perguntas e permissões; uma média única esconde regressões nas caudas e nas perguntas difíceis.

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çãoEscolha inicialMotivo
FAQ, manual e cláusula pontualRAG híbridoEvidência local, menor custo e latência
Dependências, fraude, biografias e pesquisaGraph RAG localRelações e múltiplos saltos fazem parte da resposta
Temas e panorama do corpus inteiroGraph RAG globalComunidades e map-reduce aumentam cobertura
Produto geral com perguntas variadasRoteador híbridoCada pergunta paga apenas pela complexidade necessária
Corpus pequeno e estáticoLong context ou RAG simplesConstruir e manter grafo pode não se pagar
Ontologia instável e baixa qualidade documentalMelhorar dados primeiroO 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