On knowledge graphs & RAG

The Fourth Element

Most knowledge graphs know what is true. Few know why it's true, who decided it, and whether it still holds. Context Graph adds a fourth element to every edge — turning a system of record into a system of decision.

(h, r, t, rc) quadruples 11-field RelationContext CGR3 reasoning decision traces & precedents rules & decision gate web ingester (any site)
The gap

What a flat graph forgets

Retrieval-augmented generation has two classic memories. Vector RAG fetches relevant text chunks. Knowledge graphs store (subject, predicate, object) triples. Both are good at what. Neither remembers why.

Take a pricing decision. A normal knowledge graph records:

STANDARD KG — a triple Sarah Chen MegaCorp APPROVES …but approved what? why? still valid? CONTEXT GRAPH — a quadruple Sarah Chen MegaCorp APPROVES rc = RelationContext { decision_trace: "20% discount — 5-yr   relationship + Salesforce pressure" approved_by: "Sarah Chen" approved_via: "in_person" valid_from→until: 2024-08-14 → 12-31 policy_ref: "DiscountPolicy_Standard" provenance: "Slack #deals-review" confidence_score: 0.97 }
Figure 1 — Same edge, two memories. The triple knows an approval happened; the quadruple knows the decision behind it — and can be queried on any of those fields.

With the fourth element in place, you can ask questions a flat graph simply cannot represent:

  • "Who approved discounts above 15% in Q3 2024, and are those approvals still valid?"
  • "Find all pricing exceptions approved via Slack in the last 6 months."
  • "Are there precedents for waiving standard payment terms for a renewal commitment?"

A triple is a headline. A quadruple is the headline plus the byline, the dateline, and the sources.

The model

The fourth element: rc

Context Graph extends every edge from a triple (h, r, t) to a quadruple (h, r, t, rc). RelationContext is an 11-field dataclass — the decision record attached to the relationship.

Evidence & provenance supporting_sentences[] provenance confidence_score — verbatim quotes,   source ref, reliability Authority & channel approved_by approved_via policy_ref — who, through which   channel, under what policy Time valid_from valid_until temporal_info — ISO-8601 window;   "is it still in force?" Rationale & numbers decision_trace quantitative_data — the "why", plus the   numbers that matter
Figure 2 — The 11 RelationContext fields, grouped by what they answer: where it came from, who authorized it, when it holds, and why. Every field is independently queryable.
FieldAnswers
decision_tracethe why — rationale, exception, override, approval reasoning
approved_by · approved_viawho authorized it, and through which channel (slack · zoom · email · in_person · jira · system)
valid_from · valid_until · temporal_infothe validity window — is this still in force today?
policy_refthe policy followed (or overridden)
quantitative_datathe numbers — discount %, budget, counts
supporting_sentences · provenance · confidence_scoreverbatim evidence, source reference, and extraction reliability (0–1)
◆ System of record → system of decision
A system of record tells you the relationship exists. A system of decision tells you the operational reality behind it — and lets you filter the graph by authority, channel, policy, validity, and confidence.
Ingestion

Two ways into the graph

RelationContext arrives through two paths that feed the same graph and the same vector indexes.

Documents transcripts · emails · notes Application / agent approval bots · webhooks Extraction path ainsert() → LLM extracts rc Emission path emit_decision_trace(rc) Context Graph graph + vector indexes
Figure 3 — Extraction mines RelationContext from prose at ingestion; emission writes it directly the moment a decision happens. Same graph, same indexes — so both are queryable together.

Extraction suits historical records — call transcripts, email threads, meeting notes. The LLM pulls entities, relations, and an rc JSON object per relationship.

Emission suits the live moment a decision is made — no document, no re-ingestion. An agent or workflow writes the context atomically:

from lightrag.context_graph_types import RelationContext

rc = RelationContext(
    decision_trace="VP approved 20% discount; 5-yr relationship + competitive pressure",
    approved_by="Sarah Chen", approved_via="in_person",
    valid_until="2024-12-31", policy_ref="DiscountPolicy_Standard",
    quantitative_data="20% discount", confidence_score=0.97,
)
await cg.emit_decision_trace("Sarah Chen", "MegaCorp", "discount_approval", rc)
Reasoning

CGR3: retrieve, rank, reason

Single-shot retrieval answers single-hop questions. Real decision questions are multi-hop — the answer to "are there precedents…" depends on entities you haven't found yet. CGR3 loops until it has enough.

1 · Retrieve entities + edges + rc 2 · Rank LLM orders by relevance 3 · Reason enough to answer? if not: seed new entities, iterate (≤ max_iterations)
Figure 4 — Each pass can surface new entities that seed the next retrieval, so the graph walk expands only as far as the question needs.

Edges come back with their RelationContext attached, and the answer is grounded in it:

answer = await cg.cgr3_query(
    "Are there precedents for waiving standard payment terms for a renewal?")
# cites approval chains, decision traces, temporal validity, and provenance

CGR3 implements the Retrieve→Rank→Reason paradigm (Liang et al., 2024) over the context-enriched graph.

Querying · routing

One question, the right algorithm

A knowledge graph offers several ways to retrieve — entity lookup, community/pattern analysis, vector search, multi-hop reasoning. Each is best for a different shape of question. Most systems make you pick the mode. Context Graph picks it for you.

The auto query router (POST /query/auto) makes one lightweight LLM call to classify the question's intent, routes it to the best retrieval mode, runs it, and returns the answer plus which mode it chose and why.

A question LLM classifier reads intent · cached · ~one call hybriddefault · most questions localone specific entity, named globalbroad category browsing mixspecs · details · comparisons cgr3multi-hop reasoning · comparisons Answer + mode chosen & why + latency
The router classifies once (and caches), routes to the best mode, and returns the answer with its reasoning — so mode selection is transparent, not a black box. A tiny corpus skips the graph entirely (full-context bypass).

Every response says which mode ran and why — so routing is transparent and debuggable rather than a black box. A one-line request replaces a mode-picking decision:

POST /query/auto { "query": "compare the H1H and CurQD-4 on battery and price" }
   →  { "mode": "cgr3", "mode_reason": "explicit multi-product comparison",
       "response": "…", "latency_ms": 1840 }
The payoff

Precedents & validity — your graph as case law

Once every edge carries authority, a policy basis, a validity window, and a rationale, the graph starts to behave like a body of case law: decisions you can cite, check for currency, and trace to who made them.

Precedent search

find_precedents() does semantic search over decision traces — "have we done something like this before, and how did it go?"

Temporal validity

valid_from/until turns "is this still in force today?" into a filter, not a guess — expired approvals stop counting.

Approval chains

approved_by + policy_ref reconstruct who authorized what under which rule — an audit trail by construction.

◆ Why agents care
An AI agent negotiating or advising doesn't just need facts — it needs defensible ones: the precedent it can cite, the approval still in force, the policy it's operating under. That's exactly what the fourth element stores.
Evidence

Does surfacing rc actually help?

Storing context is only useful if exposing it to the LLM produces better answers. Context Graph renders RelationContext inline in the prompt (the annotated context format) and was benchmarked head-to-head against the legacy format that hides it.

10 / 12
overall comparisons won by annotated
56%
criterion win rate (vs 21.4% legacy)
6 – 0
grounding (annotated – legacy)
DimensionAnnotated – Legacy
Completeness10 – 2
Usefulness10 – 2
Synthesis9 – 3
Grounding6 – 0

Comparative LLM-judged benchmark over the project's question set. Surfacing the decision context inline — not just storing it — is what moves the needle, most strongly on grounding (answers tied to real evidence).

Foundations

Standing on LightRAG

Context Graph is a fork of LightRAG (HKUDS). It inherits a production-grade RAG substrate and adds the decision layer on top.

Inherited from LightRAG

  • Pluggable storage: JSON, NetworkX, Neo4j, PostgreSQL, Mongo, Redis, Milvus, Qdrant, Faiss
  • Query modes: local · global · hybrid · naive · mix · bypass
  • LLM bindings: OpenAI, Ollama, Azure, Gemini, Bedrock
  • Chunking, entity/relation extraction, embeddings

Added by Context Graph

  • RelationContext extraction + storage on every edge
  • CGR3 iterative reasoning + annotated context format
  • emit_decision_trace / find_precedents + decision endpoints
  • Multi-tenant workspaces (per-request isolation) and an MCP server (8 tools)
New · Business Rules Engine

Business Rules: from memory to policy

Once every edge carries why a decision was made, the next question writes itself: should this decision have been allowed at all? Context Graph now ships a business rules engine that evaluates every decision the moment it's recorded.

A flat graph stores facts; the rc layer made it remember decisions; this layer lets it act on them — pass, flag for review, or reject — before they're ever written.

The catch: you can't exact-match free text

The fields a rule wants to read — the relation type, the approver, the channel — are LLM-extracted prose. The same decision surfaces as APPROVES, GRANTED_APPROVAL, "signed off on", "authorized". A classic rule engine matches strings exactly and would miss all but one spelling. So rules get a soft predicate, sim(), backed by fast static embeddings (model2vec, numpy-only, sub-millisecond on CPU). It matches by meaning against a small concept catalog — and it's cheap enough to run on every write. Numbers, dates and flags stay on exact predicates, where you want them.

# a rule reads like the policy a human would state
rule "large discount needs finance review"  priority 10
when
    sim(relation_type, "APPROVAL") > 0.4     # soft — meaning, not spelling
    and percent > 0.15                     # hard
    and approved_via == "slack"             # hard
then
    flag("Discount >15% approved over Slack — route to Finance for review")
end

On the real model, sim("GRANTED_APPROVAL", "APPROVAL") scores 1.00 while unrelated relations sit near 0.1 — a wide, calibratable gap. Polarity (approved vs. rejected) stays on structured fields, where static embeddings are weak.

Exact match asks "is it spelled this way?" The gate asks "is this about an approval?" — and then "is it allowed?"

Three outcomes, one audit trail

The gate sits at the single point every decision flows through. A decision passes and persists, gets flagged (persisted, marked needs_review), or is rejected (HTTP 422, nothing written). Either way it leaves a record as explainable as a numeric check — matched concept · score · threshold:

POST /graph/decision/emit  →  200 {
  "outcome": "FLAG",
  "audit": { "matched_concept": "APPROVAL", "score": 1.0, "threshold": 0.4,
             "rule": "large discount needs finance review" }
}                          # …or 422 if a rule rejects the decision

Authored over REST, per workspace

Policies are managed through a small /rules API and validated before they're stored — the DSL must parse and every concept a rule references must be defined. Each tenant (workspace) carries its own rule set and concept catalog, with a pinned embedding model so a rule that passed yesterday behaves identically today.

→ Full walkthrough: the Business Rules Engine guide — the DSL, the fields you can match on, semantic sim(), the pre-emit gate, and the plain-English rule author.

◆ Why it matters
The graph went from a system of record (facts) to a system of decision (the rc layer) to a system of policy — one where an agent's actions are checked against the institution's own rules, with a defensible reason for every pass, flag, and block.
New · ingestion

Filling the graph — from any website

A decision graph is only as good as what's in it, and in most real deployments the primary source is the customer's own website. So Context Graph now includes a smart web ingester: point it at a URL and it turns the site into grounded, cited knowledge.

Any URL a customer site Crawl · politely static + JS render (headless) + network capture · robots · rate-limit Any format html · pdf · docx · json… classified by content-type CG ingestion extract (h,r,t,rc) Graph + URL provenance
Point it at a URL; it crawls politely, reads any format, and hands the content to Context Graph's ingestion — every fact tagged with the page it came from.

Any format, judged by content-type

The crawler never guesses by file extension. It fetches each link and classifies it by content-type — HTML pages are crawled; any data payload (PDF, Word, PowerPoint, Excel, JSON, CSV, Markdown…) is downloaded and handed to Context Graph's existing document pipeline. A new format needs no new code.

It reads what the browser reads

Modern sites hide their data: content rendered by JavaScript, documents inside embedded viewers, catalogs fetched from back-end APIs. So the ingester renders pages in a headless browser and captures the network responses a page makes — finding data that never appears as a link in the HTML. On a real school-district site, the entire policy catalog lived only in a JSON API the page called at load time — invisible to ordinary scraping, captured here.

Connectors — and letting the LLM pick the right one

Some platforms don't just render with JavaScript; they hide every document behind a recursive or paged API. A Finalsite document container lists files only through a folder-tree service you have to walk; a WordPress site keeps its PDFs in a media endpoint you have to page; a BoardDocs board portal serves agendas through a chain of AJAX calls. Capturing one response isn't enough — you have to replay the platform's API. So Context Graph ships connectors: small, platform-generic plugins that recognise a technology and replay its API to enumerate and download every file. Each keys off the platform's signature, never a specific site.

But which connector fits a given site? Rather than try a fixed list in config order, Context Graph asks the LLM. Every connector carries a one-line self-description; at load time the ingester summarises the real page's signals — its generator meta tag, tell-tale markers, the API URLs it actually called — and the LLM reads those against the connector descriptions and names the plugin(s) that apply. Selection is reasoning about the real site, not pattern-matching a hard-coded rule. If the LLM is unsure it says so, and the engine safely falls back to letting each connector self-check.

Real-site signals generator tag · markers API request URLs connector self-descriptions LLM selector which platform is this? Chosen connector finalsite · wordpress · boarddocs Replay API → files every PDF/doc → the graph
The LLM chooses the connector from the site's own signals — so a new platform is a new plugin with a one-line description, not a change to the engine.

It works end-to-end on real sites. On a school district's Finalsite portal it walked the document service and pulled the full policy binder as clean, named PDFs. On creativecommons.org — a WordPress site — the selector reasoned "the api.w.org tag means WordPress", paged the media API, and downloaded the annual report and issue briefs, which Context Graph turned into a graph of over a hundred cited entities.

Polite by default, cited by design

It honours robots.txt, rate-limits per host, and stays on-domain — and every fact it extracts carries the page URL as provenance, so the mentor can cite the exact page it learned something from.

POST /scrape { "url": "https://a-district.org/policies", "render_js": true }
   →  crawl + render + capture  →  documents ingested  →  graph, with citations

→ Full walkthrough: the Web Ingester guide — the polite crawler, the connector framework, LLM-driven connector selection, the site analyst, and the /scrape job API.

◆ The honest edge
Capturing everything also captures noise (analytics, tracking beacons). Telling a policy catalog from a tracking pixel — and pulling the document URLs out of an API response — is a relevance problem, not a plumbing one. An LLM site analyst now handles it: it drops the noise and keeps the real documents. The frontier is breadth of coverage — every new platform still benefits from a connector, even though adding one is now a one-file plugin.
What's next

The road from memory to operational ontology

The decision gate, the web ingester, and the typed ontology are all in; the plan turns Context Graph into a system you don't just reason from, but operate from. (The AI rule author — plain-English policy → validated DSL, dry-run before it goes live — already ships with the rules engine.)

Now shipping

LLM site analyst

The brain on top of the web ingester: given a page, its links, and its captured API responses, an LLM judges relevance (policy catalog vs. tracking beacon) and extracts the document URLs to fetch — so the graph fills with what matters, on any site. Live behind analyze=true — see the web ingester guide.

Now shipping

Ontology layer

A typed schema — object types, typed properties, link types, cardinality — authored top-down and AI-assisted. It gives rules a real vocabulary (Order.value, Employee.role) and validates and coerces what extraction writes. Now live, with a plain-English schema author — see the ontology guide.

Now shipping

Action layer

Executable operations bound to object types (ApproveOrder), invoked over REST, authorized by the rules gate and written back as an audit edge — turning a graph you reason from into one you operate from. Live now; next come object-level access control and agent action-discovery — see the action layer guide.

The arc is deliberate: facts → decisions → policy → an operational ontology, fed from real sources. Each stage stands on its own and sharpens the next — the rules engine gets dramatically stronger once the ontology gives it typed values to reason over.

Read the plan
The full gap analysis, architecture, and build log live in the companion “Closing the Gaps” working document — including the diagrams for how a decision is matched to the graph and flows through the gate.
Use case

AI agents that remember the why

The sharpest place a decision-aware graph earns its keep is AI-driven software development. A coding agent has perfect recall inside a session and near-total amnesia between them — so it rebuilds what already exists, "fixes" deliberate choices whose reasoning it lost, and drifts from the team's methodology. Point it at Context Graph and the project gets a shared memory and a process that runs.

query before you build

Ask the graph "does this already exist, and why is it like this?" before writing a line. On our first live run it stopped a duplicate persistence module cold.

record the why

Every design choice — including the rejected option — becomes a durable, queryable ADR. The threading.Lock-over-asyncio.Lock reasoning a later session would have "fixed" is now on record.

act, governed

Typed actions run a five-guard pipeline — RBAC → lifecycle → rules gate → side effect → audit — so the methodology is enforced, not merely documented.

It's the same engine from the rest of this post, pointed at a new object graph: modules, APIs, tasks, change requests, ADRs, commits. The methodology itself is data — an agentic-dev preset of ontology, rules, actions, RBAC, and lifecycle — so the core stays generic while the agent gets a project that governs itself.

How this differs from the "code graph" crowd. Almost every tool that pairs graph or context with coding agents is a code parser: it indexes your symbols, imports, and call sites and hangs that index off the IDE for better autocomplete and retrieval. Useful — but it's still an index of what the code is, rebuilt from the source each time. Context Graph sits a level up: it's a management system on a generic decision-aware platform that remembers the decisions made across the project, enforces how the next one gets made (rules, RBAC, lifecycle), and keeps the audit trail. A code index is just one input we can ingest — not the product.

◆ Read the full use case
AI Agent Development on Context Graph — the development loop, the project graph, the five governance layers, onboarding & backfill, the one-MCP-surface, and the field results from a real single-agent project that shipped three features through the loop. With diagrams for every flow.
In practice

Where it fits · try it

Any place an AI system reasons over decisions rather than just documents: deal desks, policy and compliance, ops runbooks, support escalations.

The use case that drove these extensions is AI sales. A negotiating agent needs more than product facts — it needs the precedent it can cite ("we waived setup fees for a comparable renewal in Q2"), the discount approval still in force, and the policy it's operating under. Context Graph is the memory that makes those answers defensible — the decision layer behind agents built at Sellence.

Getting started is a few lines:

from lightrag.context_graph import ContextGraph
from lightrag import QueryParam

cg = ContextGraph(working_dir="./cg_storage", llm_model_func=..., embedding_func=...)
await cg.initialize_storages()
await cg.ainsert("Your text with decisions, approvals, and context…")

result = await cg.aquery("Your question", param=QueryParam(mode="hybrid"))
answer = await cg.cgr3_query("A multi-hop precedent question?")
◆ The takeaway
Facts tell you what is true. Decisions tell you why, who, and for how long. By making the decision the fourth element of every edge, Context Graph turns a knowledge graph from a system of record into a system you can reason — and act — from.
Dig deeper
Context Graph — Technical Field Guide is the full reference: architecture, the quadruple, ingestion, CGR3, the REST API, MCP, storage, and multi-tenancy, with diagrams. Per-feature walkthroughs: Business Rules Engine · Ontology · Action Layer · Web Ingester. The full gap analysis and build log live in Closing the Gaps.
↑ Top