Most AI agent projects fail at the same place: the boundary between the data warehouse and the document library. The agent gets SQL access to a BigQuery warehouse and a vector store over a PDF corpus, and the two never meet. When a technician asks "Which part replaced the faulty ignition coil on vehicles with this diagnostic code?", the agent must stitch together a diagnostic code from text, a part number from structured work orders, and a join path across three warehouse tables — and it hallucinates the join because the raw schema does not tell it which keys actually connect.
The fix is not a bigger model. The fix is a context layer that the agent can navigate deterministically. This article walks through an architecture — built around Neo4j, the Neocarta semantic-layer library, MCP servers, and Leiden community detection — that unifies structured warehouse metadata and unstructured document structure into graph shapes an agent can traverse. It is drawn from a hands-on workshop that grounds a coding agent on a simulated automotive repair dataset spanning recalls, bulletins, manuals, and work-order history.
Why structured and unstructured data diverge
AI agents struggle at the structured/unstructured boundary for a concrete reason: each side uses a different retrieval primitive.
The structured side (BigQuery, Snowflake, PostgreSQL) answers questions with SQL. The agent must know which tables exist, which columns are valid keys, and which join paths are real. A raw schema dump gives it names — it does not give it validated relationships. In a 900-table warehouse, this is where text-to-SQL systems hallucinate joins most aggressively, inventing relationships like
cited.patent_id = apps.patent_idwhen the real key iscited.citation_id = apps.patent_id.The unstructured side (manuals, recalls, bulletins) answers questions with vector or lexical search. The agent embeds a query, pulls the nearest chunks, and hopes the right passage surfaces. Vector search is fast and scales well, but it cannot prove a negative — it cannot tell you which documents are missing or where documentation is thin, because similarity search only retrieves what exists.
The result is that simple questions work (the vector store finds the right manual page) and estate-level questions fail (where are issues concentrated across the fleet? which documented procedures have no field evidence?). Closing that gap requires a graph that the agent can traverse in both directions.
The two graphs: metadata graph + document-structure graph
The architecture builds two separate graphs over the same Neo4j instance and lets the agent join them at query time.
The metadata graph (structured side)
The metadata graph is a semantic layer over the warehouse. It maps the physical schema — database, schema, table, column — into a traversable hierarchy in Neo4j, plus the relationships between columns (foreign keys and inferred joins from query logs), plus a glossary of business terms overlaid on top.
Neocarta, a Neo4j Labs Python library, builds this graph from BigQuery and GCP Knowledge Catalog (Dataplex) in two CLI commands. The resulting graph has:
- A database → schema → table → column containment hierarchy mirroring the warehouse.
- Foreign-key relationships between columns, so the agent sees real join paths rather than guessing.
- Inferred joins parsed from production query logs — the cheapest and most accurate source of validated join patterns, because they encode which tables real analysts actually connect.
- BusinessTerm nodes from the glossary, connected to tables and columns via
TAGGED_WITHedges, so the agent can resolve a business term like "comeback ratio" to the specific columns that compute it.
This glossary subgraph is what bridges the gap between a user's vocabulary and the physical data model. "Comeback ratio" is not a column name in any warehouse — it is a derived metric. Without the glossary link, the agent has to infer the computation; with it, the agent follows BusinessTerm:comeback_ratio -[:TAGGED_WITH]-> Table:work_orders and finds the underlying rows.
An MCP server sits in front of the metadata graph and registers tools according to the indexes available. With vector and full-text indexes present, the MCP server exposes hybrid search tools: the agent calls "get context by table business term" and the server runs three searches (vector over table embeddings, full-text over business terms, full-text over table names), fuses the results, and returns the anchor nodes — keeping the agent's context window light instead of dumping a raw schema.
The document-structure graph (unstructured side)
The document graph is deliberately not an entity-extraction graph. Instead of running an LLM to pull entities out of text, it loads the documents deterministically from their inherent structure: a containment tree that mirrors the folder hierarchy, plus cross-document links that already exist in the source files.
Every node gets a hierarchical URI as its ID. A safety bulletin inside the bulletins subfolder of the technical library gets a URI like technical_library/bulletins/safety/TSB-2042. That URI is the key to everything downstream: it lets the agent scope a full-text search to one subtree, traverse one document's links, or drill into a single section's raw text.
The graph carries three relationship types:
HAS— the containment edge (library HAS folder, folder HAS document, document HAS section, section HAS sub-section). A single relationship name keeps Cypher simple because containment goes multiple levels deep.NEXT_SECTION— gives the agent a sense of ordering within a document so it can reconstruct the reading sequence.LINKS_TO— cross-document references (a recall notice linking to a repair manual, a manual linking to a parts catalog). These are the edges that turn a tree into a graph.
This deterministic load is idempotent: drop the graph, reload the same documents, and you get the same graph back. It is faster than entity extraction, costs no LLM tokens, and is stable across runs — every traversal produces the same result as long as the source documents have not changed. The trade-off is that it relies on the documents having inherent structure and interlinking. Messy, unstructured corpora still need an LLM extraction pipeline; well-structured manuals and bulletins do not.
Three shapes the agent traverses
The agent does not query the graphs raw. The architecture defines three shapes — structured outputs that hand the agent a navigable view of the graph without dumping the whole subgraph into context.
Shape 1: Outline (the table of contents)
The outline shape is a hierarchical table of contents generated by a parameterized Cypher query. The agent calls it with an optional depth parameter and an optional starting URI. Without a URI, it returns the whole library (fine for a couple hundred documents); with a URI like technical_library/manuals/falcon-2.0, it returns that document and everything it links to.
The core Cypher uses a variable-length path on the HAS edge with a depth parameter — MATCH path = (root)-[:HAS*0..{depth}]->(descendant) — so the agent can drill down one level for a summary or go deep for the full document tree. A second, simpler query grabs the LINKS_TO edges after the outline, so the agent sees both the tree and the cross-references.
What the agent gets back is a human-readable outline with URIs: section names, nested sub-sections, and the names of linked documents. The agent can then pick a URI, call the outline again from that node, and traverse down — effectively walking the document library the way a human would use a table of contents, but programmatically.
Shape 2: Search (scoped full-text retrieval)
Search uses a Lucene full-text index built on the document and section nodes (the nodes that actually carry text — folders are excluded). It is not vector search; it is lexical, which means the agent can use semantic expansion — instructing the model to apply world knowledge so a query for "engine shuddering" also searches for "misfire" or "rough idle". You cannot do that with a pure vector index.
The key capability is subtree scoping. Because every node's ID is a hierarchical URI, the search applies a STARTS WITH post-filter after the Lucene query. The agent says "search for coils, but only under the recall notices subtree" and the query filters to nodes whose URI starts with technical_library/recalls/. This refines search without graph traversal — it filters down using the URI structure so the engine does not have to walk the containment tree to scope results.
Shape 3: Themes (community detection)
Themes are where the architecture handles estate-level questions that vector search flatly cannot answer: "where are issues concentrated?", "where is documentation thin?", "which documentation are we not leveraging from the warehouse?"
The themes shape runs Leiden community detection on the LINKS_TO graph — the inter-document reference network, not the containment tree. Leiden is the algorithm Microsoft's GraphRAG uses (via the graspologic library), and it improves on Louvain by guaranteeing that all detected communities are internally well-connected, eliminating the disconnected-community bug that affects up to 25% of Louvain's outputs.
To run Leiden at scale, the architecture uses Neo4j's Graph Data Science (GDS) library, which projects a slice of the graph into memory, runs the algorithm at high concurrency, then writes community IDs back. Before projection, the sections are collapsed up to the document level — if one section of a manual links to another document's subsection, those edges aggregate to a single document-to-document edge. This produces a cleaner interlinking view and makes the communities easier to extract.
The output for each community: the documents it groups, a conductance metric indicating how tightly interconnected the community is versus how much it connects outward, the top shared link targets (the link labels most common across the community's edges), and the highest-centrality documents. No LLM labels the themes — the names come directly from the document and link metadata. That makes the output stable: run it twice on the same data, get the same themes. Change the data, and the themes change to reflect it.
A gamma (resolution) parameter controls granularity: at the default of 1, the automotive dataset produced 13 themes; at gamma 2, it refined to 14. Hierarchical Leiden lets you pick a zoom level — coarse themes for a fleet-wide overview, finer themes for a subsystem deep dive.
How the agent stitches structured and unstructured together
The agent itself is a coding agent (Claude Code with the Neo4j Cypher skill) that loads a custom skill describing the three shapes, the MCP server connection, and the run_sql tool for the warehouse. It picks the shape based on the question.
A floor-technician question — "for this VIN with this diagnostic code, what fix has worked on similar vehicles?" — follows a deterministic path:
- Full-text search to ground the document for the diagnostic code, with semantic expansion so "misfire" also matches "rough idle."
- Outline traversal to confirm the grounding — follow the
LINKS_TOedges from the matched manual out to the related repair procedures. - Metadata graph query to get the join path from the work-orders table to the parts table for that DTC code and part.
run_sqlto pull the actual work-order history for that VIN and code.- The agent returns the specific part number — in the demo, an old ignition coil that had been revised and needed replacing — with the document evidence and the warehouse history cited.
This is a question that text-to-SQL alone struggles with because the join path is not in the raw schema. It is a question that vector search alone struggles with because the answer requires structured data (which part, which VIN). The graph architecture handles both because the agent grounds first in the document tree (establishing the right code and the right procedure), then carries that into the metadata graph (finding the real join path), and finally queries the warehouse.
An estate-level question — "are there mismatches between our documented procedures and problems we see in the field? which documentation is missing?" — is where the architecture pays off most. This is a question about a negative — proving documentation does not cover certain field codes. Vector search cannot do this by definition; it only retrieves what exists. The agent:
- Pulls work-order history from the warehouse to see which DTC codes actually occur in the field.
- Cross-references those codes against the document graph to find which codes have no code-level documentation.
- Identifies comeback patterns — vehicles returning repeatedly because the documented fix pointed to the wrong part.
In the demo run, the agent found two field codes diagnosed with the wrong procedure (a mismatch between the library document and the actual repair needed) and two field codes with no documentation at all. With the outline shape, the traversal is faster because the agent walks the LINKS_TO edges directly; without it, the agent falls back to a comprehensive full-text crawl, which works but takes longer. The hierarchical URIs and semantic expansion on the full-text index rescued the slower path — but the lesson is that the outline shape is the preferred route when you have well-linked documents.
A coverage question — "what common patterns appear across all our bulletins and recalls, and how many cars does each affect?" — runs the themes shape, then correlates each theme back to the warehouse work-order history to group work orders under the relevant theme, reporting cars affected by percentage.
The Neo4j CLI: ad-hoc Cypher for questions you did not anticipate
The three shapes are pre-built tools. Real usage produces questions the shapes do not cover, and for those the agent needs the Neo4j CLI — a command-line tool that lets the agent read the graph schema, understand what is in the graph, and write its own Cypher query on the fly.
This is the "text-to-Cypher" path, and it has improved dramatically over the last year when paired with the Cypher and GDS skills. The agent introspects the schema, reasons about the question, and writes a query that glues the shapes together — useful when a question cuts across document structure and warehouse metadata in a way the pre-built shapes did not anticipate. In production, as the skill library grows, agents increasingly prefer the flexible CLI path over the prefix-shaped tools.
Why this beats vector-only RAG for the boundary questions
Vector RAG and Graph RAG are not competitors — they handle different question types.
- Vector RAG wins on fast similarity retrieval across large unstructured corpora. For "find the manual page about ignition coil replacement," a vector index is faster and simpler.
- Graph RAG wins on questions that require navigation: multi-hop reasoning across linked documents, proving a negative, understanding how disparate parts relate to a document, and correlating structured warehouse data with unstructured documentation.
The architecture in this article uses both. The Lucene full-text index on the document graph is lexical, not vector — but the MCP server on the metadata graph uses hybrid vector + full-text + business-term retrieval. The point is neither retrieval primitive alone covers the structured/unstructured boundary. A graph that encodes the structure of both sides — the warehouse's join paths and the corpus's document hierarchy — is what lets the agent navigate both in one query.
Leiden community detection on the interlinking graph surfaces themes no one labeled — you discover that 13 natural clusters exist across the manuals, recalls, and bulletins, and that cluster 7 is entirely braking (rotors, pads, hydraulic lines) while cluster 3 is BCM and bus communications. That kind of estate-level visibility is invisible to per-document retrieval. It only emerges from the structure of the interlinking.
Practical takeaways for building this yourself
Start with a deterministic document load. If your documents have inherent structure (manuals with sections, recalls with cross-references), load them deterministically before reaching for LLM entity extraction. It is faster, cheaper, idempotent, and stable. Save entity extraction for genuinely unstructured corpora.
Use hierarchical URIs as node IDs. Every node in the document graph gets a URI that reflects its position (
technical_library/bulletins/safety/TSB-2042). This single decision enables subtree-scoped search via aSTARTS WITHpost-filter, deterministic traversal from a known starting point, and agent-side URI hopping — the agent grabs a URI from one tool and feeds it to the next.Build the metadata graph from query logs, not just foreign keys. Foreign keys tell you which joins the schema designer declared. Query logs tell you which joins real analysts actually use — and they often reveal joins that should be foreign keys but are not, plus common CTE patterns the agent can reference as few-shot examples.
Overlay the glossary as business terms, not as renamed columns. Do not rename warehouse columns to business vocabulary — you will break downstream SQL consumers. Instead, create
BusinessTermnodes tagged to the physical columns so the agent can resolve "comeback ratio" to the underlyingwork_orderscolumns without the warehouse schema changing.Define shapes as specs before writing queries. The architecture writes a spec document for each shape (outline format, theme format) describing the exact output structure the agent expects. Then it uses a Cypher skill with that spec to generate the query. Spec-first keeps the shapes consistent and lets the agent reason about what each tool returns without reading the Cypher.
Keep the two graphs unlinked initially. The metadata graph (warehouse) and the document graph (corpus) do not share edges at the data level. The agent extracts a part number or code from one and refers it to the other, doing the join in real time. This keeps the load fast and model-agnostic. You can add deterministic cross-links later for performance — but start without them.
Use Leiden, not Louvain, for community detection. Leiden guarantees well-connected communities and runs faster. It is the same algorithm Microsoft GraphRAG uses. Use the GDS library's in-memory projection so it scales to millions of nodes. Collapse sections to documents before projecting so interlinking aggregates to the document level.
Expose a
gammaparameter for theme granularity. Default to 1. Let the user refine to 2 or 3 when they want finer themes. The hierarchical Leiden output means you can also pick a zoom level post-hoc — coarse themes for a fleet summary, fine themes for a subsystem audit.
The boundary is the bottleneck
The hardest AI agent questions are not "find me a document" or "run this SQL." They are the questions that cross the boundary — where the answer lives in the union of a warehouse table and a manual section, and the agent must know the right join path and the right document tree to reach it. A metadata graph over the warehouse plus a document-structure graph over the corpus, both traversable through shapes an agent can navigate, is what closes that gap. The agent stops hallucinating joins because it has the real ones. It stops missing documents because it can walk the tree and prove the negative. And it surfaces themes no one labeled because Leiden finds them in the interlinking for free.

Discussion
0 comments