Skip to content
All case studies
Lead AI Engineer#Neo4j#Python#LangGraph#OpenSearch#FastAPI

A Graph-RAG Assistant for Multi-Hop Questions

When vector search kept failing on 'how are these connected' questions, I built a hybrid retriever over a knowledge graph and an embedding index — and let the query decide which one to trust.

April 28, 20263 min read
Multi-hop accuracy
+31pts
p95 latency
1.3s
Grounded citations
100%

Vector RAG is superb at "find me the passage about X" and quietly terrible at "how does X relate to Y through Z." Our users kept asking the second kind of question — tracing ownership chains, dependency paths, regulatory lineage — and a flat embedding index simply cannot represent a relationship it never stored. So I built a Graph-RAG assistant that keeps both a knowledge graph and a vector index, and routes each question to the retrieval it actually needs.

Problem

The domain was a web of entities: companies, subsidiaries, standards, and the obligations linking them. Analysts asked multi-hop questions:

  • "Which suppliers of Acme are subject to the same disclosure rule as Acme?"
  • "Trace the certification chain from this component to the finished product."

A vector store answers these by hoping the right multi-hop path happens to be described verbatim in one chunk. It rarely is. Recall on our multi-hop eval set sat at 46%, and even correct answers came without a traceable path a human could audit.

Approach

The insight is that these questions have two halves. There is a semantic half ("what is a disclosure rule") that embeddings handle well, and a structural half ("which nodes are two hops from Acme along SUPPLIES and SUBJECT_TO") that is a graph traversal, not a similarity search.

So I built both retrievers and a router in front of them:

  • A knowledge graph in Neo4j, populated by an extraction pipeline that pulls entities and typed relations from documents.
  • A vector index in OpenSearch over the same source chunks.
  • A router — a small classifier that inspects the query and decides whether to lead with graph traversal, vector search, or a blend.

I orchestrated the whole flow as a LangGraph state machine so each stage was a node with a typed contract and a replayable trace.

Architecture

from langgraph.graph import StateGraph, END
 
def build_assistant() -> StateGraph:
    g = StateGraph(RagState)
    g.add_node("route", classify_query)          # graph | vector | hybrid
    g.add_node("graph", traverse_knowledge_graph) # Cypher multi-hop
    g.add_node("vector", semantic_search)          # OpenSearch kNN
    g.add_node("fuse", merge_evidence)             # dedup + rank
    g.add_node("answer", generate_grounded)        # cite nodes + chunks
    g.set_entry_point("route")
    g.add_conditional_edges("route", pick_retriever, {
        "graph": "graph",
        "vector": "vector",
        "hybrid": "graph",
    })
    g.add_edge("graph", "fuse")
    g.add_edge("vector", "fuse")
    g.add_edge("fuse", "answer")
    g.add_edge("answer", END)
    return g

For structural questions the graph node compiles the intent into a bounded Cypher traversal:

MATCH path = (a:Company {name: $entity})
             -[:SUPPLIES|SUBJECT_TO*1..3]-(related)
RETURN related, relationships(path) AS edges
LIMIT 50

The traversal returns not just the answer nodes but the edges — the actual path — which becomes the citation. Every claim in the final answer points to a concrete subgraph a human can inspect.

To rank fused evidence when graph and vector both contribute, I score each candidate by a convex combination of its graph proximity and its semantic similarity:

score(c)=λ11+dg(c)+(1λ)cos(q,c)\text{score}(c) = \lambda \cdot \frac{1}{1 + d_g(c)} + (1 - \lambda)\, \cos(q, c)

where dg(c)d_g(c) is the hop distance from the seed entity and λ\lambda is tuned per query type by the router — high for structural questions, low for definitional ones.

Outcomes

Evaluated on a 300-question benchmark split into single-hop and multi-hop subsets:

MetricVector-onlyGraph-RAG
Single-hop accuracy0.880.89
Multi-hop accuracy0.460.77
Grounded citationspartial100%
p95 latency0.9s1.3s

Single-hop performance held steady — the router correctly sends those to vector search — while multi-hop accuracy jumped 31 points. The latency cost of a graph traversal was real but bounded, and worth it for questions that were previously unanswerable.

The durable lesson: retrieval architecture should mirror the shape of the question. A knowledge graph is not a nicer vector store; it is a different representation that makes relationships first-class. The moment I stopped forcing every question through one index, both kinds of questions got the retrieval they deserved.