Code Embeddings vs Code Graph: What AI Agents Actually Need
Ask five developers whether AI coding agents should use embeddings or grep and you'll get five confident, contradictory answers, and almost none of them will mention the graph. Embeddings find code that looks like your query. Grep finds code that contains your exact string. Neither one can tell you who calls a function or what breaks if you change it, and that gap is exactly where agents keep failing on real codebases.
Grep: fast, precise, blind to meaning
Grep is the oldest tool in this fight and it's underrated for a reason: it's exact. Search for resetPassword and you get every line where that string appears, instantly, with zero infrastructure and zero staleness, because it reads the files as they exist right now. The limit is baked into what it does: grep matches characters, not meaning. It can't find the function that does the same job under a different name, and it can't distinguish a real call site from a comment that mentions the function in passing. A text match only confirms a line contains a string; whether that line is a real invocation is a separate question grep never answers.
Embeddings: similarity, not connectivity
Embeddings solve a narrower version of the meaning problem. A vector index over a codebase can find resetPassword when you search for "how do I let a user recover their account," because the two are semantically close even with no shared vocabulary. That's a real capability grep doesn't have. It's also where the argument for embeddings in coding agents tends to stop, and where it gets overstated.
Jason Liu's widely-read post on this made the opposite case: that RAG, in practice embeddings, was a mistake for coding agents, and that agentic grep paired with a capable model beats a filtered, chunked embedding pipeline for finding code (Why I Stopped Using RAG for Coding Agents). The core complaint holds up: embeddings chunk code into fragments, rank them by resemblance, and hand the model disconnected pieces that lose the surrounding structure a senior engineer would read as a whole. For "find the function that handles X," a capable model exploring the repo directly, reading imports, following directory structure, often beats a filtered vector search that hands back the top five most-similar snippets and stops there.
What that framing leaves out is the graph. The comparison is grep versus embeddings, both of which answer "what code is relevant to this query" from two different angles, exact match or similarity. Neither answers "what is this code connected to." That's a different question, and it needs a different structure to answer.
The questions embeddings cannot answer
"Who calls UserService.delete" is a connectivity question, and cosine similarity was never built to answer it. The callers of a function don't necessarily read as similar to the function itself; a controller invoking delete might share almost no vocabulary with the delete method's own implementation. Rank by similarity and you surface functions that talk about deleting users conceptually, which may or may not overlap with the actual call sites wired into the running system. "What breaks if I change this function's signature" needs the transitive closure of a call graph: the callers, their callers, and so on until the walk terminates. A ranked list of nearby vectors stops at the first hop, if it finds the right hop at all. We walk through exactly what that answer looks like, and why grep and embeddings both miss it, in blast radius analysis. Connectivity questions need edges, and content stores, whether grep's file index or an embedding's vector store, don't keep edges around.
| Question | Grep | Embeddings | Graph |
|---|---|---|---|
| Find an exact string or config key | Yes, instantly | Weak, ranks by meaning not exact match | Yes, via symbol or text lookup |
| Find code by meaning, unfamiliar vocabulary | No | Yes | Yes, via semantic search |
| Who calls this function | Partial, string matches only | No | Yes, resolved edges |
| What breaks if this changes | No | No | Yes, transitive closure |
| Freshness after a commit | Always current, reads live files | Stale until reindex | Current if edges recompute incrementally |
Embedding staleness vs a live graph
There's a second, more operational gap. An embedding index is a snapshot: it's built by encoding files and stored until the next reindex job runs. Most pipelines reindex on a schedule or on push, which means there's always some window where the index describes code that no longer exists. A graph carries the same staleness risk in principle, but the fix is more tractable: edges are cheap to recompute incrementally on a commit, because they come from parsing what changed rather than re-embedding everything that might be affected. A repository with branch-aware indexing compounds this: an agent working a feature branch needs the graph and the embeddings both current to that branch, not the default branch three days behind it. We cover why this matters at the retrieval-strategy level in why AI coding agents fail on large codebases: stale context is one of the concrete failure modes agents hit at scale, alongside the context ceiling and the missing call graph.
What the data says
The gap between similarity and connectivity shows up in benchmark numbers. A Google Research study on enterprise code migration ran standard vector RAG against a hierarchical, graph-aware retrieval approach on dependency-heavy migration tasks and found the API hallucination rate dropped from 56.4% to 16.2%, and dependency-resolution quality rose from 34.8% to 65.9%, once the graph was in the retrieval loop (Beyond Vector Similarity: Hierarchical Context-Aware Graph RAG vs Standard RAG in Enterprise Code Migration). Standard RAG in that study is doing what embeddings do best, ranking by resemblance, and it still hallucinates APIs at more than three times the rate of the graph-aware approach on tasks that are fundamentally about dependencies. That's the connectivity gap showing up as a number.
The honest conclusion: they compose
None of this makes grep or embeddings obsolete. Grep is still the fastest way to find an exact string, a config key, an error message. Embeddings are still the right tool for "find code like this" when the vocabulary doesn't match and a human, or agent, can't guess the exact name. What neither one does, individually or combined, is answer a connectivity question, because neither stores the edges between symbols. That's the graph's job: it adds the connectivity layer that grep and embeddings each lack on their own.
Symvanta is built on that premise directly: it runs a code graph, nodes for symbols, edges for calls, imports, implements, and instantiates, semantic search over Qdrant embeddings, and text search side by side, and routes an agent's question to whichever one actually answers it. "Where is resetPassword defined" is a lookup. "What handles account recovery" is semantic. "What breaks if I change this" is graph. Serving all three over one MCP endpoint means an agent isn't stuck picking one strategy at the start of a session and living with its blind spots for the rest of it. If you're weighing this against a context-engine tool like Sourcegraph Cody, the question worth asking is whether the tool answers connectivity questions at all, before you get to which retrieval strategy it defaults to.
See it answer a real one on your own repository: book a 15-minute demo.