M02 Β· The RAG Baseline and Where It Breaks
For three years, the default answer to every enterprise AI context problem was automatic: "just build a RAG pipeline." This module is not a takedown of RAG β it's a precise autopsy of the specific jobs RAG was never built for. You'll watch a retrieval pipeline confidently return a stale metric definition with nothing crashing anywhere, understand exactly why no better embedding model can fix it, and learn the one-sentence rule that decides when retrieval is the right tool. You cannot evaluate the cures in Tracks 2β3 without holding this disease in your hands first.
Learning objectives
By the end of this module you will be able to:
- Describe RAG's two phases (setup and query) and what each one costs.
- Explain shredded context β why chunking structurally destroys hierarchy, tables, and cross-references, regardless of embedding quality.
- Walk through the churn-rate incident: how probabilistic retrieval of canonical facts fails silently and expensively.
- Identify the operational costs nobody advertises: re-indexing, embedding drift, and the access-control leak.
- State where RAG remains the right tool β and why "RAG first, graph when depth becomes a bottleneck" is the professional default.
RAG in two phases (a fast, honest recap)
You've likely built RAGRetrieval-Augmented Generation: retrieve relevant text from your own data and paste it into the model's prompt so it can answer from your knowledge instead of its training data. before (the sibling course spends two modules on it). Here is the whole machine in two phases, stripped to what matters for this module's argument.
Phase 1 β SETUP (done once, redone on every content change): take your documents, break them into small pieces β chunksFixed-size text segments, typically 256β1000 tokens, cut from documents so each piece fits an embedding model's input and stays "focused.", typically around 512 tokens. Convert each chunk into an embeddingA vector of hundreds of floats representing the text's meaning; texts with similar meaning get vectors that sit near each other.. Store the vectors in a vector database.
Phase 2 β QUERY (every single question): embed the user's question, find the nearest neighborsThe stored vectors closest to the query vector by cosine similarity β the mathematical stand-in for "most related text." among the stored chunk vectors, paste the winning chunks into the prompt, and let the model answer. That's it: search, then generate.
Notice what the machine fundamentally IS: a similarity engine. At no point does it know what any chunk means, whether a chunk is current, or how one chunk relates to another. It knows distances between vectors. Every failure in this module is that one sentence wearing different costumes.
Shredded context β what chunking destroys
Before: imagine a beautifully organized city map β districts, connecting roads, a legend, an index. Now run it through a paper shredder and try to navigate by picking up individual shreds, one at a time, choosing each shred by how visually similar it looks to a photo of your destination.
The pain: each shred is genuinely a piece of the real map β nothing is fabricated β but the connections are gone. The road that continued onto the next shred now ends at a torn edge. Two districts that bordered each other are in separate piles. You can find shreds that resemble your destination; you cannot follow a route.
The mapping: chunking is the shredder. A structured document β a schema with its table, a runbook with its ordered steps, an architecture doc whose section 4 qualifies section 2 β gets cut at token boundaries that ignore its structure entirely. Retrieval hands the model shreds. The model is then asked to answer route-shaped questions from confetti.
The failure has a precise structural form. As a 2026 analysis by Prism Labs put it: "If cause and effect aren't co-located in the same 512-token chunk, your system has no idea they're connected." Chunk boundaries are drawn by token count, not meaning β so a table's header row lands in one chunk and its data rows in another; a code example separates from the caveat below it ("never do this in production"); step 3 of a runbook loses the warning attached to step 2.
Crucially, this is not fixable with better embeddings. A perfect embedding model would perfectly represent the meaning of each shred β but the information you need (the relationship BETWEEN shreds) was destroyed before any embedding was computed. It is an inherent property of independent-chunk retrieval, which is why the fix in this course is architectural (add layers that preserve structure), not model-shopping.
Connect this to M01's multi-hop lesson: "what breaks if I alter this migration?" requires chaining facts across four files. Those facts were never in one chunk, so no similarity search β however good β can assemble the chain. Flat retrieval answers "what text resembles this question?"; multi-hop questions ask "what connects to what?". Different question, different machine.
The churn-rate incident β probabilistic retrieval meets canonical truth
Shredding destroys structure. The second failure is subtler: even perfectly intact chunks can be the WRONG chunks β and nothing tells you.
Here is the corpus's composite incident, assembled from real deployments, told in full because every beat matters. A data analyst asks the company's AI agent: "Write an executive SQL query calculating our churn rate for Q2." The agent embeds the query and searches a vector database holding thousands of chunked PDFs, Confluence pages, and archived Slack logs. It retrieves three high-similarity chunks: a PowerPoint deck from 2023, an old engineering wiki page, and β this detail is perfect β a Slack conversation of two data engineers arguing about how churn should be calculated.
All three chunks are genuinely "about churn." All three score high on similarity. And they contain three conflicting definitions. The model, given conflicting authorities and no way to rank them, blends. The query it writes pulls from the wrong schema. The dashboard ships. Nothing crashed. Nothing errored. Nobody notices for three weeks β until finance asks why weekly revenue doesn't match the invoice system.
The diagnosis, in the corpus's words: "the agent confidently retrieved the wrong version of the truth, because that's all a search engine can do when the underlying knowledge was never actually settled in the first place. You built a search engine. You needed a memory."
Generalize the incident and you get this module's central rule. Retrieval is probabilistic: it returns the most similar chunks, with no concept of authoritative. For exploratory questions ("what themes come up in churn discussions?") that's exactly right. For canonical facts β the metric definition, the compliance policy, the API contract, the SLA β similarity is the wrong criterion entirely. A canonical fact doesn't need to be rediscovered by search on every query. It needs to be written down once, correctly, and looked up. As the corpus puts it: for compliance docs, runbooks, and API specs, "90% semantic similarity is a failure; 100% exactness is non-negotiable."
"The retrieval failed." β No, and this is the trap: retrieval succeeded by its own definition. It returned the most similar chunks. The failure is asking a similarity engine a question whose answer needs authority, currency, and settledness β properties similarity cannot see.
"A reranker / better prompt / bigger top-k fixes it." β Rerankers reorder by relevance, not authority; a stale definition is maximally relevant. Bigger top-k retrieves MORE conflicting definitions. The fix is upstream: settle the fact once, in a place built for lookups (Track 3).
"This failure announces itself." β The whole point of the incident: it doesn't. Wrong-truth retrieval produces plausible output, ships quietly, and surfaces weeks later as a business discrepancy. Silent wrongness is the signature failure mode of this entire course β you'll meet its graph-world twin (stale graphs) in M11.
The operational bill: drift, re-indexing, and the permissions leak
Two costs of vector pipelines rarely make it into the launch blog post.
Synchronization. Embeddings are a derived copy of your data, and every derived copy must be kept in sync with its source. Content changes mean re-chunking and re-embedding; embedding-model upgrades mean re-indexing everything (vectors from different models aren't comparable). The corpus's operators call keeping embeddings synchronized with fast-moving data "an absolute operational nightmare" β and stale vectors fail exactly like the churn incident: silently, by serving yesterday's truth with today's confidence.
The access-control leak. This one costs companies real incidents. Source systems β BigQuery, Cloud Storage, your wiki β enforce per-user permissions. A typical RAG deployment copies content out of those systems into a vector indexβ¦ and the permissions usually don't come along. Result: anyone who can query the index can see everything in it, regardless of what they were allowed to see at the source. Finance-restricted tables, HR documents, incident postmortems β flattened into one searchable pool. This is among the most common, least-discussed ways retrieval systems quietly become security problems; it's specifically why Google built OKF's companion service (Knowledge Catalog) to keep access scoped to source-system permissions β the knowledge moves, and the access rules move with it.
Before shipping any retrieval index, answer in writing: who could query this index, and is that set identical to the union of who could read every source document in it? If the answer is no β and by default it is β you have built a permissions bypass with a search box on it.
Leads vs answers β the structural blind spot
Now aim RAG at code specifically, because the contrast with M01 becomes crisp. Semantic code search (what Cursor's built-in indexing does) is genuinely useful for exploration: "what files relate to authentication?" works well when you don't know where to look. But semantic search has a structural blind spot the corpus names precisely: it doesn't understand relationships. It doesn't know that handleAuth() calls validateToken(), which imports from jwt_utils. It knows these functions contain similar language.
The consequence shapes the agent's whole workflow. Semantic search returns leads β similarity-ranked hints the agent must then verify by opening files one by one (paying M00's exploration tax). A graph returns answers β definitive structural facts, pre-verified at extraction time. Same user question; fundamentally different information architecture. And recall M01's provenance lesson: the graph even labels which answers are facts vs guesses. A similarity score is not a truth label β a 0.91-similarity chunk can be flat wrong, and a 0.62 one exactly right.
Walk it, step by step
Watch the shredding happen. The retrieved chunks are all plausibly related to the question, and the answer built from them is confidently incomplete β which is the failure mode that costs you, not an empty result.
Where RAG still wins β and the professional default
If this module has made you want to delete your vector database: stop. That would be replacing one over-correction with another.
RAG was built to solve one problem well, and it still solves it better than anything in this course: finding relevant passages inside a huge pile of unstructured documents you cannot realistically curate ahead of time. Millions of support tickets. Years of Slack. A document lake of one-off design docs and PDFs. Nobody will ever write concept files for that corpus β and its questions are genuinely exploratory ("what themes come up in churn interviews?"), which is similarity's home turf.
Keep the base rates in view, too: industry surveys found roughly 72% of enterprise RAG deployments underdelivered or failed in year one β but the dominant causes were bad chunking strategies, stale indexes, and mismatched embedding models, not the absence of a graph. The corpus's warning deserves italics: adding a knowledge graph to a flawed retrieval pipeline gives you a flawed retrieval pipeline with higher operational overhead. And in high-stakes territory, retrieval alone measurably fails: legal-domain studies show RAG systems hallucinating on 17β33% of complex queries, because retrieval doesn't model multi-document logic.
So the professional default, per Atlan's guidance, is: "RAG first, graph when depth becomes a bottleneck." Run retrieval for the long tail. Watch for the two specific failure signatures β multi-hop structural questions failing (add a code graph: Track 2), canonical facts coming back stale or conflicting (add curated knowledge: Track 3). M10 turns this into a full decision matrix. Adding layers without an observed failure adds complexity, not capability.
| Vector RAG | Knowledge graph (structural / narrative) | |
|---|---|---|
| Core structure | Independent chunks in vector space | Nodes + typed edges / linked concept files |
| Retrieval | Probabilistic nearest-neighbor | Deterministic traversal / exact file read |
| Best question shape | "What's related to X?" (exploratory) | "What connects to X?" / "What IS the definition of X?" (structural, canonical) |
| Maintenance | Re-chunk, re-embed, re-index; drift management | Re-extract on change (Track 2) / git commits and lint (Track 3) |
| Failure mode | Silently plausible wrong answers | Silently stale graphs (M11's whole subject) |
| Right corpus | Massive, unstructured, uncurated | Code; stable, high-stakes, curated knowledge |
Code walkthrough β make the failure happen, then fix it properly
Theory becomes conviction when the failure runs on your machine. orderflow's docs/architecture.md deliberately preserves a stale paragraph β an old WAU definition ("any API call in 7 days," no tester exclusion) kept under a "Historical note" heading, exactly like real wikis keep them. The canonical definition lives in services/orders/metrics.py. We'll build a tiny retrieval pipeline that surfaces the stale one, then answer the same question deterministically.
Chunk 1 β the chunker. We split docs on paragraph boundaries into ~400-char pieces, keeping {text, source, chunk_index}. Note what we're already losing at this line: the "Historical note (STALE)" heading that qualifies the paragraph can land in a different chunk than the definition it disclaims. The shredder, in one function. Gotcha: we use keyword overlap instead of real embeddings β deliberately. The failure we're demonstrating is about what similarity ranks, not how similarity is computed; keyword scoring shows it without an API key, and a real embedder makes it worse, not better, because the stale paragraph is semantically immaculate.
Chunk 2 β retrieval. Score every chunk by query-term overlap, return the top 3. The stale paragraph is keyword-dense ("weekly active usersβ¦ 7 daysβ¦") so it ranks β often above the correct source, which phrases things differently in code.
Chunk 3 β the deterministic alternative. Same question, zero search: read ONE known file (the M06-style concept for WAU) and print its Computation Rules. No ranking, no luck, no conflicting authorities β a lookup, because a canonical fact deserves a lookup.
from pathlib import Path
DOCS = Path("../sample-project/docs")
# ---- Chunk 1: the shredder ----
def chunk_docs(max_len=400):
chunks = []
for path in sorted(DOCS.glob("*.md")):
try:
text = path.read_text(encoding="utf-8")
except OSError as exc:
print(f"skip {path}: {exc}") # never let one bad file kill the pipeline
continue
buf = ""
for para in text.split("\n\n"):
if len(buf) + len(para) > max_len and buf:
chunks.append({"text": buf, "source": path.name, "i": len(chunks)})
buf = ""
buf += para + "\n\n"
if buf.strip():
chunks.append({"text": buf, "source": path.name, "i": len(chunks)})
return chunks
# ---- Chunk 2: similarity by keyword overlap (embeddings make this WORSE, not better) ----
def retrieve(query, chunks, k=3):
terms = set(query.lower().split())
scored = sorted(chunks,
key=lambda c: -len(terms & set(c["text"].lower().split())))
return scored[:k]
chunks = chunk_docs()
print(f"{len(chunks)} chunks indexed")
for c in retrieve("how is weekly active users calculated?", chunks):
print(f"--- {c['source']} #{c['i']} ---\n{c['text'][:180]}...\n")
# One of these is the STALE definition from architecture.md's historical note.
# Nothing crashed. The pipeline 'worked'.
# ---- Chunk 3: the deterministic alternative β a lookup, not a search ----
import frontmatter
concept = frontmatter.load(
"../M06-okf-authoring/solution/knowledge/analytics/metrics/weekly_active_users.md")
print(concept["title"]) # exactly one authority,
print(concept.content) # version-controlled, current, whole.import { readFileSync, readdirSync } from "node:fs";
import { join } from "node:path";
const DOCS = "../sample-project/docs";
// ---- Chunk 1: the shredder ----
function chunkDocs(maxLen = 400) {
const chunks = [];
for (const name of readdirSync(DOCS).filter((f) => f.endsWith(".md")).sort()) {
let text;
try { text = readFileSync(join(DOCS, name), "utf-8"); }
catch (err) { console.error(`skip ${name}: ${err.message}`); continue; }
let buf = "";
for (const para of text.split("\n\n")) {
if (buf.length + para.length > maxLen && buf) {
chunks.push({ text: buf, source: name, i: chunks.length });
buf = "";
}
buf += para + "\n\n";
}
if (buf.trim()) chunks.push({ text: buf, source: name, i: chunks.length });
}
return chunks;
}
// ---- Chunk 2: similarity by keyword overlap ----
function retrieve(query, chunks, k = 3) {
const terms = new Set(query.toLowerCase().split(/\s+/));
return [...chunks]
.map((c) => ({ c, score: c.text.toLowerCase().split(/\s+/).filter((w) => terms.has(w)).length }))
.sort((a, b) => b.score - a.score)
.slice(0, k).map((x) => x.c);
}
const chunks = chunkDocs();
console.log(`${chunks.length} chunks indexed`);
for (const c of retrieve("how is weekly active users calculated?", chunks)) {
console.log(`--- ${c.source} #${c.i} ---\n${c.text.slice(0, 180)}...\n`);
}
// One of these is the STALE definition. Nothing crashed. The pipeline "worked".
// ---- Chunk 3: the deterministic alternative ----
const concept = readFileSync(
"../M06-okf-authoring/solution/knowledge/analytics/metrics/weekly_active_users.md", "utf-8");
console.log(concept); // one authority, version-controlled, current, whole
You built both halves of this module's argument in 60 lines. The retrieval half surfaced a stale definition because it was similar β and no error, log line, or score warned you. The lookup half returned the canonical definition because someone settled it once in a known place. The question "how do we make retrieval smarter?" quietly became "why are we searching for something we could have written down?" β which is precisely the door Track 3 walks through.
Hands-on lab β labs/M02-rag-shreds-context
π Get the files: labs/M02-rag-shreds-context on GitHub β or clone the whole course once: git clone https://github.com/varasrinivas/knowledge-graph-course.git
What you'll build: the pipeline above, from scratch, plus a short written reflection. Time: 25β35 min. Prerequisites: M01 lab; pip install python-frontmatter.
- Step 1 β Chunk the docs. Write
chunker.pysplittingsample-project/docs/into ~400-char paragraph-aligned chunks. Run it. β Expect ~8β15 chunks with{text, source, chunk_index}. - Step 2 β Retrieve. Write
retrieve.pyscoring by term overlap for "how is weekly active users calculated?"; print the top 3. β The stale architecture.md paragraph appears among them. - Step 3 β The deterministic alternative. Load the M06 solution's WAU concept with
python-frontmatter; print its Computation Rules. β 7-day window, completed orders, tester exclusions. - Step 4 β Reflect (in comments). (a) Would a better embedding model fix Step 2? (b) What process would have prevented the stale paragraph from existing? You'll build exactly that process in M08.
Troubleshooting: if the stale paragraph doesn't surface, your chunker may be merging whole files into one chunk β check the max-length split fires; the demonstration depends on the disclaimer heading separating from the definition.
Knowledge check
Module summary
The machine
RAG = a similarity engine: setup (chunk/embed/store) + query (nearest neighbors into the prompt). It knows distances, never meaning, currency, or relationships.Shredded context
Chunk boundaries sever tables, steps, and cross-references before embedding. Unfixable by better models β the loss is upstream.The incident
Three similar chunks, three conflicting churn definitions, one blended wrong dashboard, three silent weeks. Similar β authoritative.The hidden bill
Perpetual re-indexing and drift; and the ACL leak β a permissions bypass with a search box, unless access rules travel with the knowledge.Leads vs answers
Similarity hints must be verified file-by-file; provenance-labeled edges are usable facts. Different information architectures.The default
RAG first β for the unstructured long tail. Graph when depth becomes a bottleneck; curation when canon goes stale. 72% of RAG failures are RAG done badly, not RAG missing a graph.Next β M03: Parsing Code into Graphs with tree-sitter. Track 2 begins: where M01's edges actually come from. You'll parse real orderflow source into an AST, watch a fault-tolerant parser shrug off syntax errors, and extract call edges with zero LLM calls β the deterministic pass that cannot hallucinate.
References
- Prism Labs β Beyond Vector Embeddings in 2026 (the 512-token co-location analysis)
- Atlan β Knowledge Graphs vs RAG for AI (2026): "RAG first, graph when depth becomes a bottleneck"
- Google Cloud β Knowledge Catalog (source-permission-preserving serving for OKF bundles)
- Legal-domain RAG hallucination studies (17β33% on complex multi-document queries)
- The stale-definition fixture:
labs/sample-project/docs/architecture.md(Historical note) - Lab:
labs/M02-rag-shreds-contextΒ· canonical WAU:labs/M06-okf-authoring/solution/knowledge/analytics/metrics/weekly_active_users.md