Back to blog
Agent Memory Graph vs Vector Store for Multi-Hop Reasoning

This blog is written by AI for SEO

Agent Memory Graph vs Vector Store for Multi-Hop Reasoning

HelixDB11 min read

Ask your agent "which decisions did the people who reviewed this incident make later?" and watch top-k vector recall hand back five paragraphs about the incident. Every one is relevant. None of them answers the question. The reviewers' later decisions live in documents that never mention the incident, so no embedding of your question will ever land near them.

That is the core of the agent memory graph vs vector store question for multi-hop reasoning. Vector similarity finds text that looks like the query. A multi-hop question asks for text that is connected to the query through other entities. Those are different retrieval problems.

This guide is a runnable how-to. You will write down the hop chain, reproduce the failure with plain top-k recall, model the memory as a graph, answer the question with a traversal in TypeScript, then scope a vector search to the traversed neighbourhood. The steps work with any graph plus vector setup. HelixDB is the one-engine version, using the @helix-db/helix-db SDK.

Step 1: Write down the multi-hop question and its hop chain

Before touching a database, write the question as a chain of hops. It's the cheapest debugging step in the whole guide, and most teams skip it.

The question: "Which decisions did the people who reviewed this incident make later?"

The chain:

  1. Incident (given, e.g. INC-212)

  2. Reviewers of that incident: Person -[reviewed]-> Incident

  3. Decisions those people made: Person -[made]-> Decision

  4. Filter: only decisions made after the review

That is three hops with a time filter. Look at what the answer is made of: Person and Decision nodes, joined by explicit edges. Nowhere in the question is there a phrase that would appear in the text of a decision record. "Decision to migrate billing to async jobs" says nothing about INC-212.

Apply this test to any agent question. If you can draw the chain and the intermediate nodes are entities (people, tickets, services, customers), you have a multi-hop question. If the answer sits in a single passage that paraphrases the query, you have a lookup question, and a vector index is fine. Most agent memory traffic is a mix, which is why Step 6 is about routing between the two.

One practical habit: keep a small file of five to ten of these hop chains from real agent failures. They become your regression set for everything below. The GraphRAG-style framing of vector, graph and episodic memory as complementary layers is a useful mental model here, but the hop chain is what makes it testable.

Step 2: Reproduce the failure with top-k vector recall

Reproduce the failure before you fix it. Otherwise you're just adding infrastructure.

Take your memory store (incident write-ups, review comments, decision records) as plain chunks with embeddings. Run the question as a top-k query:

const question =
  "Which decisions did the people who reviewed INC-212 make later?";
const hits = await vectorIndex.search(await embed(question), { k: 5 });
for (const h of hits) console.log(h.score.toFixed(3), h.text.slice(0, 90));

vectorIndex and embed are stand-ins for whatever you run today: pgvector, Qdrant, Pinecone, an in-process index. The result is the same everywhere.

You will see something like this: the incident summary, two review comments quoting the incident ID, a postmortem action list, and a Slack export mentioning INC-212. All five chunks are about the incident. Zero are decisions made by the reviewers afterwards.

The cause is mechanical, not a tuning problem. A decision record such as "We decided to cap retry fan-out at 3 per worker" is semantically distant from the query, because the query's meaning is dominated by the incident ID and the word "reviewed". Raising k to 50 doesn't fix it. You pull in more incident-adjacent text, and the decisions only appear by accident, buried under noise your LLM then has to sort through.

Write down the number: for each question in your regression file, how many of the expected answer nodes appear in the top 10? For this question it's typically zero. That is your baseline. The hybrid retrieval material in the Hindsight comparison of knowledge graphs and vector search for agent memory describes the same pattern: similarity answers "what looks like this", not "what is connected to this".

Step 3: Model memory as a graph (people, incidents, decisions, reviewed and made edges)

The fix starts with the data model, and it's smaller than you think. You need three node labels and two edge labels.

  • Nodes: Person (name, team), Incident (title, summary), Decision (text, decided_at)

  • Edges: reviewed from Person to Incident, made from Person to Decision

Put decided_at on the Decision node as an integer timestamp. HelixDB doesn't provide built-in temporal modelling, so you model time yourself with an ordinary property. It does give you efficient time-range indexing, which is exactly the access pattern in hop 4. Put the embedding on the Decision node too: in HelixDB, vectors are a property on nodes and edges, so there's no separate vector table to keep in sync.

Here is the write side, run against an embedded on-disk database. Embedded mode needs no CLI. If you'd rather run a server, the local path is helix init local then helix start dev (needs Docker or Podman, serves on port 6969), and you swap the client for Client.server(url).

import { Client, g, writeBatch } from "@helix-db/helix-db";

const client = await Client.embedded({
  kind: "disk",
  root: "./memory-db",
  database: "agent-memory",
});

The exact node-creation builder calls, index definitions and vector property types are in the HelixDB docs. Build the writes with writeBatch() and g(), then call .send(). Nothing runs until you do. Closing the writer persists the data, so there's no flush step.

Two modelling decisions matter more than the syntax. First, model reviewed and made as separate edge labels instead of one generic related_to. Traversal cost and answer precision both depend on edge labels being specific. Second, make entities canonical. One Person node per human, not one per document mention. If you need a walkthrough of extracting those entities from raw text, see how to build a knowledge graph from meeting notes.

Step 4: Answer the question with a graph traversal (TypeScript with @helix-db/helix-db)

Now the same question as a traversal. The logic is exactly your Step 1 chain: start at the incident, walk reviewed edges backwards to people, walk made edges forward to decisions, filter by time.

A sketch using the read-batch pattern from @helix-db/helix-db 3.2.0:

import { Client, g, readBatch } from "@helix-db/helix-db";

// client opened as in Step 3 (or Client.embeddedReader for a second process)
const query = readBatch()
  // 1. start: the incident node (look it up by an indexed property)
  // 2. traverse incoming `reviewed` edges to Person nodes
  // 3. traverse outgoing `made` edges to Decision nodes
  // 4. filter decided_at > reviewedAt, order by decided_at, limit 20
  // build each step with g(), return Decision text and decided_at
  ;

const result = await query.send(client);

I left the traversal steps as comments on purpose. The builder method names for in/out traversal, predicates and ordering come from the published types and docs, and you should copy them from there rather than from a blog post. Take away the structure: one query, evaluated by following edges, with three hops expressed as three steps. Property filters, comparison predicates, ordering, limit and skip are all part of the same DSL. The SDK builds a JSON request and sends it to POST /v2/query using HelixDB's query DSL, while Helix Cloud also accepts Cypher at POST /v2/cypher.

The result is a deterministic set: every decision made by anyone who reviewed INC-212, after the review. In our example that's four decisions, none of which mention the incident. Top-k recall returned zero of them.

That's the real advantage of a memory graph over a vector store for multi-hop reasoning: the connection is stored, not inferred. It also fits your regression file. The expected nodes either come back or they don't, and you can assert on it. For a deeper take on when this representation beats embeddings, read when to use a knowledge graph instead of embeddings.

Step 5: Scope vector search to the traversed neighbourhood (pre-filtering)

Pure traversal gives you precision. Often you then want relevance within that set: "of the decisions those reviewers made, which ones are about retry behaviour?" That's where vector search comes back in, scoped to the neighbourhood rather than the whole index.

The naive way is post-filtering: run a global top-k, then discard everything outside the reviewers' decisions. It fails for the same reason Step 2 failed. If the 50 nearest vectors in your whole corpus don't include the reviewers' decisions, filtering leaves you with nothing. You threw away the answer before applying the constraint.

Pre-filtering inverts the order. Traverse first to get the candidate set, then rank only those candidates by similarity to the query embedding. HelixDB supports this directly: vector search can be scoped to things related to each other, so similarity runs over a graph-filtered candidate set instead of the whole index. Because vectors are a property on the nodes, the traversal and the ranking live in one engine and one query. There's no round trip to a second database to fetch candidate IDs.

Two documented limits to design around. An unrestricted vector search caps at 800 effective results. A traversal-scoped search applies that ceiling after candidate intersection, and rejects the query rather than silently clamping. A candidate stream over 1,000,000 unique entities is a query error. For the reviewer case, candidate sets are tiny, so neither matters. For a hub node (a person who reviewed thousands of incidents) they will, and you should add a time-range filter in the traversal before the vector step.

The companion walkthrough on pre-filtering vector search on graph edges covers the edge-level variant, and the HelixDB repo at github.com/HelixDB/helix-db has the current index options (cosine, euclidean, manhattan, global or tenant-partitioned).

Step 6: Decide per question whether to use graph, vector, or both

Choosing between a memory graph and a vector store isn't a platform decision. It's a per-question routing decision, and you can make it with three checks.

Use vector only when the answer sits in one passage that paraphrases the query. "What was our retry policy for the payments worker?" Single hop, fuzzy wording, no entity chain. A top-k index is the right tool and a graph adds nothing.

Use graph only when the question names entities and relations explicitly and the answer is a set, not a ranking. "Who reviewed INC-212?" "Which services depend on the auth gateway?" Counting, listing and path questions belong here, and the answer should be exact.

Use both when the question has a connected scope and a fuzzy criterion. "Of the decisions made by INC-212's reviewers, which ones concern retries?" Traverse to scope, then vector-rank inside the scope. This is the common shape for agent memory use cases: company brains, support histories, code and incident knowledge, shared memory across multiple agents (see shared memory for multi-agent systems).

A cheap router works well. Have the LLM classify the question into those three buckets and extract any named entities, then dispatch. If no entity in the question resolves to a node in the graph, fall back to vector search.

This is also the honest case for one engine. You can build all three paths with a separate graph database and vector database, and the logic above holds either way. The cost is the sync layer between them and the extra network hop on every scoped query. Keeping both in HelixDB removes those two failure points. The routing logic stays the same.

What to do next and troubleshooting

Run your regression file from Step 1 against both retrieval paths and record how many expected nodes each returns. That table is your argument for adding a graph, and it persuades better than any architecture diagram.

When the traversal returns less than you expected, check these in order:

  • Missing edges. The most common cause. If an LLM extracted entities from documents, some reviewed edges never got written. Count edges per incident and spot-check against source. A traversal can only follow what exists.

  • Fan-out. A person who authored 4,000 decisions turns hop 3 into a firehose. Add a time-range filter or a limit before the vector step, and remember the candidate-stream limits from Step 5.

  • Stale or duplicate entities. "J. Park" and "Jin Park" as two nodes silently splits a reviewer's decisions across two traversals. Resolve identities at write time and keep one canonical Person per human.

  • Time ranges modelled manually. HelixDB has no built-in temporal layer. If you need "as of" semantics, store valid_from and valid_to as properties and filter on them. The time-range indexing keeps this cheap, but the modelling is yours.

Once the hop chain works, read the agent memory architectures overview for how episodic and semantic layers fit around this, then the HelixDB introduction docs for the full query reference.

Conclusion

If your agent already runs top-k recall and misses connected context on two- and three-hop questions, do this today: write down five failing questions as hop chains, model the entities and edges they imply, and rerun them as a traversal followed by a scoped vector search. Expect the vector-only baseline to return nothing for the connected ones. You can do all of it in one process with the embedded client, no server and no second database. Install @helix-db/helix-db, open an on-disk database, and port your first incident-and-reviewer example in an afternoon. If it answers a question your vector store never could, star HelixDB on GitHub and tell us what you are building.

Build with HelixDB

Give your coding agent the setup prompt, or sign up and deploy a database.

Sign up