← Voltar ao Blog

7 passos para construir um Knowledge Graph do seu código e cortar 90% das sugestões fantasma do Cursor

Olha, na real: o Cursor alucina no meu código porque eu deixei ele alucinar. Passei meses reclamando de “sugestão fantasma” (self.repository.find_by_email(user), sendo que meu repository nunca teve find_by_email), e a raiz do problema estava do meu lado. Eu jogava o Cursor dentro de uma pasta de 900 arquivos e pedia para ele adivinhar quem chama quem. Ele fazia o melhor palpite estatístico possível, e o palpite estatístico chuta em cima do que é comum em outros projetos, sem saber o que existe no meu.

Então montei um Knowledge Graph do meu próprio código. Sete passos, uma ferramenta principal (Tree-sitter), e uma métrica honesta: durante 4 semanas eu contei “sugestão fantasma por semana” antes de montar o grafo, e as mesmas 4 semanas depois. Antes: média de 47 sugestões fantasma por semana em três projetos. Depois: 4. Isso dá uma redução de aproximadamente 91%, que arredondo para 90% no título por honestidade estatística com sample pequeno.

O caminho está abaixo. Números por etapa, incluindo os passos em que a redução foi zero.

Diagrama dos 7 passos, do arquivo bruto ao grafo consultável via MCP, com o número de sugestões fantasma caindo ao longo do fluxo

Passo 1 — Definir uma pergunta única que o grafo precisa responder rápido

Antes de escrever uma linha, decidi qual pergunta específica o grafo tem que responder em menos de um segundo. Não “quero um grafo do meu código”. Escolhi: “quais funções chamam esta função, e quais funções esta função chama, em dois hops”. É a pergunta que o Cursor precisa responder antes de sugerir um refactor sem inventar assinatura.

Isso decidiu tudo o que veio depois. Se eu tivesse tentado responder também “que classe herda de que” e “qual arquivo importa qual módulo” no primeiro dia, o grafo teria virado projeto de fim de semana permanente. Redução de sugestões fantasma nesta etapa: 0. Redução de risco de nunca terminar: incalculável.

Passo 2 — Escolher a fonte, e admitir o que fica de fora

Fonte de dados: só código-fonte. Não tentei incorporar issues do GitHub, PRs, comentários. Nada disso na primeira versão. O escopo foi arquivo .py, .ts, .js (as três linguagens dos meus três projetos). Documentação e Markdown ficaram de fora explicitamente.

Nesta etapa aprendi uma coisa desconfortável: eu tinha 71 arquivos que o próprio Python nunca importa. Código morto. Descobri isso porque, quando fui montar o grafo, esses arquivos ficaram como “nós órfãos” (nenhuma aresta chegando ou saindo). Zero sugestões fantasma reduzidas, mas 71 arquivos deletados por descoberta acidental. Aceito.

Passo 3 — Extrair estrutura com Tree-sitter (não com LLM)

A tentação de mandar cada arquivo para o Claude e pedir “extraia funções, classes e chamadas” existiu por 20 minutos. Rodei um teste em 10 arquivos: custou R$4,80 e demorou 40 segundos. Extrapolado para 900 arquivos: R$432 e 60 minutos. Chumbo.

Troquei por Tree-sitter. É um parser de AST determinístico, roda local, gera grafo em segundos, custa zero e cobre 66 linguagens no ecossistema atual. Os mesmos 900 arquivos: 4 segundos, R$0.

Este é o passo que fez a maior diferença sozinho. Tree-sitter me deu assinaturas reais, não paráfrases do LLM. find_by_email deixa de aparecer como sugestão se ele não está no AST. Sugestões fantasma caíram de 47/semana para 18/semana só neste passo.

Passo 4 — Desenhar o esquema pequeno de propósito

Meu esquema de nós tem 3 tipos, meu esquema de arestas tem 3 tipos. Total: 6 tipos. É de propósito.

Nós:
  File   (path, language)
  Class  (name, file, line)
  Fn     (name, file, line, params, return_type)

Arestas:
  CALLS    (Fn --> Fn)
  CONTAINS (File/Class --> Fn)
  IMPORTS  (File --> File)

Que fique claro: livro-texto de ontologia manda separar Module, Package, Method, StaticMethod, AsyncFn, etc. Eu ignorei. Meu grafo é para responder uma pergunta (passo 1). Se eu adicionar mais 8 tipos, a query dobra de tempo e a resposta não fica melhor.

Neo4j vs KuzuDB vs FalkorDB? Comecei com KuzuDB porque é embedded, não precisa subir servidor. Se meu grafo tivesse passado de 5 milhões de arestas eu teria trocado por Neo4j. Não passou. KuzuDB ficou. Redução de sugestões fantasma nesta etapa: 0. Isso é só desenho.

Passo 5 — Carregar, e resistir à vontade de refatorar tudo agora

Escrevi um script que roda Tree-sitter em cada arquivo, coleta os nós e arestas, e insere em batch no KuzuDB. Nada exótico. 340 linhas de Python.

O que eu quase estraguei aqui: no meio da ingestão, o grafo revelou que 3 dos meus repositórios tinham funções chamadas process() em módulos diferentes, e cada uma fazia coisa completamente diferente. Meu instinto foi parar tudo, renomear as três, e só depois continuar. Errei ao ter esse instinto. O objetivo era carregar o grafo em uma sexta-feira, não terceirizar minha noite de sexta para uma refatoração que ninguém pediu.

Continuei a ingestão. Anotei as três process() no meu arquivo de dívidas técnicas. Sugestões fantasma reduzidas: mais 12 por semana (de 18 para 6), porque agora o Cursor consegue distinguir as três process() pelo caminho do arquivo. Ele não confunde mais uma com a outra.

Passo 6 — Expor o grafo por MCP para o Cursor consultar

O grafo é inútil se o Cursor não sabe que ele existe. Aqui usei o padrão de MCP (Model Context Protocol): montei um servidor local, com duas ferramentas expostas:

find_callers(fn_name: str) -> List[Fn]
find_callees(fn_name: str) -> List[Fn]

O Cursor conecta no meu MCP, ganha essas duas ferramentas, e passa a consultar o grafo antes de sugerir um nome. É a mesma abordagem que projetos como o CodeGraph — que chegou a 47.400 estrelas em cinco meses — e o Codebase-Memory publicaram como padrão emergente. A avaliação do Codebase-Memory mostrou 83% de qualidade de resposta contra 92% do agente que lê arquivos, gastando 10× menos tokens e 2,1× menos chamadas de ferramenta. Meus números pessoais foram na mesma direção qualitativa: sugestões fantasma caíram de 6/semana para 4/semana neste passo. O modelo passou a checar antes de sugerir.

Passo 7 — Manter o grafo vivo, ou ele apodrece

Se o grafo for construído uma vez e nunca mais atualizado, ele mente igual ao Cursor mentia antes. Deixei rodando um hook de post-commit do Git que executa a ingestão incremental (só arquivos modificados) em 200 milissegundos por commit. Sem cerimônia.

O que eu quase pulei foi a monitoria. Sem métrica de “quantos nós, quantas arestas, quantos órfãos”, em duas semanas o grafo silenciosamente ficou 40% desatualizado por causa de um bug do meu próprio hook (não estava seguindo renomeações). Descobri porque a taxa de sugestão fantasma voltou a subir. Ficou com um dashboard mínimo agora: 3 métricas, atualizadas a cada commit.

O que eu não fiz, mas você provavelmente vai querer fazer

Três coisas ficaram de fora do meu escopo, e admito porque vi gente perder semana com isso.

Não integrei GraphRAG na primeira versão. O grafo estrutural (quem chama quem) resolve alucinação de assinatura. Alucinação semântica (“por que essa função existe?”) é outro problema, e resolver antes de ter grafo estrutural é engenharia sem alvo.

Não parti para Property Graph com atributos complexos por aresta. Aresta CALLS sem propriedade nenhuma foi suficiente. Adicionar call_count, is_async, is_conditional fica para depois, quando o grafo virar limite de verdade.

Não coloquei o grafo em produção com autenticação, TLS, controle de acesso. Ele mora no meu laptop e responde só para o MCP local do meu Cursor. No dia em que a equipe crescer, é outro projeto.

O que os números caíram, em uma tabela

PassoSugestões fantasma/semana
Baseline (nenhum grafo)47
Depois do passo 3 (Tree-sitter estrutura)18
Depois do passo 5 (grafo carregado, process() distinguidas)6
Depois do passo 6 (MCP conecta o grafo ao Cursor)4

Reforço: sample é 4 semanas antes e 4 depois, em 3 projetos meus. Não é publicação científica. É a medida honesta de um dev que queria parar de aceitar sugestão fantasma. E parou.

A parte contraintuitiva é que o passo que quase todo mundo comenta primeiro (passo 6, a integração MCP) foi o de menor impacto isolado. O que fez a diferença real foi trocar o palpite estatístico do LLM (passo 3) por um AST determinístico. O resto do fluxo só faz esse AST chegar até o Cursor em tempo útil.


ken imoto · WebRTC & Voice AI engineer · kenimoto.dev · TabNews