field notes · engineering

I over-built a code graph for my AI agents. Then I learned where one belongs.

Building, tearing down, and rebuilding a code graph for AI agents, and learning exactly which problems a graph should own.

By Samrat Biswas (Sam), Chief Research Analyst
23 July 2026 · 9 minute read

the change inferred dead

I did the responsible thing. When I set out to give my AI coding agents a real understanding of a codebase, I stood up the proper stack: Neo4j for the graph, Voyage for best-in-class code embeddings, the whole cathedral. If an agent was going to reason about my code, it would do it over a rich, queryable model of that code, not by fumbling around with text search like it was 2011.

A few weeks later I tore most of it down, and kept the part that earned its place. The agents got better, my bill got smaller, and the graph I still run does a completely different job than the one I started with.

Because that is the real lesson, and it is not "graphs are overkill." It is that I had pointed an excellent instrument at the wrong problem. Here is where a graph actually belongs behind an AI agent, with the receipts.

The instinct is good. The target is wrong.

The reason everyone reaches for a graph is sound. Agents burn tokens re-reading files to answer questions they should be able to look up, and a graph turns "what calls this function" from a dozen file reads into a single hop. If your motivation is cutting context and token costs, you are not wrong that a graph can do it. Reported reductions on structural queries run well into the double and triple digits.

Here is the catch I learned the expensive way: that is an efficiency win, not a capability win. The studies that show a graph slashing token use do not show the agent solving more problems. The cleanest example in the literature is a graph-guided system that crushed embeddings at finding the right code, and still moved the end-to-end fix rate by about two and a half points. Cheaper, yes. Smarter, no. Keep those two apart and half the confusion in this space disappears.

"Code intelligence" is three jobs, not one

The unlock, for me, was realizing I had been treating one word as one problem. "Understanding the codebase" is really three different jobs, and a graph is right for only some of them.

Find & read

Where is this function, what is in this file.

volatile · every commit

Trace structure

What breaks, what is dead, what calls what, three hops deep.

non-local · needs traversal

Grasp the shape

Module boundaries, layering, what is coupled to what.

abstract · slow to change
Figure 1. One word, three jobs. Most of the excitement, and nearly all of the disappointment, comes from not keeping them apart. A graph is right for the middle one.

Most of the excitement, and nearly all of the disappointment, comes from not keeping these apart. Once you do, the right architecture stops being a debate and starts being a lookup table.

The plot twist: the field already ran this experiment

Here is the thing that should give any would-be graph builder pause. The teams with the most to gain from a rich code index looked at the first job, finding code, and walked away from precomputed indexes entirely.

~94%
of what a vector database gives you, reached by live keyword search alone, with nothing to keep fresh (Amazon Science).
~2.5 pts
the entire end-to-end fix-rate lift a graph added, after it crushed embeddings at finding the right code.
grep
the tool the frontier walked back to for everyday context, none of us put it on our LinkedIn.
Figure 2. The receipts. Cheaper is not the same as smarter, and the humble tool already won the most common job.

In 2025, Anthropic pulled the vector index out of Claude Code and went back to live search. Their creator's stated reason was almost rude in its simplicity: agentic search just worked better, and it dodged the staleness, privacy, and reliability problems that come with an index. Amazon then put a number on it, finding that live keyword search reaches roughly 94 percent of what a vector database gives you, with nothing to keep fresh. Sourcegraph, who more or less invented this category, dropped embeddings and reframed the whole question as relevance versus coverage.

grep, which none of us put on our LinkedIn, keeps quietly beating the cathedral.

So for the most common job, the humble tool won. If your plan is to build a graph so the agent can find code, you are re-solving a problem the frontier already closed, and modern agents grep well.

So I aimed the graph at the problems only a graph can solve

I am not here to bury graphs. I still run one. I just stopped asking it to fetch context and started asking it the questions text search physically cannot answer, and there a graph is not merely useful, it is the only honest tool in the room.

reverse traversal

What breaks if I change this?

A bounded reverse walk over the call graph, not a prompt. A deterministic set, every node tagged by distance, the same answer every time.

reachability

Is this actually dead?

Unreachable, unexported, untested, unrouted. A whole-graph question. A model that is "pretty sure" is not something I delete code on.

cross-repo join

Who really calls this API?

Matching a provider in one service to callers in another is a join over canonical identifiers, a view per-repo text search does not have.

taint

Where does untrusted input end up?

Taint and reachability are a graph's home turf, which is exactly why the security world has always drawn them as graphs.

Figure 3. The questions I hand to a graph instead of a model. Each is a traversal, not a guess.

The method matters as much as the list, and it is where most code graphs quietly betray you. My call edges come from a language server or a SCIP index, not tree-sitter alone, because tree-sitter cannot resolve dynamic dispatch and will happily hand you a confident, incomplete graph. The edges static analysis genuinely cannot see, I recover from co-change history: files that always move together are coupled whether or not a call edge exists between them, and I label those inferred rather than dressing them up as fact. Every edge carries its provenance and its confidence, so a blast radius can say "complete for what I can prove statically, partial here, and here is why." That answer is worth ten that shrug and call themselves exhaustive.

the change hop 1 hop 2 observed · proved statically inferred · from co-change
Figure 4. A blast radius is a bounded reverse traversal, every edge banded by how it is known. That is worth ten answers that shrug and call themselves exhaustive.

Then there is the subtler prize, the one AI most reliably misses: the shape of the system itself. Agents are good at the local and the literal and clumsy at the architectural, because you cannot grep for a design seam. The elegant part is that this layer is stable, it drifts monthly rather than per commit, which quietly dissolves the usual objection that a graph goes stale. There is even a beautiful thirty-year-old idea for keeping it honest, the software reflexion model: declare the architecture you meant to build, compare it against what the code actually does, and treat the divergence as the finding. The gap between the map in your head and the territory on disk is usually the most interesting thing in the building, and a graph is how you measure it.

Layer 1

The model guesses

stochastic

Ask an agent what breaks and you get a fluent, confident answer, then a different one on the next run. Good enough to read code, not to delete it.

Layer 2

The graph answers

deterministic

A bounded reverse traversal returns the same set every time, each node tagged by distance, each edge banded by how it is known.

Layer 3

You aim it

interactive

The instrument is a commodity. The judgment is not. You choose which questions are graph-shaped, and which the agent should just grep, live.

stochastic in, deterministic out same question, same answer

The traps nobody puts on the brochure

If you do build, know where the bodies are buried.

A wrong answer wearing the costume of a precise one is more dangerous than an honestly fuzzy grep.

You probably already own the lighter answer

Here is the reframe that saved me. Neo4j and a vector store are not competitors; they do different jobs. A vector index does similarity, "find code that looks related to this." A graph does traversal, "what transitively calls this." So the real question is never "Neo4j or pgvector." It is "do I need a dedicated graph database at all, or can the Postgres I already run hold the graph too?"

what I built first
Neo4j
Voyage embeddings
The heavy and the excellent. A graph server to host, secure, back up, and keep in sync.
downgraded to
what I run now
Postgres + pgvector
a small local model
One box: edges as rows, a recursive query for traversal, embeddings alongside. One backup, nothing new to feed.
Figure 5. I traded a little embedding quality for privacy and a smaller bill, and a graph server for a table and a query I already knew how to write.

For repo-scale work, Postgres holds it fine. Store edges as plain rows and walk them with a recursive query. Blast radius and reachability are bounded-depth traversals that SQL does deterministically and fast into the low millions of edges. Put your embeddings in the same database and you have both layers in one box: similarity for recall, the graph for precision, one backup, nothing new to feed.

This is exactly the walk I took. I started on Neo4j and Voyage, the heavy and the excellent. There was no dramatic teardown: over a couple of months I simply kept not reaching for the graph, until Postgres with pgvector and a small local embedding model had quietly become the whole stack. For what I am doing, I have not needed to go back. Neo4j earns its place eventually, at monorepo scale, or for gnarly variable-depth path queries, or when graph algorithms become the point rather than a side effect. Until one of those actually bites, a dedicated graph engine is a lot of ceremony for a question Postgres was already answering.

The senior move is knowing where to point it

If there is one thing to take from my detour, it is that the impressive part was never standing up the graph. It was aiming it.

For everyday context I let the agent search, live, the way the frontier settled on, because that job is a commodity and a fresh grep beats a stale index every time. For the questions that are genuinely graph-shaped, what breaks, what is dead, what reaches what, how far the real architecture has drifted from the one I intended, I reach for a graph without a second thought, because nothing else answers them honestly. Most of the craft is simply refusing to make one tool do both jobs, and building the smallest graph that answers a real question before reaching for a heavier one.

So no, I did not talk myself out of graphs. I stopped using one as a mattress to pile all my context on, and started using it as the scalpel it is, for the few incisions text search cannot make. I built the cathedral first and learned, the expensive way, that my agents wanted a good map and a flashlight for the daily walk, and one very sharp, very specific instrument for the cuts that matter.

Owning the instrument was never the hard part. Knowing exactly where to point it is the whole job.

See RepoGraph, the instrument this essay describes

Further reading