Documentation Index
Fetch the complete documentation index at: https://docs.agentium.in/llms.txt
Use this file to discover all available pages before exploring further.
GraphRAG
When to use a graph instead of vectors
Vector RAG is great for semantic similarity: “find documents about X”. It struggles with relationship reasoning:
- “Which projects does Alice’s direct reports work on?”
- “How many bugs has team B closed in the last 30 days?”
- “Trace this transaction’s path through the ledger.”
Those questions need a graph database. GraphRAG generates a Cypher query from the natural-language question, runs it against Neo4j or Memgraph, and returns the rows.
Combine the two via HybridRetriever for the best coverage.
Architecture
┌──────────────────────────────┐
user question ──────────▶│ GraphRAGRetriever │
│ 1. get live schema (labels, │
│ relationship types, │
│ property keys) │
│ 2. LLM ➜ Cypher │
│ 3. ensure LIMIT │
│ 4. run Cypher │
└──────────┬───────────────────┘
│
▼
┌──────────────────────────────┐
│ CypherStore │
│ neo4j-driver or compatible │
└──────────────────────────────┘
CypherStore
Low-level interface for any graph DB that speaks the Cypher / Bolt protocol.
interface CypherStore {
readonly providerId: string;
connect(): Promise<void>;
runCypher(cypher: string, params?: Record<string, unknown>): Promise<CypherRecord[]>;
getSchema(): Promise<CypherSchema>;
close(): Promise<void>;
}
interface CypherRecord {
values: Record<string, unknown>; // column name -> value
}
interface CypherSchema {
nodeLabels: string[];
relationshipTypes: string[];
propertyKeys?: string[];
}
Neo4jCypherStore
import { Neo4jCypherStore } from "@agentium/core";
const store = new Neo4jCypherStore({
uri: process.env.NEO4J_URI ?? "bolt://localhost:7687",
username: process.env.NEO4J_USER ?? "neo4j",
password: process.env.NEO4J_PASSWORD ?? "neo4j",
database: "neo4j", // optional, default is "neo4j"
});
await store.connect();
Requires: npm install neo4j-driver (already an optional peer dep of @agentium/core).
Defaults pull from NEO4J_URI, NEO4J_USER, NEO4J_PASSWORD environment variables.
MemgraphCypherStore
Memgraph speaks the same Bolt protocol as Neo4j, so the adapter just sub-classes:
import { MemgraphCypherStore } from "@agentium/core";
const store = new MemgraphCypherStore({
uri: process.env.MEMGRAPH_URI ?? "bolt://localhost:7687",
username: process.env.MEMGRAPH_USER,
password: process.env.MEMGRAPH_PASSWORD,
});
Differences from Neo4j: Memgraph defaults to no auth (username: ""). It’s faster for streaming analytics and ships some unique procedures (e.g. mgps.*).
runCypher(cypher, params?)
Execute any Cypher query, return rows as CypherRecord[]:
const rows = await store.runCypher(
"MATCH (p:Person {name: $name})-[:KNOWS]->(other) RETURN other.name AS knows",
{ name: "Alice" },
);
for (const r of rows) console.log(r.values.knows);
Always parameterize. Never string-interpolate user input into Cypher.
getSchema()
Returns the live schema by calling db.labels(), db.relationshipTypes(), db.propertyKeys():
const schema = await store.getSchema();
console.log(schema.nodeLabels); // ["Person", "Project"]
console.log(schema.relationshipTypes); // ["KNOWS", "WORKS_ON"]
console.log(schema.propertyKeys); // ["name", "email", "title"]
This is what GraphRAGRetriever reads to ground the LLM’s Cypher generation.
GraphRAGRetriever
LLM-to-Cypher with schema-aware prompting.
import { GraphRAGRetriever, Neo4jCypherStore, openai } from "@agentium/core";
const store = new Neo4jCypherStore({ /* ... */ });
await store.connect();
const retriever = new GraphRAGRetriever({
store,
model: openai("gpt-4o"),
maxRecords: 25, // default 25 - auto-appends LIMIT 25 if missing
systemPrompt: undefined, // optional override - see below
});
const result = await retriever.retrieve("Who works on the Atlas project?");
console.log(result.cypher); // "MATCH (p:Person)-[:WORKS_ON]->(:Project {name: 'Atlas'}) RETURN p.name LIMIT 25"
console.log(result.text); // "name=Alice\nname=Bob"
retrieve(query) return shape
interface GraphRAGResult {
cypher: string; // the Cypher the model generated (post-LIMIT-injection)
records: CypherRecord[]; // raw rows from the DB
text: string; // plain-text rendering of records (great as RAG context)
}
Default system prompt
You convert a natural-language question into a Cypher query that runs against a Neo4j-compatible graph.
Rules:
1. Use ONLY the labels and relationship types listed in the schema.
2. Return only Cypher - no prose, no markdown fences.
3. Always end with a LIMIT clause (default 25 rows).
4. Prefer MATCH ... RETURN over WRITE operations.
Pass systemPrompt to override for domain-specific instructions (e.g. “use the German names for nodes” or “always project full names + emails”).
Safety: automatic LIMIT
If the model omits LIMIT, GraphRAGRetriever appends one with the configured maxRecords. This is the difference between a runaway query and a useful one.
For destructive operations (CREATE, DELETE, MERGE, SET), the default system prompt says “prefer MATCH … RETURN”. For an extra hard fence, run a Cypher parser yourself or use a read-only Neo4j user.
Markdown fence stripping
Models sometimes wrap their Cypher in cypher ... fences despite being told not to. The retriever strips that automatically.
HybridRetriever
Compose vector search + graph search + optional rerank into a single retrieval pipeline.
import {
GraphRAGRetriever,
HybridRetriever,
InMemoryVectorStore,
Neo4jCypherStore,
OpenAIEmbedding,
openai,
CohereReranker,
} from "@agentium/core";
const vector = new InMemoryVectorStore(new OpenAIEmbedding());
await vector.upsert("notes", { id: "1", content: "Alice manages Bob and Carol." });
await vector.upsert("notes", { id: "2", content: "Project Atlas is shipping in Q4." });
const graphStore = new Neo4jCypherStore({ /* ... */ });
await graphStore.connect();
const graph = new GraphRAGRetriever({ store: graphStore, model: openai("gpt-4o") });
const hybrid = new HybridRetriever({
vector: { store: vector, collection: "notes", topK: 10 },
graph: { retriever: graph },
rerank: new CohereReranker(),
topK: 5,
});
const results = await hybrid.retrieve("Who works on Atlas?");
for (const r of results) {
console.log(`[${r.source}] ${r.score.toFixed(3)} ${r.content}`);
}
Algorithm
- Parallel fan-out: vector search + graph retrieval run concurrently.
- Score normalization: vector cosine scores (0–1) and graph row-rank scores (1 - rank/N) are unioned.
- Reciprocal Rank Fusion: combine the two ranked lists with
k=60 (the standard RRF constant). Each result keeps the better of its vector or graph rank, plus a small bonus for appearing in both.
- Optional rerank: if a
Reranker is configured, the top topK * 3 fused results are passed to it for a final reorder.
HybridResult
interface HybridResult {
source: "vector" | "graph"; // which retriever surfaced this
id: string; // unique id, prefixed with v: or g:
content: string;
score: number; // RRF or rerank score
}
Config
interface HybridRetrieverConfig {
vector?: { store: VectorStore; collection: string; topK?: number };
graph?: { retriever: GraphRAGRetriever };
rerank?: Reranker;
topK?: number; // default 10
}
Either vector or graph may be omitted — HybridRetriever becomes a one-side pipeline. Useful when you want to keep the rerank-and-fuse boilerplate but only have one source.
| Step | Latency |
|---|
store.getSchema() (cached at the DB level) | ~5ms |
| LLM Cypher generation (GPT-4o-mini) | ~600ms |
runCypher (small Neo4j) | ~10ms |
| RRF fusion | < 1ms |
| Rerank (Cohere, 30 candidates) | ~200ms |
Total Hybrid retrieve(): ~800ms for typical queries.
Best practices
- Cache the schema at startup if your graph schema is stable. Read it once and pass it into
GraphRAGRetriever via a custom systemPrompt.
- Use a read-only DB user. The LLM should never have credentials that can DROP nodes.
- Cap costs. Set
maxRecords low (10-25) — the LLM can ask for more by paginating in subsequent calls if it really needs them.
- Log all generated Cypher. Add a hook that emits
result.cypher to your observability stack so you can spot prompt regressions.
- Pair with vector for breadth. Pure GraphRAG misses semantic matches that aren’t in the graph. Pure vector misses relationship answers. Hybrid is almost always the right choice in production.
See also