# Knowledge Graphs: The Memory Layer Your AI Agent Needs

## Before we start: a quick thought experiment

You are building an AI agent to help a compliance analyst at a government agency. The analyst types:

> "Show me all vendors with active contracts who are linked to sanctioned entities."

Your agent has a vector database loaded with thousands of contract PDFs. (A vector database is a tool that finds documents by *similarity* — you give it a question, it finds the chunks of text that are most similar in meaning.) It retrieves the most *similar* documents to the query. Some are relevant. Some are not. And critically — the agent has no way to *traverse* the relationship between a vendor, its parent company, and a sanctions list.

It cannot answer the question correctly. Not because the model is bad. Because the data has no structure the agent can reason over.

This is the problem knowledge graphs solve.

---

## Part 1: The fundamentals

### What is a knowledge graph?

A knowledge graph is a way of storing information as **things** and **relationships between things** — rather than as rows in a table or chunks of text in a database.

Every piece of information in a knowledge graph is expressed as a triple:

```plaintext
(Subject) --[Relationship]--> (Object)
```

That's it. Three parts. Subject, relationship, object.

Here are some examples:

```plaintext
(NASA) --[AWARDS]--> (Contract #FA8650)
(Contract #FA8650) --[INVOLVES]--> (Acme Corp)
(Acme Corp) --[LOCATED_IN]--> (United States)
(Contract #FA8650) --[FALLS_UNDER]--> (FAR Part 15)
```

Each of these triples is a **fact**. String enough facts together and you have a graph — a web of entities connected by meaningful relationships.

Here is what that looks like visually:

```plaintext
                    ┌─────────────┐
                    │    NASA     │  (Agency)
                    └──────┬──────┘
                           │ AWARDS
                           ▼
                    ┌─────────────┐        ┌──────────────┐
                    │Contract     │───────▶│  Acme Corp   │  (Vendor)
                    │#FA8650      │INVOLVES└──────┬───────┘
                    └──────┬──────┘               │ LOCATED_IN
                           │ FALLS_UNDER          ▼
                           ▼               ┌──────────────┐
                    ┌─────────────┐        │United States │  (Country)
                    │  FAR Part   │        └──────────────┘
                    │     15      │  (Regulation)
                    └─────────────┘
```

Each box is a **node** (a thing). Each arrow is an **edge** (a relationship). This is a knowledge graph — four nodes, three relationships, and already you can answer questions no spreadsheet or document store could: *"What regulation governs the contract that NASA awarded to a US-based vendor?"*

---

### The three building blocks

#### 1\. Nodes (entities)

A node represents a *thing* in your domain. In a government contracting graph:

- An **Agency** (e.g. NASA, DoD, HHS)
- A **Contract** (e.g. a specific procurement)
- A **Vendor** (e.g. a company awarded the contract)
- A **Regulation** (e.g. FAR Part 15)
- A **Country** (e.g. where the vendor is incorporated)

Each node has **properties** — key-value pairs that describe it:

```plaintext
(:Vendor {
  name: "Acme Corp",
  ein: "12-3456789",
  country: "US",
  risk_flag: false,
  founding_year: 1998
})
```

![](https://cdn.hashnode.com/uploads/covers/6ae46465de3ac20c1539a321/bd57318e-0927-47b0-bc88-e633b66f085b.png align="center")

#### 2\. Edges (relationships)

An edge connects two nodes and gives the relationship a name. The name is not arbitrary — it encodes meaning:

```plaintext
(Agency)-[:AWARDS]->(Contract)
(Contract)-[:INVOLVES]->(Vendor)
(Vendor)-[:SUBSIDIARY_OF]->(ParentCompany)
(ParentCompany)-[:LISTED_ON]->(SanctionsList)
```

Edges can also carry properties:

```plaintext
(Agency)-[:AWARDS {
  date: "2024-03-15",
  award_type: "competitive"
}]->(Contract)
```

#### 3\. The graph as a whole

Individually, each triple is just a fact. Together, they form a navigable structure — one that an AI agent can traverse to answer multi-hop questions:

> "Which agencies have contracts with vendors whose parent companies appear on a sanctions list?"

A graph traversal answers this in one query. A vector database cannot answer it at all.

---

### Knowledge graph vs. vector database — the right tool for each job

This is not an either/or choice. They solve different problems.

| Question type | Best tool |
| --- | --- |
| "Find contracts similar to this one" | Vector DB |
| "Summarise these 50 PDF contracts" | Vector DB |
| "Which vendors have contracts in APAC?" | Knowledge Graph |
| "Is this vendor connected to a sanctioned entity?" | Knowledge Graph |
| "What regulations apply to this contract type?" | Knowledge Graph |
| "Find me documents about procurement fraud" | Vector DB |
| "Trace the ownership chain of this supplier" | Knowledge Graph |

**The rule of thumb:** if the question requires traversing *relationships* between known entities, use a knowledge graph. If the question requires finding *similar* content from unstructured text, use a vector database.

In a mature enterprise AI stack, you use both — the vector database handles unstructured retrieval, the knowledge graph handles structured reasoning. Your agent decides which to query based on the question type.

![](https://cdn.hashnode.com/uploads/covers/6ae46465de3ac20c1539a321/8a36cc55-dc50-4d7e-bc39-b43f71246418.png align="center")

---

### Why AI agents specifically need knowledge graphs

In Post 1, we identified three failure modes that kill enterprise AI agent deployments:

1. Hallucination of relationships
2. Context collapse mid-task
3. Non-deterministic answers

A knowledge graph fixes all three — here is how:

**Hallucination of relationships** — the agent can no longer invent connections. When it needs to know if Vendor A is related to Entity B, it queries the graph. If the relationship is not there, the answer is "no relationship found" — not a confident fabrication.

**Context collapse mid-task** — the graph provides a stable, persistent world model the agent can re-query at any step. It does not need to "remember" domain facts across reasoning steps — it looks them up.

**Non-deterministic answers** — the same graph traversal query returns the same result every time. The agent's reasoning is now grounded in a consistent data source.

---

> **Key takeaways from Part 1 — before you continue:**
>
> 1. A knowledge graph stores information as **things** (nodes) and **relationships** (edges) — not as rows or text chunks.
> 2. Every fact is a triple: `(Subject)--[Relationship]-->(Object)`. Simple, but powerful when combined at scale.
> 3. Knowledge graphs and vector databases are **complementary** — use a KG for relationship questions, a vector DB for similarity search.
> 4. For AI agents specifically, a KG provides a stable world model that eliminates hallucinated relationships and inconsistent answers.

---

## Part 2: Lab exercise

> **What you will build:** A knowledge graph schema for US government contract data, populated from public data at [data.gov](http://data.gov). By the end of this exercise you will have a working Neo4j schema and a set of Cypher queries your AI agent can call.

### A note on tooling: what is Neo4j?

**Neo4j** is the most widely used graph database in the world. Think of it as the MySQL of graph data — battle-tested, well-documented, and free to start. It uses a query language called **Cypher**, which is designed to look like the ASCII diagrams you have already seen: `(Node)-[:RELATIONSHIP]->(Node)`. If you can read the diagrams above, you can already read Cypher.

Other graph databases exist (Amazon Neptune, TigerGraph, NebulaGraph) but Neo4j has the largest community and is the best place to start learning.

### Setup

You will need:

- **Neo4j Desktop** (free) — download from [neo4j.com/download](http://neo4j.com/download)
- Or **Neo4j AuraDB Free** — a free cloud instance at [neo4j.com/cloud/aura-free](http://neo4j.com/cloud/aura-free)
- No prior database experience needed — we walk through every step

### Step 1 — Understand the domain

Before writing a single line of schema, map out your entities and relationships on paper (or a whiteboard). This is the most important step.

For US government contracts (sourced from [USASpending.gov](http://USASpending.gov) — a public dataset), the core entities are:

```plaintext
Entities:
  - Agency         (the government body spending the money)
  - Contract       (the specific award)
  - Vendor         (the company receiving the award)
  - Regulation     (the rule governing the contract type)
  - Country        (where the vendor operates)

Relationships:
  - Agency    AWARDS        Contract
  - Contract  INVOLVES      Vendor
  - Contract  FALLS_UNDER   Regulation
  - Vendor    LOCATED_IN    Country
  - Vendor    SUBSIDIARY_OF Vendor  (self-referential — parent/child companies)
```

Draw this out. Each box is a node type. Each arrow is a relationship. This diagram *is* your schema.

---

### Step 2 — Define your node schema in Cypher

**A quick Cypher primer before we start:**

| Keyword | What it does |
| --- | --- |
| `CREATE` | Adds a new node or relationship to the graph |
| `MATCH` | Finds existing nodes or relationships |
| `WHERE` | Filters results by a condition |
| `RETURN` | Specifies what to output from a query |
| `(:Label {prop: value})` | Describes a node with a type and properties |
| `-[:RELATIONSHIP]->` | Describes a directed relationship between nodes |

That is the entire syntax you need for this lab.

Open Neo4j and run the following to create constraints (this ensures uniqueness and speeds up queries):

```cypher
// Constraints — run these first
CREATE CONSTRAINT agency_id IF NOT EXISTS
  FOR (a:Agency) REQUIRE a.agency_code IS UNIQUE;

CREATE CONSTRAINT contract_id IF NOT EXISTS
  FOR (c:Contract) REQUIRE c.contract_id IS UNIQUE;

CREATE CONSTRAINT vendor_ein IF NOT EXISTS
  FOR (v:Vendor) REQUIRE v.ein IS UNIQUE;
```

Now create your sample nodes:

```cypher
// Create Agencies
CREATE (:Agency {
  agency_code: "NASA",
  name: "National Aeronautics and Space Administration",
  region: "Federal",
  annual_budget_usd: 25400000000
});

CREATE (:Agency {
  agency_code: "DOD",
  name: "Department of Defense",
  region: "Federal",
  annual_budget_usd: 858000000000
});

// Create Vendors
CREATE (:Vendor {
  ein: "95-1234567",
  name: "Aerospace Solutions Inc",
  country: "US",
  risk_flag: false,
  year_founded: 1985
});

CREATE (:Vendor {
  ein: "98-7654321",
  name: "Global Tech Partners",
  country: "US",
  risk_flag: true,
  risk_reason: "Parent company under OFAC review"
});

// Create Contracts
CREATE (:Contract {
  contract_id: "NAS-2024-001",
  value_usd: 4500000,
  start_date: "2024-01-15",
  end_date: "2026-01-14",
  status: "active",
  contract_type: "cost_plus_fixed_fee"
});

CREATE (:Contract {
  contract_id: "DOD-2024-089",
  value_usd: 125000000,
  start_date: "2024-03-01",
  end_date: "2027-02-28",
  status: "active",
  contract_type: "firm_fixed_price"
});

// Create Regulations
CREATE (:Regulation {
  code: "FAR-15",
  title: "Contracting by Negotiation",
  jurisdiction: "Federal"
});

CREATE (:Regulation {
  code: "DFARS-252",
  title: "Defense Federal Acquisition Regulation Supplement",
  jurisdiction: "DoD"
});
```

---

### Step 3 — Create the relationships

This is where the graph comes alive. Run these to wire everything together:

```cypher
// NASA awards Contract NAS-2024-001
MATCH (a:Agency {agency_code: "NASA"})
MATCH (c:Contract {contract_id: "NAS-2024-001"})
CREATE (a)-[:AWARDS {
  award_date: "2024-01-10",
  award_type: "competitive"
}]->(c);

// DoD awards Contract DOD-2024-089
MATCH (a:Agency {agency_code: "DOD"})
MATCH (c:Contract {contract_id: "DOD-2024-089"})
CREATE (a)-[:AWARDS {
  award_date: "2024-02-20",
  award_type: "sole_source"
}]->(c);

// Contracts involve Vendors
MATCH (c:Contract {contract_id: "NAS-2024-001"})
MATCH (v:Vendor {ein: "95-1234567"})
CREATE (c)-[:INVOLVES]->(v);

MATCH (c:Contract {contract_id: "DOD-2024-089"})
MATCH (v:Vendor {ein: "98-7654321"})
CREATE (c)-[:INVOLVES]->(v);

// Contracts fall under Regulations
MATCH (c:Contract {contract_id: "NAS-2024-001"})
MATCH (r:Regulation {code: "FAR-15"})
CREATE (c)-[:FALLS_UNDER]->(r);

MATCH (c:Contract {contract_id: "DOD-2024-089"})
MATCH (r:Regulation {code: "DFARS-252"})
CREATE (c)-[:FALLS_UNDER]->(r);
```

---

### Step 4 — Query the graph like an agent would

Now the interesting part. These are the kinds of queries an AI agent would run to answer compliance questions:

**Query 1: Find all active contracts involving flagged vendors**

```cypher
MATCH (a:Agency)-[:AWARDS]->(c:Contract)-[:INVOLVES]->(v:Vendor)
WHERE c.status = "active"
  AND v.risk_flag = true
RETURN a.name AS agency,
       c.contract_id AS contract,
       c.value_usd AS value,
       v.name AS vendor,
       v.risk_reason AS reason
ORDER BY c.value_usd DESC;
```

Expected result:

```plaintext
agency                    | contract      | value       | vendor               | reason
Department of Defense     | DOD-2024-089  | 125,000,000 | Global Tech Partners | Parent company under OFAC review
```

**Query 2: Which regulations govern the highest-value contracts?**

```cypher
MATCH (c:Contract)-[:FALLS_UNDER]->(r:Regulation)
RETURN r.title AS regulation,
       count(c) AS contract_count,
       sum(c.value_usd) AS total_value
ORDER BY total_value DESC;
```

**Query 3: Multi-hop — agencies connected to risk through vendor relationships**

```cypher
MATCH (a:Agency)-[:AWARDS]->(c:Contract)-[:INVOLVES]->(v:Vendor)
WHERE v.risk_flag = true
RETURN DISTINCT a.name AS at_risk_agency,
       count(c) AS flagged_contracts;
```

This last query is the one that proves the KG's value. No vector database can answer "which agencies are indirectly exposed to risk through their vendors" — it requires traversing three hops of relationships.

---

### Step 5 — Make it agent-callable

The final step is wrapping your Cypher queries as tools your AI agent can call. Here is a simple Python pattern using the Neo4j driver:

```python
from neo4j import GraphDatabase

class ContractKGTools:
    def __init__(self, uri, auth):
        self.driver = GraphDatabase.driver(uri, auth=auth)

    def get_flagged_vendor_contracts(self) -> list[dict]:
        """Agent calls this to find active contracts with flagged vendors."""
        query = """
        MATCH (a:Agency)-[:AWARDS]->(c:Contract)-[:INVOLVES]->(v:Vendor)
        WHERE c.status = "active" AND v.risk_flag = true
        RETURN a.name AS agency, c.contract_id AS contract,
               c.value_usd AS value, v.name AS vendor
        ORDER BY c.value_usd DESC
        """
        with self.driver.session() as session:
            return [dict(record) for record in session.run(query)]

    def trace_vendor_exposure(self, agency_code: str) -> list[dict]:
        """Agent calls this to check an agency's risk exposure."""
        query = """
        MATCH (a:Agency {agency_code: $code})-[:AWARDS]->
              (c:Contract)-[:INVOLVES]->(v:Vendor)
        WHERE v.risk_flag = true
        RETURN c.contract_id AS contract, v.name AS vendor,
               v.risk_reason AS reason
        """
        with self.driver.session() as session:
            return [dict(record) for record in session.run(query, code=agency_code)]
```

Your agent now has deterministic, relationship-aware tools. The same query returns the same result every time. No hallucination of structure. No context collapse.

**What is happening here in plain English:** Each method is a named, reusable question you can ask the graph. The agent calls `get_flagged_vendor_contracts()` the same way it would call a calculator or a search API. The graph answers with structured data. The agent reasons over that data. No guessing, no similarity matching — just facts from a source of truth.

---

## What you built — and what is still missing

By the end of this exercise you have:

- A graph schema with 5 node types and 4 relationship types
- Sample data modelled from real government contracting patterns
- 3 agent-callable Cypher queries that traverse multi-hop relationships
- A Python wrapper that exposes the graph as agent tools

But here is what the graph does not yet know:

The property `risk_flag: true` on a Vendor node — what does that actually mean? Who decides? What rules determine when a vendor becomes flagged? What is the definition of "active" for a contract?

The graph has the *structure*. It does not have the *meaning*.

That is exactly what an ontology provides — and that is what the next post is about.

---

## What's coming next

- **Post 3:** Ontology: giving your AI a world model
- **Post 4:** The semantic layer: bridging your data warehouse and your LLM
- **Post 5:** Graph RAG vs. vector RAG — when structure beats similarity search
- **Post 6:** Enterprise patterns: KG + agents in the real world

---
