GraphRAG para microsserviço: 7 passos para saber quais ADRs quebram seu service
Pergunta que RAG vetorial nunca respondeu direito para mim: “quais ADRs, se eu ignorar, quebram o microsserviço orders-api?”. Rodei essa pergunta contra dois setups no mesmo monorepo simulado de 47 microsserviços e 312 ADRs (Architecture Decision Records em formato MADR). Um setup era RAG vetorial clássico com embedding text-embedding-3-large. O outro era GraphRAG da Microsoft (microsoft/graphrag, release de junho/2026) alimentado com Tree-sitter para o código e um extrator de entidade específico para ADRs.
Resultado: o RAG vetorial trouxe 8 ADRs relevantes “por similaridade de palavra”. Só que 3 desses não afetavam orders-api de fato, e 4 dependências reais (via serviço intermediário payments-adapter) simplesmente não apareceram. Precisão baixa, recall pior. O GraphRAG retornou 11 ADRs, com 9 verdadeiramente ligados a orders-api via caminho SERVICE --DEPENDS_ON--> SERVICE --GOVERNED_BY--> ADR.
Este post é o roteiro em 7 passos de como cheguei nesse setup, com os tradeoffs que quase me fizeram desistir na metade. Escrevo isso porque três projetos de KG que acompanhei em 2026 morreram no passo 2 — extração de entidade — e não quero que o seu morra também.
Aviso de escala: o “monorepo de 47 microsserviços e 312 ADRs” é uma simulação que montei para benchmark. Baseei a topologia em casos públicos (Uber, Airbnb monorepo posts) mas os números específicos são meus. Não é dado de cliente.
Por que a busca vetorial perde neste tipo de pergunta
RAG vetorial é ótimo para “achar documento próximo”. Você embed a pergunta, embed cada ADR, cosine similarity, top-K, joga no LLM. Funciona bem em base de conhecimento textual homogênea.
O problema aparece quando a pergunta é relacional, não semântica:
- “Qual ADR afeta o microsserviço X?” — precisa saber que X depende de Y, e Y é governado por ADR-042.
- “Qual decisão de arquitetura foi contradita pela nova ADR-088?” — precisa comparar
SUPERSEDESexplícito, não similaridade de palavra. - “Quais serviços vão precisar rebuild se eu deprecar ADR-015?” — precisa expandir o grafo em
Nhops a partir do nó ADR.
A busca vetorial responde essas por acidente, quando o texto dos ADRs menciona o nome do serviço literalmente. Basta o autor do ADR ter escrito “aplica-se ao domínio de pagamentos” em vez de “aplica-se a payments-adapter” para o vetor não conectar mais nada.
Passo 1: definir o schema de nó e aresta antes de escrever qualquer código
O erro mais caro que vi (e cometi) é começar pela extração. Você vai extrair coisa demais, coisa errada, e o grafo vira uma nuvem de nós órfãos.
O schema que travei depois de 3 iterações:
Nós:
| Rótulo | Descrição | Propriedades chave |
|---|---|---|
Service | Microsserviço | name, repo_path, owner_team |
File | Arquivo-fonte | path, language, service (fk) |
Function | Função/método | name, file, signature |
ADR | Architecture Decision Record | id, status, date, title, path |
Decision | Decisão granular dentro de ADR | text, category |
Arestas:
| Tipo | Direção | Cardinalidade |
|---|---|---|
CONTAINS | Service → File | 1 |
CALLS | Function → Function | N |
DEPENDS_ON | Service → Service | N |
GOVERNED_BY | Service → ADR | N |
SUPERSEDES | ADR → ADR | 1 |
RELATES_TO | ADR → Service | N |
Deixa GOVERNED_BY e RELATES_TO separados, mesmo parecendo redundante. GOVERNED_BY é derivado explicitamente do frontmatter do MADR (campo applies_to:), RELATES_TO vem da extração LLM do corpo do ADR. Você vai querer diferenciar depois na hora de dar peso.
Passo 2: extração determinística com Tree-sitter (não use LLM aqui)
Este é o passo em que os projetos morrem. A tentação é jogar o repo inteiro na API do GPT-5 e pedir “extraia funções, classes, chamadas”. Não faça isso. Sai caro, sai errado, sai lento.
Use Tree-sitter para tudo que é estrutura sintática:
import tree_sitter_python as tspython
from tree_sitter import Language, Parser
PY_LANGUAGE = Language(tspython.language())
parser = Parser(PY_LANGUAGE)
def extract_functions(source_bytes: bytes) -> list[dict]:
tree = parser.parse(source_bytes)
query = PY_LANGUAGE.query("""
(function_definition
name: (identifier) @func_name
parameters: (parameters) @params)
""")
captures = query.captures(tree.root_node)
return [
{"name": node.text.decode(), "line": node.start_point[0] + 1}
for node, tag in captures if tag == "func_name"
]
Rodou local, custou zero, é reprodutível. Extrai Function, Class, Import, e as arestas CALLS, IMPORTS, CONTAINS. O conteúdo do arquivo nem sai da sua máquina, o que resolve o problema de compliance para código proprietário.
O papel do LLM vem depois, no passo 5, para o dado não estruturado (ADR texto livre).
Passo 3: mapear microsserviço via convenção de path
O grafo até aqui não sabe o que é microsserviço. Ele só tem arquivo e função. Preciso agrupar.
A convenção mais barata é services/<nome>/** no monorepo. Um walker simples:
from pathlib import Path
def service_of(file_path: str, repo_root: Path) -> str | None:
rel = Path(file_path).relative_to(repo_root)
parts = rel.parts
if len(parts) >= 2 and parts[0] == "services":
return parts[1]
return None
Criei o nó Service para cada diretório e a aresta CONTAINS para cada arquivo. Simples, mas 100% preciso — sem chamar LLM.
Para DEPENDS_ON entre serviços, usei duas heurísticas: (a) import cruzado entre serviços (from services.payments import ... dentro de services/orders/) e (b) client HTTP configurado (padrão PAYMENTS_URL = os.environ[...]). Cobertura combinada: 91% das dependências no meu benchmark. Os 9% restantes são chamadas via message broker, que exigiram extração manual do arquivo topics.yaml.
Passo 4: parse dos ADRs (frontmatter primeiro, corpo depois)
ADRs no formato MADR (Michael Nygard / adr.github.io) têm frontmatter estruturado:
---
id: ADR-042
title: "Adotar autenticação JWT stateless para orders-api"
status: accepted
date: 2026-03-14
applies_to:
- orders-api
- api-gateway
supersedes: ADR-018
---
Este frontmatter é ouro. Extrai direto:
- Nó
ADRcom id, title, date, status - Aresta
GOVERNED_BYpara cada serviço emapplies_to - Aresta
SUPERSEDESpara o ADR anterior
Sem LLM, sem ambiguidade, sem custo. Se o seu time não escreve applies_to no frontmatter, esta é a primeira mudança de processo que vai destravar tudo. É baratíssimo pedir ao autor do ADR para marcar 2 tags. Um lint no PR que rejeita ADR sem applies_to custa 10 linhas de código.
Passo 5: agora sim, LLM — mas só para o corpo do ADR
O corpo do ADR é texto livre com detalhe importante que o frontmatter não captura: “esta decisão afeta indiretamente o serviço notifications-worker porque muda o formato do payload que ele consome”. O notifications-worker não está em applies_to: — está numa frase.
Aqui o LLM ganha. Prompt simples com output estruturado:
prompt = f"""
Extraia entidades e relações do texto do ADR abaixo.
Retorne JSON com esta estrutura:
{{
"affected_services": ["nome-do-service", ...], // serviços mencionados no corpo
"referenced_adrs": ["ADR-XXX", ...], // ADRs citados
"decisions": [ // decisões granulares
{{"text": "...", "category": "security|performance|api|data|other"}}
]
}}
Texto do ADR:
---
{adr_body}
---
"""
Rodei com Claude Sonnet 4.6, temperature 0. Custo médio: ~$0.008 por ADR. Para 312 ADRs, deu $2.50. Ficou barato o suficiente para eu reprocessar tudo a cada mudança de schema.
Depois criei arestas RELATES_TO para cada affected_service extraído, e REFERENCES para cada referenced_adrs.
Passo 6: consultar com Cypher, não com “prompt engineering”
Muita gente monta o KG e depois joga tudo no LLM em prosa. Erro. A vantagem do grafo é ter linguagem de consulta declarativa.
A pergunta original (“quais ADRs afetam orders-api?”) vira Cypher direto:
MATCH (s:Service {name: 'orders-api'})
MATCH (s)-[:DEPENDS_ON*0..2]->(dep:Service)
MATCH (dep)-[:GOVERNED_BY|RELATES_TO]->(adr:ADR)
WHERE adr.status = 'accepted'
RETURN DISTINCT adr.id, adr.title, adr.date
ORDER BY adr.date DESC
Traduzindo: pega orders-api, expande até 2 hops nas dependências, coleta todos os ADRs que governam ou se relacionam com aqueles serviços, filtra por status accepted. Roda em ~40ms em Neo4j 5.x com o grafo carregado em memória.
O LLM entra só na última etapa, para gerar a resposta em linguagem natural a partir dos resultados. Assim o LLM não “raciocina sobre grafo” — apenas sumariza o que já veio filtrado pelo Cypher.
Passo 7: fechar o loop com um lint que rejeita ADR sem service explícito
O grafo é um ativo vivo. Se você não travar a qualidade da entrada, ele degrada em 6 meses.
Coloquei um GitHub Action que roda em cada PR que toca docs/adr/*.md:
# Rejeita se o frontmatter não tiver applies_to
python3 tools/lint_adr.py docs/adr/*.md --require applies_to
E outro que atualiza o grafo automaticamente após merge:
on:
push:
branches: [main]
paths:
- 'docs/adr/**'
- 'services/**/*.py'
- 'services/**/*.ts'
jobs:
rebuild-kg:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: python3 tools/rebuild_kg.py --incremental
Custo por rebuild incremental: ~$0.30 em LLM (só reprocessa ADRs modificados). Custo por rebuild full (raro): ~$3. Barato o suficiente para deixar em produção.
Onde eu quase desisti (e por que você não deveria)
No passo 2 achei que Tree-sitter ia cobrir 100%. Não cobre. Chamadas dinâmicas (getattr, factory patterns, dependency injection) não aparecem no AST. Passei 3 dias tentando resolver isso com o LLM antes de aceitar que a cobertura de 85% é ok — o Cypher expande via DEPENDS_ON* e pesca a maior parte do que o AST perdeu.
No passo 5 quase gastei $150 tentando fazer o LLM extrair “todos os conceitos” do corpo do ADR. Não funciona. Você extrai barulho. Cortando o prompt para 3 categorias específicas (affected_services, referenced_adrs, decisions) o resultado ficou útil e barato.
E o resultado final vs o RAG vetorial baseline:
| Métrica | RAG vetorial | GraphRAG (este pipeline) |
|---|---|---|
| Precisão em “quais ADRs afetam service X” | 62% | 89% |
| Recall (ADRs realmente relevantes) | 55% | 82% |
| Latência p50 | 380ms | 470ms |
| Custo por consulta | $0.002 | $0.004 |
| Custo de indexação inicial (312 ADRs + 47 services) | $8 | $14 |
O GraphRAG custa quase o dobro por consulta e por indexação. Em compensação, ganha 27 pontos de precisão em uma pergunta que o time faz toda semana antes de refatorar. Para nós valeu. Para você, depende de quantas vezes por semana alguém pergunta “isso quebra o quê?”.
Se o seu monorepo tem menos de 10 serviços e menos de 30 ADRs, honestamente fica no vetor + grep. GraphRAG começa a pagar quando o grafo de dependência tem mais de 3 hops médios entre serviços, o que costuma acontecer a partir de ~20-30 microsserviços.
Já escrevi sobre esse contraste com foco em custo em GraphRAG vs RAG clássico: 4 projetos, quando vale 7x o custo, e sobre a jornada de construir a base de conhecimento em 3 meses em O banco de conhecimento de 300 nós em 3 meses e MCP com 27k tokens perde para KG de 8x menos — este post é o “próximo passo” prático de ambos, focado no caso ADR × microsserviço.
ken imoto · WebRTC & Voice AI engineer · kenimoto.dev · TabNews
Este artigo foi útil?