Three-Graph Router
Overview
Real agentic systems integrate trusted structured data (CSVs, databases) with
untrusted extracted data (documents, reviews). Merging them into one graph is
the failure: unverified information pollutes trusted data, provenance is lost,
extraction errors cascade, and validating agent reasoning becomes impossible.
The Three-Graph Architecture solves this by separating knowledge on origin,
certainty, and semantic role:
- Domain graph — trusted, curated, entity-resolved. The canonical product
list, the definitive org hierarchy. High certainty, stable IDs, protected
from contamination.
- Lexical graph — original unstructured text in structured form. Document
and Chunk nodes, immutable, complete provenance (every chunk links back to
its source). This is the "retrieval" in RAG.
- Subject graph — entities/facts an LLM extracted from the lexical graph,
kept SEPARATE from domain until entity resolution establishes confident
links. Extraction artifacts with explicit uncertainty (confidence, model
version, timestamp).
The router refuses the boundary violations that quietly destroy the
architecture: raw text without provenance is refused from the lexical graph,
extractions without a confidence score are refused from the subject graph, and
an extraction is never written straight into the domain graph. The critical
operation is entity resolution: a subject entity
links to a domain entity via CORRESPONDS_TO only when similarity clears a
confidence threshold (default 0.85; 0.95 high-stakes, 0.75 exploratory). The
worked example: a review mentions "the Stockholm chair", the system extracts a
Subject_Product, finds Product(PROD_12345, "Stockholm Chair") in the domain
graph, and links them if similar enough — enabling the cross-graph query
domain -> CORRESPONDS_TO -> subject -> EXTRACTED_FROM -> lexical with full
provenance.
When to Use
- Ingesting mixed structured + unstructured sources into one agent knowledge base
- Designing the trusted-vs-extracted separation for a graph RAG system
- Preventing LLM extraction errors from contaminating a system of record
- Implementing the CORRESPONDS_TO linkage between extracted and canonical entities
Phrases: "three-graph", "domain/lexical/subject graph", "CORRESPONDS_TO",
"entity resolution linkage", "keep extractions separate", "provenance",
"trusted vs extracted knowledge".
When NOT to Use
- Single trusted source. If all data is curated and entity-resolved, it all
lives in the domain graph; the separation buys nothing.
- The matching algorithm itself. This skill GATES the link with a threshold;
the actual embedding/Jaro-Winkler matcher is a swappable seam, not this skill's
job.
- Graph storage/query engine selection. Use
graph-model-selector for the
model class; this routes records, it does not pick Neo4j-vs-RDF.
- Append-only logs with no notion of "trusted". No domain/subject split
applies.
Process
| Step |
Input |
Action |
Output |
Verification |
| 1 |
Record(payload, origin, entity_resolved, has_provenance, confidence) |
lib.route(record) |
{graph, label_suffix, reasons, requires_resolution} |
structured+resolved->domain; raw_text+provenance->lexical; extraction+confidence->subject |
| 2 |
raw_text record without provenance |
lib.route(record) |
raises ValueError |
lexical graph requires provenance — refuses silent insert |
| 3 |
extraction record without confidence |
lib.route(record) |
raises ValueError |
subject graph requires uncertainty metadata |
| 4 |
extraction marked entity_resolved=True |
lib.route(record) |
raises ValueError |
extractions never enter domain directly |
| 5 |
subject name + {domain_id: name} candidates + threshold |
lib.link_subject_to_domain(...) |
Correspondence(subject_id, domain_id, similarity, linked, threshold) |
links only if best similarity >= threshold |
| 6 |
start graph + target graph |
lib.cross_graph_query_path(start, target) |
edge-type sequence |
domain->lexical = [CORRESPONDS_TO, EXTRACTED_FROM] |
Rationalizations
| Agent rationalization |
Documented rebuttal |
| "Just merge everything into one graph — the separation is overhead." |
The chapter names the exact failures: unverified info pollutes trusted data, provenance is lost, extraction errors cascade, validation becomes impossible. The separation is the entire value proposition; merging is the anti-pattern the architecture exists to prevent. |
| "The LLM extracted it with 0.9 confidence, write it straight to the domain graph." |
No. Confidence is not entity resolution. The extraction is a SUBJECT artifact until CORRESPONDS_TO links it to a canonical domain entity. route raises if you try to mark an extraction entity_resolved and skip the subject graph. The domain graph is the single source of truth precisely because extractions cannot bypass resolution. |
| "Provenance on lexical chunks is bookkeeping I can skip." |
Provenance is what lets the agent cite the exact source passage and lets analysts audit retrieval quality. The chapter lists "complete provenance" as a defining lexical characteristic. route refuses provenance-less raw_text on purpose. |
| "I'll set the CORRESPONDS_TO threshold to 0.5 so more links form." |
0.5 floods the domain graph with false links — the conflation error the chapter warns is as damaging as fragmentation. Defaults: 0.95 high-stakes, 0.85 standard, 0.75 exploratory. Lowering it is a deliberate, documented precision/recall trade-off, not a default. |
| "Re-extracting the subject graph is dangerous — it'll change my data." |
The opposite: because subject is separate from domain, you re-extract subject from the immutable lexical graph as models improve, and the domain graph is untouched. The separation is what makes re-extraction safe. |
Red Flags
- Many extractions routed with
requires_resolution=False. Bug — every
subject entity needs resolution before it can be trusted as domain.
link_subject_to_domain linking almost everything. Threshold too low;
you are conflating distinct entities. Raise it and re-audit.
link_subject_to_domain linking almost nothing. Threshold too high OR the
similarity stub is wrong for the domain — swap in the real matcher at the seam.
- Domain graph node count growing on every ingestion run. Extractions are
leaking into domain. Verify
route is the only write path to domain and that
it rejects extraction origins.
- Lexical chunks with no source-document edge. Provenance broken; agent
citations become unverifiable.
Non-Negotiable Verification
- Run the benchmark battery.
python cli.py benchmark must report 10/10:
- each origin routes to the correct graph
- provenance-less raw_text, confidence-less extraction, and
extraction-marked-resolved all RAISE (boundary enforcement)
- linkage respects the threshold and picks the best candidate
- cross-graph path is correct
- Run the scenario.
python cli.py scenario stockholm-chair routes all
three record types and shows the CORRESPONDS_TO link forming.
- Verify CLI help.
python cli.py --help exits 0 and prints this SKILL.md
description (so any harness can discover the skill from --help).
Security Posture
- Prompt injection. Records are untrusted by design - subject-graph
extractions come from adversarial documents. The router never executes
payload content; its refusal rules ARE the defense that keeps injected
extractions out of the trusted domain graph. The attack to resist is
threshold-lowering or marking extractions entity_resolved to bypass the gate.
- Data exfiltration. No network calls, no file writes. Record payloads and
provenance metadata stay in-process; routing decisions go to stdout and the
caller owns downstream piping.
- Privilege escalation. A CORRESPONDS_TO link is the escalation surface: it
promotes extracted data toward trusted status. The gate links only above the
confidence threshold, and no code path writes an extraction directly to
domain - keep those invariants when swapping in a real matcher at the seam.
Source Attribution
Distilled from Agentic GraphRAG (O'Reilly, by Anthony Alcaraz and Sam Julien) Ch3 — Knowledge
Representation, section "The Three-Graph Architecture for Agent Knowledge"
(domain / lexical / subject graphs, Figure 3-2) and "Entity Resolution and
Linking Across Graphs" (the CORRESPONDS_TO three-stage linking pipeline and the
0.95 / 0.85 / 0.75 Jaro-Winkler thresholds). The Stockholm-chair worked example
is the chapter's own illustration of subject-to-domain resolution.
1---2name: three-graph-router3description: Route an incoming record/fact into the correct graph of the Three-Graph Architecture (Ch3) — DOMAIN (trusted, entity-resolved single source of truth), LEXICAL (verbatim source text with provenance, the "retrieval" in RAG), or SUBJECT (LLM-extracted artifacts kept SEPARATE from domain until entity resolution links them). The router enforces the boundaries that make the architecture work — an extraction can NEVER be written straight into the domain graph; it must enter subject and link via CORRESPONDS_TO above a confidence threshold (default 0.85). Use when ingesting mixed structured + unstructured data into an agent knowledge graph, when designing the separation between trusted and extracted knowledge, or when preventing extraction errors from contaminating ground truth. NOT for single-source trusted data (no separation needed), NOT for the entity- resolution matching algorithm itself (this gates the link; a real matcher swaps in at the seam), NOT for graph storage/query engine choice.4---56# Three-Graph Router78## Overview910Real agentic systems integrate trusted structured data (CSVs, databases) with11untrusted extracted data (documents, reviews). Merging them into one graph is12the failure: unverified information pollutes trusted data, provenance is lost,13extraction errors cascade, and validating agent reasoning becomes impossible.1415The Three-Graph Architecture solves this by separating knowledge on **origin,16certainty, and semantic role**:1718- **Domain graph** — trusted, curated, entity-resolved. The canonical product19 list, the definitive org hierarchy. High certainty, stable IDs, protected20 from contamination.21- **Lexical graph** — original unstructured text in structured form. Document22 and Chunk nodes, immutable, complete provenance (every chunk links back to23 its source). This is the "retrieval" in RAG.24- **Subject graph** — entities/facts an LLM extracted from the lexical graph,25 kept SEPARATE from domain until entity resolution establishes confident26 links. Extraction artifacts with explicit uncertainty (confidence, model27 version, timestamp).2829The router refuses the boundary violations that quietly destroy the30architecture: raw text without provenance is refused from the lexical graph,31extractions without a confidence score are refused from the subject graph, and32an extraction is never written straight into the domain graph. The critical33operation is entity resolution: a subject entity34links to a domain entity via `CORRESPONDS_TO` only when similarity clears a35confidence threshold (default 0.85; 0.95 high-stakes, 0.75 exploratory). The36worked example: a review mentions "the Stockholm chair", the system extracts a37`Subject_Product`, finds `Product(PROD_12345, "Stockholm Chair")` in the domain38graph, and links them if similar enough — enabling the cross-graph query39`domain -> CORRESPONDS_TO -> subject -> EXTRACTED_FROM -> lexical` with full40provenance.4142## When to Use4344- Ingesting mixed structured + unstructured sources into one agent knowledge base45- Designing the trusted-vs-extracted separation for a graph RAG system46- Preventing LLM extraction errors from contaminating a system of record47- Implementing the CORRESPONDS_TO linkage between extracted and canonical entities4849Phrases: "three-graph", "domain/lexical/subject graph", "CORRESPONDS_TO",50"entity resolution linkage", "keep extractions separate", "provenance",51"trusted vs extracted knowledge".5253## When NOT to Use5455- **Single trusted source.** If all data is curated and entity-resolved, it all56 lives in the domain graph; the separation buys nothing.57- **The matching algorithm itself.** This skill GATES the link with a threshold;58 the actual embedding/Jaro-Winkler matcher is a swappable seam, not this skill's59 job.60- **Graph storage/query engine selection.** Use `graph-model-selector` for the61 model class; this routes records, it does not pick Neo4j-vs-RDF.62- **Append-only logs with no notion of "trusted".** No domain/subject split63 applies.6465## Process6667| Step | Input | Action | Output | Verification |68|------|-------|--------|--------|--------------|69| 1 | `Record(payload, origin, entity_resolved, has_provenance, confidence)` | `lib.route(record)` | `{graph, label_suffix, reasons, requires_resolution}` | structured+resolved->domain; raw_text+provenance->lexical; extraction+confidence->subject |70| 2 | raw_text record without provenance | `lib.route(record)` | raises `ValueError` | lexical graph requires provenance — refuses silent insert |71| 3 | extraction record without confidence | `lib.route(record)` | raises `ValueError` | subject graph requires uncertainty metadata |72| 4 | extraction marked `entity_resolved=True` | `lib.route(record)` | raises `ValueError` | extractions never enter domain directly |73| 5 | subject name + `{domain_id: name}` candidates + threshold | `lib.link_subject_to_domain(...)` | `Correspondence(subject_id, domain_id, similarity, linked, threshold)` | links only if best similarity >= threshold |74| 6 | start graph + target graph | `lib.cross_graph_query_path(start, target)` | edge-type sequence | domain->lexical = [CORRESPONDS_TO, EXTRACTED_FROM] |7576## Rationalizations7778| Agent rationalization | Documented rebuttal |79|------------------------|--------------------|80| "Just merge everything into one graph — the separation is overhead." | The chapter names the exact failures: unverified info pollutes trusted data, provenance is lost, extraction errors cascade, validation becomes impossible. The separation is the entire value proposition; merging is the anti-pattern the architecture exists to prevent. |81| "The LLM extracted it with 0.9 confidence, write it straight to the domain graph." | No. Confidence is not entity resolution. The extraction is a SUBJECT artifact until CORRESPONDS_TO links it to a canonical domain entity. `route` raises if you try to mark an extraction `entity_resolved` and skip the subject graph. The domain graph is the single source of truth precisely because extractions cannot bypass resolution. |82| "Provenance on lexical chunks is bookkeeping I can skip." | Provenance is what lets the agent cite the exact source passage and lets analysts audit retrieval quality. The chapter lists "complete provenance" as a defining lexical characteristic. `route` refuses provenance-less raw_text on purpose. |83| "I'll set the CORRESPONDS_TO threshold to 0.5 so more links form." | 0.5 floods the domain graph with false links — the conflation error the chapter warns is as damaging as fragmentation. Defaults: 0.95 high-stakes, 0.85 standard, 0.75 exploratory. Lowering it is a deliberate, documented precision/recall trade-off, not a default. |84| "Re-extracting the subject graph is dangerous — it'll change my data." | The opposite: because subject is separate from domain, you re-extract subject from the immutable lexical graph as models improve, and the domain graph is untouched. The separation is what makes re-extraction safe. |8586## Red Flags8788- **Many extractions routed with `requires_resolution=False`.** Bug — every89 subject entity needs resolution before it can be trusted as domain.90- **`link_subject_to_domain` linking almost everything.** Threshold too low;91 you are conflating distinct entities. Raise it and re-audit.92- **`link_subject_to_domain` linking almost nothing.** Threshold too high OR the93 similarity stub is wrong for the domain — swap in the real matcher at the seam.94- **Domain graph node count growing on every ingestion run.** Extractions are95 leaking into domain. Verify `route` is the only write path to domain and that96 it rejects extraction origins.97- **Lexical chunks with no source-document edge.** Provenance broken; agent98 citations become unverifiable.99100## Non-Negotiable Verification1011021. **Run the benchmark battery.** `python cli.py benchmark` must report 10/10:103 - each origin routes to the correct graph104 - provenance-less raw_text, confidence-less extraction, and105 extraction-marked-resolved all RAISE (boundary enforcement)106 - linkage respects the threshold and picks the best candidate107 - cross-graph path is correct1082. **Run the scenario.** `python cli.py scenario stockholm-chair` routes all109 three record types and shows the CORRESPONDS_TO link forming.1103. **Verify CLI help.** `python cli.py --help` exits 0 and prints this SKILL.md111 description (so any harness can discover the skill from --help).112113## Security Posture114115- **Prompt injection.** Records are untrusted by design - subject-graph116 extractions come from adversarial documents. The router never executes117 payload content; its refusal rules ARE the defense that keeps injected118 extractions out of the trusted domain graph. The attack to resist is119 threshold-lowering or marking extractions entity_resolved to bypass the gate.120- **Data exfiltration.** No network calls, no file writes. Record payloads and121 provenance metadata stay in-process; routing decisions go to stdout and the122 caller owns downstream piping.123- **Privilege escalation.** A CORRESPONDS_TO link is the escalation surface: it124 promotes extracted data toward trusted status. The gate links only above the125 confidence threshold, and no code path writes an extraction directly to126 domain - keep those invariants when swapping in a real matcher at the seam.127128## Source Attribution129130Distilled from *Agentic GraphRAG* (O'Reilly, by Anthony Alcaraz and Sam Julien) Ch3 — Knowledge131Representation, section "The Three-Graph Architecture for Agent Knowledge"132(domain / lexical / subject graphs, Figure 3-2) and "Entity Resolution and133Linking Across Graphs" (the CORRESPONDS_TO three-stage linking pipeline and the1340.95 / 0.85 / 0.75 Jaro-Winkler thresholds). The Stockholm-chair worked example135is the chapter's own illustration of subject-to-domain resolution.