Engineering

The graph is not the point: designing a knowledge graph for retrieval

Most agent knowledge graphs are decoration. The research, the strategy, and the bounded Personalized PageRank algorithm that makes ours earn its place in retrieval.

Every agent memory product ships the same screenshot: glowing nodes, hairline edges, a dark canvas that looks unmistakably like a mind. It demos beautifully. It is also, in most systems, decoration.

That was the uncomfortable thing we learned building Kiomon’s knowledge graph. A graph is not valuable because it looks like a brain. It is valuable only at the exact moments it changes what the agent recalls — and that turns out to be a much narrower, much more interesting problem than “connect all the things”.

This is the strategy behind our graph, the research it is built on, and the bounded traversal algorithm that makes it earn its place.

Single-fact lookup “what did we decide about X?” Plain retrieval 60.9 Graph traversal 49.3 Plain retrieval wins this class Complex reasoning “how did the migration affect auth?” Plain retrieval 42.9 Graph traversal 50.9 Graph traversal wins this class GraphRAG-Bench (2025): a graph helps multi-hop reasoning and contextual synthesis. On single-fact lookup it loses — and inflates the prompt several-fold.

The trap: building a graph for the picture

Most teams build the graph first, then ask retrieval to use it. The graph becomes a second database that nothing reads — a rendering artifact with a beautiful force-directed layout and no job in the pipeline.

We lived this. Our graph was genuinely good-looking, and effectively unused. Retrieval touched it in exactly one way: to down-rank contradictions among candidates that flat search had already chosen. The graph could not nominate a single memory on its own. Worse, because our edges all carried a generic related_to label, a machine’s similarity guess was indistinguishable from a link a human had drawn by hand. When we finally wanted to remove the noisy guesses, we could not — deleting them meant deleting the user’s own links too. Two graphs had quietly accumulated because one missing property made them inseparable.

The fix was not a better graph. It was deciding what the graph was for. We settled on a single sentence as the contract:

The graph is a ranking signal. If an edge cannot change which memories reach the agent, it is not worth storing.

A graph is a specialist, not an upgrade

The instinct is to treat “add a knowledge graph” as strictly better retrieval. The evidence says otherwise, and being honest about it shaped the whole design.

Across public GraphRAG evaluations, graph traversal loses on single-fact lookup while winning on multi-hop reasoning and cross-document synthesis. The cost is hidden in the prompt: naive graph retrieval can inflate context from a few thousand tokens to tens of thousands, and the extra, logically-related-but-redundant nodes reduce context relevance even when recall looks fine.

So we stopped asking “how do we graph the retrieval?” and started asking “which queries is a graph actually for?” The answer — associative, temporal, cross-session synthesis — is exactly where a naive memory layer is weakest. Everything else should stay cheap.

The shape that pays for itself: entities and memories as equal nodes

We deliberately rejected two popular shapes.

First, triplet-only knowledge graphs — stripping everything down to (subject, relation, object). The consistent finding across recent systems is that raw passages must stay first-class alongside graph structure. A triple answers “what is true”; the passage answers “why we believe it”.

Second, community-summary / global GraphRAG, which clusters the corpus, summarises each cluster with an LLM, and searches the summaries. It is the most expensive shape to index, the most expensive per query, and the weakest on the single-fact lookups that dominate day-to-day agent work.

What we adopted is the bipartite entity↔memory graph: memories and entities are co-equal nodes, and an edge means “this memory is evidence for this entity”. A query can then hop memory → entity → memory, which yields a hop that means something real: these two memories are about the same thing. That is far more defensible than “these two documents had similar embeddings”.

Why you must not project naively

The useful artifact — a memory-to-memory graph — is a projection of that bipartite structure, and projection is where graphs explode. If one entity is linked to n memories, the projection creates C(n, 2) pair edges. An entity attached to 30 memories becomes 435 edges. An entity attached to 300 becomes 44,850.

And those edges are usually meaningless. The entities that connect the most memories are precisely the ones that carry the least information: a company name, a product everyone mentions, a language keyword. In graph terms they are hubs; in retrieval terms they are noise that pulls unrelated memories into every result.

Evidence · bipartite Naive projection Bounded projection hub auth ● memory ▭ entity one hub → C(deg,2) noise only specific entities survive Projection is where graphs explode: prune by entity document-frequency before you pair.

Our rule is a document-frequency window. An entity seen once connects nothing, so it is not a node in the projected graph. An entity seen across more than a bounded share of the workspace is treated as a stopword and refused — before pair expansion, not after. Only entities that are specific enough to be informative but common enough to recur become edges. The graph gets sparser as the corpus grows, which is exactly the opposite of the naive projection’s behaviour.

The traversal: bounded Personalized PageRank

With a meaningful graph, how does a query actually use it?

Breadth-first search is the obvious answer and the wrong one: it treats every path as equally important, so one noisy edge can drag a whole neighbourhood into the result. Instead we use Personalized PageRank, the same idea behind Google’s original ranking and, more recently, the strong results of HippoRAG 2 on associative memory.

The intuition is a random surfer. Start the surfer on the memories that hybrid search already believes are relevant — the seeds. At each step the surfer follows an edge, but with a damping factor it occasionally teleports back to the seeds rather than wandering off. Nodes the surfer visits often are relevant to this query, not merely connected. Score mass therefore concentrates near the seed set and fades with distance and with the strength of intervening evidence.

Three properties make this the right fit for an agent memory layer:

  • No LLM at query time. No router, no summariser, no LLM re-ranker. It is arithmetic over a sparse graph.
  • No prompt inflation. Only the top-scoring memories enter the context, and passages stay retrievable — so the graph adds recall without adding noise.
  • Cheap. Sparse power iteration converges in roughly k sweeps over the visited subgraph, so cost scales with the frontier, not the workspace.
Query seeds Bounded Personalized PageRank Fuse into ranking hybrid candidates teleport α back to seeds 1 2 3 4 top-k → prompt Arithmetic, not inference: the graph leg adds recall without a single extra LLM call.

The graph activation then enters the same reciprocal-rank-fusion stage as lexical and vector search. Rank fusion is parameter-free and score-agnostic, which matters because a PageRank mass and a BM25 rank have no comparable scale. The graph becomes a third opinion in a vote that already works, rather than a fragile weighted blend that breaks when the corpus shifts.

# Bounded Personalized PageRank — one retrieval call
function GraphActivation(seeds, damping=0.85, frontierCap, sweeps):
    mass = uniformMass(seeds)            # the surfer starts on known-relevant memories

    for step in 1..sweeps:
        next = teleportMass(seeds) * (1 - damping)
        for node in visited(mass):       # only the reached subgraph, not the workspace
            for (edge, weight) in outEdges(node).take(frontierCap):
                next[edge.target] += damping * mass[node] * weight
        mass = normalize(next)           # scores shrink with distance and weak evidence
        if converged(mass): break

    return rankDescending(mass).drop(seeds)   # new candidates the seeds could not reach

# Graph rank joins lexical and vector ranks in the existing fusion stage
context = reciprocalRankFusion(lexical, vector, graphActivation)

The frontierCap and sweeps bound are deliberate. An unbounded walk is a denial-of-service vector on a dense graph, and beyond two hops the signal is nearly always noise. Bounded traversal keeps the leg predictable on every request.

Every edge needs a provenance and an expiry

Two properties separate a graph you can trust from a graph you merely have.

Provenance: who believes this?

An edge is a claim, and the source of the claim matters as much as the claim. A machine’s similarity guess, a human’s hand-drawn link, and an inference the engine derived from a contradiction are three different kinds of knowledge that happen to share a line on a canvas.

We now make every writer state its origin, and traversal filters by it. The machine’s guess does not get to outrank a person’s assertion. This was not cosmetic: it is the property whose absence let two incompatible graphs accumulate, and it turned “delete the guesses” from an impossible migration into a one-line filter. An unlabelled edge is worse than no edge, because you can neither trust it nor safely remove it.

Bi-temporal validity: invalidation, not deletion

Facts change. A memory that said “we deploy on Fridays” is not wrong after the policy changes — it was true, and then it stopped being true. The strong pattern from temporal knowledge graphs (Zep/Graphiti) is invalidation, not deletion: a superseding fact sets a validity end on the old edge instead of erasing it.

Traversal filters to currently-valid edges only. Without that filter, multi-hop retrieval cheerfully propagates stale facts and confidently answers with last quarter’s truth. With it, the system can still answer the genuinely useful question: what did we believe on a given date?

Memory over time: consolidation, decay, contradiction

A static graph is a snapshot. The interesting behaviour is what happens to the edges as the corpus lives.

The design borrows three ideas from how human memory works:

  • Hebbian consolidation. Memories that are repeatedly activated together are strong evidence that they belong together. Repeated episodes about the same debugging pattern eventually consolidate into a single reusable procedure. The graph does not just store links; it forms them from co-activation.
  • Decay and reinforcement. Unused edges and memories lose strength on a schedule; recalled ones gain it. Pinned and reference knowledge never decays. This keeps recall sharp instead of letting a decade of trivia accumulate equal weight.
  • Contradiction handling. When a new claim conflicts with an existing one, authority decides. A high-confidence, human-approved fact outranks a low-confidence inference. The loser is quarantined or superseded — never silently overwritten, because a memory system that overwrites the user’s own stated beliefs is one you cannot audit.

You cannot tune a graph you cannot score

Here is the humbling part. For a long time we were optimising the wrong variable: connectedness. More edges felt like more intelligence.

Then we instrumented retrieval properly — recording which memories were actually injected, which were cited or rated helpful, and which queries they came from — and A/B’d the graph leg in shadow mode before letting it change what users saw. The measurement produced a result no amount of graph beauty could have argued with: on an early, thin corpus, most documents genuinely shared no entities. The graph was sparse because the corpus was sparse, not because the algorithm was weak. Adding linking machinery would have manufactured noise, not signal.

That reframed the goal. The right target is not a dense graph. It is a graph where precision — the share of edges a human would agree with — is high enough that every hop is defensible. A graph that connects everything explains nothing. Sparse and correct beats dense and speculative, every time.

What we would tell anyone building one

If you take one thing from this, take the ordering:

  1. Decide what the graph is for before you build it. If it is not a ranking signal, it is a rendering artifact — and you should know that on purpose.
  2. Model entities and memories as equal nodes. Hops through a shared entity are meaningful; hops through embedding similarity are not.
  3. Prune before you project. Bound entity degree by document frequency, or one hub will generate a meaningless clique.
  4. Traverse with Personalized PageRank, not BFS, and bound the frontier. Graph structure should re-weight what you already found, not flood the context.
  5. Put provenance on every edge and a validity interval on every belief. These two columns are the difference between a graph you can edit safely and one you are afraid to touch.
  6. Instrument before you tune. The bottleneck is usually the corpus, and only measurement will tell you that.

A knowledge graph is not a feature you can screenshot into existence. It is a retrieval decision, made one edge at a time — and it only earns its keep when a hop changes what your agent remembers.

All field notes

Kiomon Engineering

We build persistent context, knowledge, and memory infrastructure for AI agents over native MCP.

Give your AI agent a brain that remembers

Stream persistent context to Claude Code, Cursor, and any MCP agent with zero setup friction.

Get started free View docs