O que é RAG com Grafo?
RAG com Grafo (GraphRAG) é um padrão de projeto de IA para apps com LLM e agentes, do grupo Contexto e conhecimento. Navegar relações entre entidades quando a resposta depende de conexões, não de um trecho.
GraphRAG
RAG com Grafo (GraphRAG) é um padrão de projeto de IA para apps com LLM e agentes, do grupo Contexto e conhecimento. Navegar relações entre entidades quando a resposta depende de conexões, não de um trecho.
Representar entidades e suas relações num grafo e percorrer essas relações para responder perguntas que dependem de conexões, não de um trecho isolado. Serve para domínios em que a resposta está espalhada por vários documentos ligados entre si.
O detetive que não procura uma pista, procura o fio que liga as pistas.
Bruna é engenheira de confiabilidade numa empresa com 300 microsserviços. Quando um alerta dispara no serviço de pagamentos, a pergunta certa raramente é “o que diz a documentação de pagamentos?”. É “quais serviços pagamentos chama, quais desses mudaram hoje, e quem é o dono de cada um?”.
O RAG da empresa busca trechos parecidos com a pergunta. Ele acha a página de pagamentos, mas não segue as dependências. A informação de que o serviço de antifraude (chamado por pagamentos) teve deploy às 14h está em outro documento, que não se parece com a pergunta.
Esse tipo de resposta não está em nenhum trecho. Está na ligação entre trechos.
O mesmo buraco aparece em contratos e NF-e: “esta cláusula ainda vale?” depende de quem revogou o quê, não de um parágrafo parecido com a pergunta. RAG por similaridade não caminha.
A pergunta que este padrão responde: como responder perguntas cuja resposta depende de relações entre várias coisas, espalhadas por várias fontes?
Construa um grafo com as entidades do domínio (serviços, pessoas, contratos, cláusulas, leis) e as relações entre elas (chama, é dono de, cita, revoga, depende de). As relações vêm de fontes estruturadas sempre que possível (catálogo de serviços, banco de dados) e, quando necessário, são extraídas de texto por um modelo.
Na hora da pergunta:
Microsoft Research publicou GraphRAG (Edge et al., 2024) como uma abordagem para perguntas que pedem visão de um corpus inteiro: o grafo é extraído dos documentos e grupos densos ganham resumos. A lição que vale para produção, mesmo fora daquele paper, é a mesma: extraia relações de fonte confiável primeiro; extração por modelo entra só onde a fonte não existe, com revisão.
flowchart LR
Q(["1. Pergunta"]):::io --> E(["2. Identificar entidades"]):::llm
E --> T["3. Percorrer o grafo"]:::tool
G[("Grafo: entidades e relações")]:::store --> T
T --> C["4. Subgrafo e trechos ligados"]:::tool
D[("Documentos")]:::store --> C
C --> M(["5. Responder citando o caminho"]):::llm
Pergunta: “Quais serviços chamados por pagamentos tiveram deploy nas últimas 6 horas?”.
O resolvedor de nomes mapeia “pagamentos” para o nó payments. A consulta anda no máximo 2 saltos em CALLS, cruza com DEPLOYED recente e OWNS:
payments -CALLS-> antifraud dono: time-risco deploy: r-8812 às 14:05
payments -CALLS-> ledger dono: time-finanças deploy: (nenhum nas 6h)
payments -CALLS-> notify dono: time-msg deploy: r-8801 às 09:12
O contexto que o modelo recebe não é “os 5 trechos mais parecidos com a pergunta”. É essa lista de fatos, mais um parágrafo da runbook de cada serviço que mudou. A resposta cita o caminho. Se antifraud não estiver no grafo, o sistema diz que a entidade não resolveu, em vez de inventar um dono.
Compare com o RAG por trechos na mesma pergunta:
| Desenho | O que entra no contexto | O que costuma sair |
|---|---|---|
| RAG por similaridade | Página de pagamentos, às vezes a de deploy genérico | Uma descrição de pagamentos, sem o deploy das 14h |
| RAG com grafo | Arestas CALLS + deploys recentes + donos |
“antifraud (r-8812, 14:05), notify (r-8801, 09:12)” |
schema Entities { names: list[string], intent: "dependencies" | "ownership" | "impact" | "other" }
function answer(question):
ents = llm(ENTITY_PROMPT, question, model="small") -> Entities # 2
nodes = graph.resolve(ents.names) # nome para nó
sub = graph.query("""
MATCH (s:Service)-[:CALLS*1..2]->(d:Service)
WHERE s.id IN $ids
OPTIONAL MATCH (d)<-[:OWNS]-(t:Team)
OPTIONAL MATCH (d)-[:DEPLOYED]->(r:Release) WHERE r.at > now() - duration('PT6H')
RETURN s, d, t, r""", ids=nodes) # 3
passages = docs.for_entities(sub.nodes, k_per_entity=2) # 4
trace.span("graph_rag", nodes=len(sub.nodes), edges=len(sub.edges))
return llm(ANSWER_PROMPT, question, render(sub), passages) # 5
Grafo extraído só de texto, sem revisão, costuma parecer inteligente na demo e mentir em produção. Se a única fonte for PDF de contrato, extraia poucas relações (partes, vigência, índice) com schema e humano no circuito, como no cenário de NF-e.
Comece com um grafo que cabe em memória, alimentado pelo catálogo que você já tem. Banco de grafos entra quando as consultas ficam mais ricas que uma vizinhança de 2 saltos, não no primeiro dia.
CALLS numa malha densa explodem o subgrafo. LIMIT e profundidade máxima são parte do padrão, não otimização.payments CALLS antifraud.| Aspecto | O que acontece | Como aparece no trace |
|---|---|---|
| Chamadas | Identificação de entidades, uma consulta ao grafo e a resposta | span graph_rag com nós e arestas |
| Custo | Manter o grafo atualizado; extração por modelo custa por documento | custo de ingestão |
| Latência | A consulta ao grafo costuma ser rápida; travessias profundas não | duração da consulta |
| Falha: entidade não resolvida | Nome escrito de outro jeito não acha o nó | subgrafo vazio |
| Falha: grafo desatualizado | Relação que já não existe | resposta com dependência antiga |
| Falha: subgrafo grande demais | Travessia sem limite enche o contexto | número de nós acima do esperado |
{ "question": "Quais serviços chamados por pagamentos tiveram deploy nas últimas 6 horas?", "expected_path": ["payments -CALLS-> antifraud", "antifraud -DEPLOYED-> r-8812"], "expected_answer_contains": ["antifraud"] }
Avalie também o caso negativo: um serviço que pagamentos não chama não pode aparecer no caminho. Falso positivo de aresta é pior que um “não sei”.
Se nodes no span passa de algumas dezenas, a consulta está larga demais para o tipo da pergunta. Ajuste profundidade e tipo de relação, não o prompt da resposta.
O grafo concentra mapa da organização: quem chama quem, quem é dono, o que mudou. Permissão entra na travessia. Um estagiário que pergunta sobre pagamentos não deve receber o subgrafo da folha. Filtrar nós depois de o modelo responder é tarde.
Relações extraídas de documento externo (PDF de fornecedor, wiki aberta) são conteúdo não confiável persistido. Uma cláusula que “estabelece que o fornecedor é dono do serviço de pagamentos” não pode virar aresta OWNS sem revisão. Veja Ordem Vinda do Dado.
| Prós | Contras |
|---|---|
| Responde perguntas de relação e vários saltos | Construir e manter o grafo custa |
| Respostas explicáveis pelo caminho | Grafo errado gera respostas confiantes e erradas |
| Usa fontes estruturadas que já existem | Resolver nomes para entidades é difícil |
| Combina bem com trechos de texto | Mais uma infraestrutura (banco de grafo) |
As duas abas resolvem as entidades da pergunta, percorrem as relações e montam o contexto para a resposta.
Python. Use quando o grafo de dependências cabe em memória, carregado do catálogo de serviços.
import anthropic
import networkx as nx
client = anthropic.Anthropic()
def build_graph(catalog: list[dict], deploys: list[dict]) -> nx.DiGraph:
g = nx.DiGraph()
for svc in catalog: # fonte estruturada: catálogo de serviços
g.add_node(svc["name"], kind="service", owner=svc["owner"])
for dep in svc["calls"]:
g.add_edge(svc["name"], dep, rel="CALLS")
for d in deploys: # eventos recentes
g.nodes[d["service"]].setdefault("deploys", []).append(d)
return g
def neighborhood(g: nx.DiGraph, start: str, depth: int = 2) -> list[str]: # 3
lines = []
for node, dist in nx.single_source_shortest_path_length(g, start, cutoff=depth).items():
data = g.nodes[node]
recent = [d["at"] for d in data.get("deploys", [])]
lines.append(f"{node} (dono: {data.get('owner')}, distância {dist}, deploys recentes: {recent})")
return lines
def answer(question: str, g: nx.DiGraph) -> str:
names = [n for n in g.nodes if n in question.lower()] # 2. resolução simples por nome
context = "\n".join(line for n in names for line in neighborhood(g, n)) # 4
reply = client.messages.create( # 5
model="claude-sonnet-5-5", max_tokens=2000,
system="Responda usando só as relações listadas. Mostre o caminho de dependências que sustenta a resposta.",
messages=[{"role": "user", "content": f"<grafo>\n{context}\n</grafo>\n\n{question}"}],
)
return reply.content[-1].textO que você ganhou: um grafo montado de fontes confiáveis, com travessia limitada e explicável. O que você perdeu: a resolução por nome exato é frágil; use apelidos e um modelo pequeno quando os nomes variarem.
TypeScript. Use quando o grafo vive num banco de grafos e a travessia é uma consulta.
import Anthropic from "@anthropic-ai/sdk";
import neo4j from "neo4j-driver";
const client = new Anthropic();
const driver = neo4j.driver(process.env.NEO4J_URL!, neo4j.auth.basic(process.env.NEO4J_USER!, process.env.NEO4J_PASSWORD!));
export async function answer(question: string, serviceIds: string[]) {
const session = driver.session({ defaultAccessMode: neo4j.session.READ });
try {
const res = await session.run( // 3. travessia limitada a 2 saltos
`MATCH (s:Service)-[:CALLS*1..2]->(d:Service) WHERE s.id IN $ids
OPTIONAL MATCH (t:Team)-[:OWNS]->(d)
OPTIONAL MATCH (d)-[:DEPLOYED]->(r:Release) WHERE r.at > datetime() - duration('PT6H')
RETURN s.id AS from, d.id AS to, t.name AS owner, collect(r.version) AS releases LIMIT 50`,
{ ids: serviceIds },
);
const facts = res.records
.map((r) => `${r.get("from")} -> ${r.get("to")} (dono: ${r.get("owner")}, releases recentes: ${r.get("releases")})`)
.join("\n"); // 4
const reply = await client.messages.create({ // 5
model: "claude-sonnet-5-5",
max_tokens: 2000,
system: "Responda usando só as relações listadas. Mostre o caminho de dependências que sustenta a resposta.",
messages: [{ role: "user", content: `<grafo>\n${facts}\n</grafo>\n\n${question}` }],
});
return reply.content.find((b) => b.type === "text")?.text ?? "";
} finally {
await session.close();
}
}O que você ganhou: consultas de vários saltos executadas pelo banco, com limite explícito. O que você perdeu: mais uma infraestrutura para manter sincronizada com as fontes.
RAG com Grafo (GraphRAG) é um padrão de projeto de IA para apps com LLM e agentes, do grupo Contexto e conhecimento. Navegar relações entre entidades quando a resposta depende de conexões, não de um trecho.
Nos cenários reais Assistente jurídico e Agente de incidentes (on-call).