Code Graphs para Coding Agents: O Formato de Entrega Importa Mais que o Algoritmo
Passei um fim de semana apontando um coding agent para um monorepo Go de 480 mil linhas e vendo ele entrar em loop de grep por 38 chamadas de ferramenta em uma pergunta. Code graphs derivados de AST resolvem isso, mas o formato de entrega — MCP local via stdio, serviço remoto ou skill — muda a economia mais do que o algoritmo do grafo. Aqui está onde eu colocaria um em 2026, com um indexador Go mínimo que dá para soltar ao lado do agente.
Passei um fim de semana apontando um coding agent para um monorepo Go de 480 mil linhas e vendo ele perder. O agente entrou em loop de grep por 38 chamadas de ferramenta tentando responder uma pergunta: se eu mudar a assinatura de BillingClient.Charge, quais handlers quebram? Ele achou o método. Leu o arquivo. Perdeu três chamadores porque os nomes dos pacotes deles não tinham nada em comum com "billing". Custo total: menos de três dólares. Saída útil total: um quase-acerto.
É sobre aquela tarde que este post fala. O pitch de coding agents num repo de 5 mil linhas é honesto. O pitch num codebase real é desonesto por padrão — agentes achatam estrutura em similaridade vetorial e loops de grep, e estrutura é exatamente o que você precisa para responder à pergunta.
Code graphs resolvem isso. A pergunta de 2026 não é se construir um. Os argumentos para isso já estão decididos a partir de aproximadamente 50 mil linhas. A pergunta é onde colocá-lo, porque o formato de entrega muda a economia mais do que o algoritmo do grafo.
Dois modos de falha fazem a maior parte do estrago
O primeiro é o achatamento de contexto. RAG vetorial encontra chunks que parecem lexicalmente similares à query. Pergunte "quais controllers chamam ShoppingCartService" e você recebe de volta ShoppingCartService mais outras classes de serviço cujo texto se parece com ele. Os controllers que dependem dele nunca entram no top-k porque o texto deles nunca menciona carrinho de compras. O paper da DKB sobre graph-RAG para codebases reproduz isso em Shopizer, ThingsBoard e OpenMRS Core, e a lacuna não fecha com k maior.
O segundo é a cegueira multi-hop. "Se eu mudar essa interface, o que quebra?" precisa de uma traversal, não de um score de similaridade. Um agente pode simular uma traversal com grep — mas o custo é linear no tamanho do repo, todo hop custa uma chamada de ferramenta, e o agente geralmente desiste depois do terceiro nível.
Um grafo pré-computado colapsa os dois. Construa uma vez a partir do AST, armazene as arestas, faça traversal sob demanda. A escolha interessante é onde esse armazenamento vive.
As opções em cima da mesa
Existem quatro formas de dar ao coding agent consciência estrutural sobre um repo grande, mais dois baselines sem estrutura que vou descartar rapidamente.
Os baselines, para completude. Despejar long-context (jogar o repo inteiro numa janela de 1M tokens) queima dinheiro em toda query e só funciona uma vez. RAG vetorial ingênuo acha algo rápido e acha a coisa errada em perguntas multi-hop. Grep agêntico puro é o que o agente faz por padrão e é justamente do que estou tentando escapar.
As opções reais:
| Formato de entrega | O que é | Onde vive | Cold-start | Custo por query |
|---|---|---|---|---|
| Apenas skill | Um SKILL.md ensinando o agente a usar rg, ast-grep e a CLI do tree-sitter com competência | Contexto, sob demanda | Zero | Limitado pelas primitivas; ainda O(repo) para "quem chama X" |
| MCP local stdio | Indexador tree-sitter → SQLite entregue como binário único e rodando ao lado do agente | Máquina do desenvolvedor | Segundos para um repo médio | Sub-milissegundo em queries quentes |
| MCP remoto | Grafo centralizado (Apache AGE, FalkorDB, Neo4j) acessado via HTTP/SSE | Algum servidor | Segundos a minutos | Hop de rede mais lookup |
| Adaptativo (skill + MCP local) | Skill roteia entre primitivas e o MCP com base na pergunta | Máquina do desenvolvedor | Igual ao local | Barato por padrão, escala sob demanda |
Este post é sobre escolher entre as linhas dois, três e quatro. O baseline só-skill é a resposta certa com frequência o bastante para ser mencionado, e nunca o bastante para ser recomendado sozinho.
Os números que realmente decidem
Dois papers recentes rodaram o mesmo workload nesses paradigmas e publicaram todo número que você precisa.
Em custo de indexação, medido no Shopizer (cerca de 1.200 arquivos Java), os autores do DKB cronometram o knowledge graph derivado de AST em 2,81 segundos end-to-end. Um knowledge graph extraído por LLM sobre o mesmo repo leva 200,14 segundos, e — esta é a parte que ninguém cita — a extração é probabilística. A taxa de sucesso por arquivo é 0,69. De ~1.200 arquivos, 377 são silenciosamente dropados. Seu "knowledge graph" está com um terço do codebase faltando e o agente não tem como saber qual terço.
Em custo end-to-end em quinze queries de arquitetura e rastreamento de código, também do paper da DKB, a faixa é:
- RAG vetorial ingênuo: $0,04
- Grafo derivado de AST: $0,09
- Grafo extraído por LLM: $0,79
No workload maior do OpenMRS-core + ThingsBoard, a mesma razão se mantém: ingênuo $0,149, grafo AST $0,317, grafo LLM $6,80. A abordagem AST custa aproximadamente 2× o baseline ingênuo e aproximadamente 1/20× a abordagem LLM, e responde mais perguntas multi-hop corretamente do que ambos.
Em latência de query, o paper Codebase-Memory mede traversal BFS sobre um CTE recursivo de SQLite na marca de sub-milissegundo em queries quentes. A mesma avaliação reporta 99% menos tokens do que baselines de exploração de arquivos em perguntas equivalentes. Os números parecem otimistas até você ler as queries — elas são exatamente as perguntas que um coding agent faz, só que respondidas contra uma tabela em vez de um corpus.
Em cobertura, o grafo AST atinge 0,90 dos chunks no Shopizer com 1.158 nós. O grafo extraído por LLM atinge 0,64 com 842 nós. A diferença não é gosto algorítmico; é o extrator estocástico falhando de formas previsíveis.
A conclusão honesta sobre a questão do algoritmo: código já tem um parser determinístico. Use-o. Reserve a extração de grafo via LLM para os corpora que não têm um — prosa, PDFs, tickets de suporte.
O atravessamento sobre o qual ninguém fala: servidores MCP comem sua janela de contexto
A coisa que mais me surpreendeu não está em nenhum dos dois papers. Está em dois posts que apareceram com meses de diferença: Code Execution with MCP da Anthropic em 4 de novembro de 2025, e a atualização Code Mode da Cloudflare em abril de 2026. Ambos chegam à mesma conclusão por lados diferentes.
Definições de ferramentas para um servidor MCP conectado ficam no contexto do modelo antes da primeira mensagem do usuário. Um setup típico de cinco servidores para desenvolvedor adiciona cerca de 55.000 tokens de definições antes que alguém diga olá. A medição da Cloudflare no MCP da Cloudflare — mais de 2.500 endpoints de API — chega a 1,17 milhão de tokens de definições se carregadas avidamente. A solução em ambos os posts é a mesma: apresentar as ferramentas MCP como uma API de código, deixar o agente descobrir e chamar escrevendo pequenos scripts em uma sandbox, e cortar o contexto avido para um stub. A Anthropic mede 150.000 → 2.000 tokens no exemplo canônico deles (redução de 98,7%). A Cloudflare mede 1,17M → ~1.000 tokens no deles (99,9%).
Por que isso importa para code graphs especificamente? Porque um MCP de grafo remoto que expõe dez ferramentas (find_callers, find_callees, class_hierarchy, impact_radius, …) coloca dezenas de milhares de tokens no contexto independentemente de você fazer query ou não. Um MCP local stdio faz o mesmo. A rota de Skill evita isso. A rota adaptativa — uma skill que sabe quando recorrer a um MCP local — pega o melhor dos dois: padrão barato, lookup estrutural quando a pergunta merece.
Esse é o eixo que eu não teria escolhido só pelo seed. Escolher o formato de entrega é em parte uma questão de o que o agente paga só para saber que a ferramenta existe.
Uma matriz de decisão que eu de fato colaria em um design doc
Testei quatro situações reais contra as opções. A matriz abaixo é a versão que eu defenderia num quadro branco.
| Situação | Escolha | Por quê |
|---|---|---|
| Dev solo, < 50k LOC, trabalho exploratório | Skill + boas primitivas de grep/ast-grep | Custo de indexação supera o ganho; o agente já faz isso razoavelmente |
| Dev solo, monorepo grande, perguntas multi-hop frequentes | MCP local stdio com grafo AST | Queries sub-ms, código fonte nunca sai da máquina, binário único |
| Time de N pessoas, semântica compartilhada, governança | MCP remoto com Apache AGE ou Neo4j | O grafo em si é um ativo organizacional — ownership, topologia de deploy, histórico de review todos vivem ali |
| Workload misto, querendo disciplina de custo | Adaptativo: skill primeiro, escala para MCP local em ambiguidade sinalizada | Economiza tokens nos 70% fáceis das perguntas |
| Auditoria de arquitetura única (one-shot) | Despejo de long-context, pular a infra | Construir o grafo custa mais que a resposta |
| Stack pesada em macros ou codegen | Skill + grep, aceitar a limitação | Grafo AST perde código gerado; fingir que não é o movimento perigoso |
O caso de macros merece mais que uma linha. A avaliação Codebase-Memory reporta 0,58 de cobertura em C pesado em macros. O padrão generaliza — em qualquer lugar onde a sintaxe de superfície é reescrita antes da semântica, o grafo só-AST tem desempenho ruim. Macros de C, macros de Lisp, proc-macros de Rust, processadores de anotação na JVM como KSP e KAPT, frameworks de codegen que produzem fontes em build time. O construtor do grafo vê a entrada e o agente pergunta sobre a saída. Se você plugar um grafo AST num codebase desses e confiar nele, você vai entregar uma regressão porque o grafo reportou nenhum chamador da função que o código macro-expandido na verdade invoca trinta vezes.
Um exemplo mínimo trabalhado, em Go
Escolhendo Go de propósito — o seed pendia para Kotlin/Spring e eu queria uma stack com parser determinístico na biblioteca padrão e sem cerimônia de CGO. O exemplo abaixo é a menor coisa que faz o trabalho interessante: percorrer um repo Go, parsear todo arquivo com go/ast, extrair declarações de função e expressões de chamada, armazenar em SQLite, responder "quem chama X". Esse é o motor que um servidor MCP exporia como find_callers.
// callgraph.go — minimal AST-based call-graph indexer for a Go repo.
package main
import (
"database/sql"
"fmt"
"go/ast"
"go/parser"
"go/token"
"io/fs"
"log"
"os"
"path/filepath"
"strings"
_ "modernc.org/sqlite"
)
func main() {
if len(os.Args) != 3 {
log.Fatalf("usage: %s <repo> <fqn>", os.Args[0])
}
root, target := os.Args[1], os.Args[2]
db, err := sql.Open("sqlite", ":memory:")
if err != nil {
log.Fatal(err)
}
defer db.Close()
if _, err := db.Exec(`
CREATE TABLE symbols(fqn TEXT PRIMARY KEY, file TEXT, line INT);
CREATE TABLE edges(caller TEXT, callee TEXT);
CREATE INDEX idx_callee ON edges(callee);`); err != nil {
log.Fatal(err)
}
fset := token.NewFileSet()
filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() || !strings.HasSuffix(path, ".go") {
return nil
}
f, err := parser.ParseFile(fset, path, nil, 0)
if err != nil {
return nil
}
pkg := f.Name.Name
for _, decl := range f.Decls {
fn, ok := decl.(*ast.FuncDecl)
if !ok {
continue
}
caller := pkg + "." + fn.Name.Name
pos := fset.Position(fn.Pos())
db.Exec(`INSERT OR IGNORE INTO symbols VALUES(?,?,?)`, caller, pos.Filename, pos.Line)
ast.Inspect(fn, func(n ast.Node) bool {
if call, ok := n.(*ast.CallExpr); ok {
if name := resolve(call.Fun, pkg); name != "" {
db.Exec(`INSERT INTO edges VALUES(?,?)`, caller, name)
}
}
return true
})
}
return nil
})
rows, _ := db.Query(`SELECT caller FROM edges WHERE callee = ?`, target)
defer rows.Close()
fmt.Printf("callers of %s:\n", target)
for rows.Next() {
var c string
rows.Scan(&c)
fmt.Println(" -", c)
}
}
func resolve(e ast.Expr, pkg string) string {
switch v := e.(type) {
case *ast.Ident:
return pkg + "." + v.Name
case *ast.SelectorExpr:
if x, ok := v.X.(*ast.Ident); ok {
return x.Name + "." + v.Sel.Name
}
}
return ""
}Rode com:
go run callgraph.go ./my-repo billing.Charge
As partes não óbvias. go/ast e go/parser são biblioteca padrão — sem CGO, sem cabeçalhos de gramática. modernc.org/sqlite é um SQLite puro em Go, o que mantém o build em binário único. ast.Inspect percorre o corpo da função e extrai cada *ast.CallExpr. O helper resolve é deliberadamente pequeno: para um *ast.Ident ele qualifica com o pacote atual; para um *ast.SelectorExpr como billing.Charge ele qualifica com o alias do pacote importado. Isso cobre a maioria das chamadas em um codebase real e quebra graciosamente em receivers tipados como interface — que é o ponto cego do AST de novo, dessa vez dentro da linguagem.
O wrapper MCP em torno disso é a parte chata. Uma goroutine lendo JSON-RPC delimitado por newline em stdin, despachando pelo nome do método (find_callers, find_callees), e escrevendo respostas em stdout. A spec do MCP é pequena o suficiente para que o loop stdio inteiro caiba em mais 60 linhas.
O que eu testaria
Escrevi um callgraph_test.go para me convencer de que o indexador funcionava antes de confiar nele em um repo real. A fixture é um único arquivo svc.go escrito em t.TempDir():
package svc
func ChargeOrder(id string) { logPayment(id); chargeStripe(id) }
func logPayment(id string) {}
func chargeStripe(id string) {}Três asserções, nenhuma delas inteligente. Depois da indexação, find_callers("svc.logPayment") retorna exatamente ["svc.ChargeOrder"]. find_callers("svc.chargeStripe") retorna exatamente ["svc.ChargeOrder"]. find_callers("svc.ChargeOrder") retorna a lista vazia. Um quarto teste parseia um arquivo com erro de sintaxe deliberado e confirma que o indexador pula o arquivo em vez de derrubar a execução — falha de um arquivo nunca deveria envenenar o grafo.
O smoke test de performance que eu não pularia: indexar um repo sintético de N arquivos, medir tempo de cold-start e p50 de queries quentes, plotar contra o tamanho do repo. O paper Codebase-Memory afirma sub-milissegundo em codebases reais. O que o paper não consegue me dizer é o que acontece no meu repo na minha máquina, e um benchmark de 30 linhas fecha essa lacuna em cinco minutos.
Onde eu pousaria
Três coisas em que acredito depois de um fim de semana batendo nisso.
Para a maioria das pessoas trabalhando na maioria dos codebases, a resposta é MCP local stdio com um grafo derivado de AST, não remoto, não extraído por LLM. O fonte nunca sai da máquina, o binário entrega em um arquivo, as queries são baratas, e o algoritmo casa com os dados — código tem parser, use-o.
A abordagem de knowledge graph extraído por LLM é a moda e quase nunca vale 20× o custo da abordagem determinística para código especificamente. A taxa de sucesso por arquivo de 0,69 é a parte que eu não consigo superar. Um grafo que silenciosamente perde um terço do seu repo é pior do que nenhum grafo, porque o agente agora responde errado com confiança.
O padrão que eu construiria hoje, se estivesse começando do zero, combina as duas tendências. Uma skill pequena que ensina o agente a usar grep e ast-grep para perguntas baratas, e um MCP local stdio atrás de uma sandbox de execução de código para as multi-hop. Code Execution with MCP da Anthropic e Code Mode da Cloudflare estão convergindo para o mesmo formato, e ele se alinha exatamente com o que code graphs precisam: na maior parte do tempo o agente não deveria pagar o custo de contexto do grafo existir.
Conclusões
- Escolha o formato de entrega antes do algoritmo. O algoritmo do grafo importa menos do que onde o grafo vive.
- Grafos derivados de AST custam cerca de 2× um RAG ingênuo e 1/20× um grafo extraído por LLM, e respondem mais perguntas corretamente do que ambos.
- Um MCP local stdio mantém o fonte na máquina e serve queries em sub-milissegundo com caches quentes.
- Servidores MCP custam contexto faça você query ou não — roteamento adaptativo via skill é o padrão subestimado.
- Grafos só-AST perdem codegen e código macro-expandido. Se você vive em um codebase desses, nomeie a limitação e impeça o agente de confiar no grafo.
Quando recorrer a cada um
Recorra a um MCP local stdio quando você for um desenvolvedor solo em um repo grande fazendo perguntas multi-hop todo dia. Recorra a um MCP remoto quando o grafo em si for um ativo compartilhado que codifica mais do que AST — ownership, topologia de deploy, histórico de review. Recorra a uma skill sozinha em repos pequenos, ou como a camada de caminho-barato em um setup adaptativo. Pule completamente a infraestrutura estrutural para auditorias one-shot e stacks pesadas em macros onde o grafo mentiria.
O agente no meu repo Go de 480 mil linhas, depois que construí o indexador acima, respondeu a mesma pergunta em três chamadas de ferramenta e 14k tokens. A razão não é que o algoritmo ficou melhor. A razão é que movi a estrutura para onde o agente podia atravessar sob demanda em vez de reconstruí-la a cada pergunta.
Curtindo? Talvez goste disso aqui.
Nada parecido — quer tentar outro ângulo?
Posts Relacionados
Transformando Engenharia de Contexto de LLM em um Loop de Avaliação com DSPy
Notas de dois fins de semana cavando o DSPy. Parei de tratar prompts como a fonte da verdade e comecei a tratá-los como saída compilada de uma assinatura tipada, uma métrica e um otimizador. Aqui está o menor programa end-to-end que mantive, como o MIPROv2 de fato busca, e onde a abordagem cai por terra na prática.
Expondo Agentes Spring AI via o Protocolo A2A: O Que a Interoperabilidade Realmente Te Dá
A integração A2A do lado servidor no Spring AI já está estável o suficiente para colocar em produção, mas o protocolo é mais útil em fronteiras organizacionais, não como substituto de RPC interno. Este post percorre o que de fato muda em uma codebase Spring AI, onde ainda existem arestas afiadas, e um framework prático de decisão entre A2A, MCP e REST puro.
A Espinha Dorsal Determinística: Por Que Sistemas de IA em Produção Estão Se Afastando de Agentes Totalmente Autônomos
Agentes totalmente autônomos são difíceis de limitar, difíceis de testar e caros de operar. Uma espinha dorsal determinística com etapas de agente estreitas devolve o controle de fluxo a você enquanto mantém a inteligência onde ela importa. Veja como projetar, testar e migrar nessa direção.