Session 4 · Week 2 · Thu 15 Oct 2026 · 75 min

Design your context pipeline.

Tuesday was the theory. Tonight you make five concrete decisions about what reaches your model's window, and walk out with Project 1: your Context & Retrieval Design.

Ehsan Gazar
Production-Ready Systems with LLMs and Agents · session 4 of 8
Tonight builds an artifact

From theory to a design you submit.

0–10Recap the pipeline + the five decisionsthe deliverable shape
10–30Worked example + how to decide eacha real corpus, the five choices in depth
30–70Build yoursthe template, decision by decision
70–85Share + pressure-testwe try to break a few
85–90Submit + Week 3 previewcost & reliability next
By the end of tonight

You'll walk out with…

1A named corpus and freshness policy for what the model can draw on.
2A concrete retrieval + ranking strategy, not "we use a vector DB."
3A window budget and an assembly order that respects it.
4A written Context & Retrieval Design, ready to submit as Project 1.
Your target shape

The pipeline, one more time.

flowchart LR
  Q["Query"] --> H(("Hybrid
retrieve")) H --> RR["Re-rank"] RR --> AS["Assemble
to budget"] AS --> M{"Model"} classDef s fill:#DCEBFE,stroke:#1F2937,color:#0E1726; classDef m fill:#EEE6FF,stroke:#1F2937,color:#0E1726; class RR,AS s; class M m;

Five decisions turn this generic shape into your pipeline. That's the whole deliverable.

The deliverable, decomposed

Five decisions, each defended.

1
Sources
which corpus, how fresh, who's allowed to see what
2
Chunking
the unit, size, overlap, metadata
3
Retrieval
dense, sparse, or hybrid; filters
4
Ranking
re-rank method, how many survive
5
Assembly
window budget, order, dedup, what's cut
The five decisions, in flow

Each decision is one stage of the pipeline.

flowchart LR
  S["1 Sources"] --> C["2 Chunking"]
  C --> R["3 Retrieval"]
  R --> K["4 Ranking"]
  K --> A["5 Assembly"]
  A --> M{"Model"}
  classDef d fill:#DCEBFE,stroke:#1F2937,color:#0E1726;
  classDef m fill:#EEE6FF,stroke:#1F2937,color:#0E1726;
  class S,C,R,K,A d;
  class M m;
        

Fill these five boxes and the design is done. The rest of tonight is defending each choice out loud.

Worked example · a support assistant

One corpus, five decisions.

Sourceshelp-centre articles + resolved tickets · re-index nightly · filter to the user's plan tier
Chunkingone article section per chunk, 15 percent overlap · keep title + URL as metadata
Retrievalhybrid: BM25 for error codes + dense for phrasing · top 40
Rankingcross-encoder re-rank · keep top 5 · drop anything below a score floor
Assemblybudget 3k tokens for context · highest score last (near the question) · cite sources
The five decisions, as config

This is what "decided" looks like.

# retrieval config · support assistant
sources:  help_center, resolved_tickets
filter:   plan_tier == user.plan          # access check BEFORE retrieval
retrieve: hybrid(bm25 + dense), top_k=40
rerank:   cross_encoder, keep=5, min_score=0.35
assemble: budget=3000 tokens, best_chunk_last, cite=true
fallback: if max_score < 0.35: say_no_answer()  # never invent

Six lines, every decision explicit. If your design can't collapse to something this concrete, a blank is still undecided.

Decision 1 · deeper

Sources decide freshness and safety.

Corpus
the exact set the model may draw on. Name it: which stores, which document types, what is deliberately excluded.
Freshness
how stale is too stale. Re-index cadence, and a date on every chunk so old policy can be spotted and dropped.
Access
who may retrieve what. The permission filter is a source decision, enforced at query time, never trusted to the prompt.
the tell

If you cannot answer "could this chunk ever reach a user who should not see it?", the source decision is not finished. Two of tonight's three failure modes are settled right here.

Decision 2 · deeper

Chunking is a resolution dial.

Fixed-size
N tokens with overlap. Cheap and predictable, but happily splits a sentence in half. The baseline to beat.
Structural
split on the document's own seams: headings, sections, list items, table rows, code blocks. Units stay whole.
Semantic
break where the meaning shifts. Cleaner units, higher indexing cost. Earns its keep on long prose.
Parent / late
match on a small precise chunk, then feed the model its larger parent. Precision to find, context to answer.
the trade-off

Small chunks match precisely but arrive stripped of context. Large chunks carry context but dilute the match and eat budget. Pick the unit first, then tune size against your own queries.

Decision 3 · deeper

Dense, sparse, or both.

Dense
embedding similarity. Robust to paraphrase and synonyms, weak on exact tokens like SKUs and error codes.
Sparse · BM25
keyword overlap. Nails IDs, names, and rare terms, blind to "cancel" vs "unsubscribe."
Hybrid
run both, fuse the ranks. Covers meaning and exact match at once. The safe production default.
before you rank

Metadata filters run first: plan tier, language, date, permissions. They shrink the candidate set and enforce access, so retrieval only ever scores documents this user is allowed to see.

Decision 4 · deeper

Retrieve wide, then narrow hard.

First passhybrid retrieve top 40 · fast and recall-oriented · goal: the right chunk is somewhere in here
Re-rankcross-encoder scores each candidate against the query · slow but accurate · now order actually reflects relevance
Keepsurvive the top 5 · drop anything under a score floor · a thin, high-signal set beats a fat, noisy one
lost in the middle

Models attend most to the start and end of the window and skim the middle. So fewer, better chunks win, and where you place them in assembly matters as much as which ones survive.

Decision 5 · deeper

Assembly is arithmetic, not vibes.

# window budget · support assistant
model_window:   16000 tokens
reserve:        system 400 + question 200 + answer 1200
context_budget: 3000 tokens        # the rest is headroom, not fuel

order:   best_chunk_last          # nearest the question, beats lost-in-the-middle
dedup:   drop near-identical chunks before counting tokens
cut:     lowest-ranked first until under_budget
cite:    keep source + url on every chunk that survives
the discipline

Decide the number, then everything else follows: order for attention, dedup so you do not pay twice, and a rule for what gets cut when it overflows. "Stuff in as much as fits" is not a budget.

When the basics are not enough

Levers to reach for later.

+
Query rewriting
expand, split, or clarify the question before you retrieve. One vague query becomes several sharp ones.
+
HyDE
retrieve against a hypothetical answer, not the raw question. Closes the gap between how questions and docs are written.
+
Contextual retrieval
prepend a one-line "where this came from" to each chunk before embedding. A lone chunk stops being orphaned.
+
Agentic retrieval
let the model retrieve, read, and retrieve again. Power for multi-hop questions, at real cost and latency.
order of operations

Ship the five-decision baseline first and measure it. Add these only where a metric tells you the baseline is losing. Complexity you cannot measure is complexity you cannot defend.

Your success signal, made concrete

Four numbers that tell you it works.

Recall@k
did the right chunk survive the first pass at all? If not, no re-rank or prompt can save the answer.
Context precision
of what you kept, how much was actually relevant? High precision keeps the window clean and cheap.
Faithfulness
is every claim in the answer supported by retrieved context? This is your hallucination gauge.
Answer relevance
did the response actually address the question, not just cite something nearby?
tonight's ask

Your "success signal" in the template is one of these, chosen and named. You do not measure it tonight, you commit to it. Week 5 turns the chosen number into a real eval harness.

See it end to end · the whole system

RAG is two pipelines, not one.

flowchart LR
  subgraph IDX["Indexing: offline, scheduled"]
    direction LR
    D["Docs"] --> CH["Chunk"] --> EM["Embed"] --> VS[("Vector
store")] end subgraph QRY["Query: online, per request"] direction LR Q["Question"] --> QE["Embed"] --> RT["Retrieve"] --> RK["Re-rank"] --> AS["Assemble"] --> G{"LLM"} --> AN["Answer"] end VS -.-> RT classDef s fill:#DCEBFE,stroke:#1F2937,color:#0E1726; classDef m fill:#EEE6FF,stroke:#1F2937,color:#0E1726; class CH,EM,RT,RK,AS s; class G m;

The index is built offline and reused. Only the bottom row runs per question. The dotted line is the only place the two phases meet: retrieval reads what indexing wrote.

Phase 1 · offline indexing

Build the index once.

// ingest.ts: runs offline, on a schedule (nightly re-index)
import { embed, chunk } from "./common/llm";
import { vectors } from "./common/store";

for (const doc of await loadDocs()) {          // help articles + tickets
  for (const part of chunk(doc, { unit: "section", overlap: 0.15 })) {
    const vector = await embed(part.text);          // one vector per chunk
    await vectors.upsert({
      id: part.id,
      values: vector,
      metadata: { url: doc.url, planTier: doc.planTier, updatedAt: doc.updatedAt },
    });
  }
}

Chunk, embed, store, with the metadata that later powers access filters and freshness. Run this on a schedule, never per request.

Phase 2 · online, per question

The five decisions, in code.

// answer.ts: runs online, once per user question
export async function answer(q: string, user: User) {
  const hits = await vectors.search(await embed(q), {
    topK: 40,
    filter: { planTier: user.planTier },       // access BEFORE retrieval
  });
  const ranked = await rerank(q, hits);            // cross-encoder
  const kept = ranked.filter(h => h.score >= 0.35).slice(0, 5);
  if (kept.length === 0) return "No confident answer.";   // never invent

  const context = assemble(kept, { budget: 3000, order: "best-last" });
  return complete({
    system: "Answer only from context. Cite sources.",
    user: q,
    context,
  });
}

Retrieve, rank, assemble, generate. Every one of tonight's five decisions is a literal argument here: topK, filter, the score floor, the budget, the order.

Real world · one question, traced

"Why was I charged twice?" end to end.

sequenceDiagram
  actor U as User
  participant A as App
  participant V as Vector store
  participant R as Reranker
  participant L as LLM
  U->>A: Why was I charged twice
  A->>A: check plan tier and permissions
  A->>V: embed query, search top 40
  V-->>A: 40 candidate chunks
  A->>R: re-rank against the query
  R-->>A: keep top 5 above the floor
  A->>L: question plus 5 chunks plus cite rule
  L-->>A: grounded answer with sources
  A-->>U: answer with citations
        

Same corpus, same config, one real request. If any hop is undefined in your design, that is the blank you go back and fill.

Build yours · in order

The pipeline drill.

1Name the sources and the access rule.What's the corpus, how fresh must it be, and who is allowed to retrieve what? Security starts here.
2Choose the chunk unit and metadata.What is one retrievable piece, and what must travel with it (source, date, permissions)?
3Pick retrieval + ranking.Hybrid or not, filters, first-pass size, re-rank method, how many survive.
4Set the window budget and order.Tokens for context, assembly order, dedup, and what you cut when it overflows.
5State how you'll know it works.One line: the metric that tells you retrieval is helping (feeds Week 5 evals).
Project 1 · fill this in

Context & Retrieval Design.

Sources & freshness
Corpus: · updated · access rule:
Chunking
Unit: · size / overlap: · metadata:
Retrieval
Strategy: · filters: · first-pass k:
Ranking
Re-rank by: · keep: · score floor:
Assembly & budget
Context budget: tokens · order: · cut first:
Success signal
I'll know retrieval helps when
Design against these

Three ways retrieval betrays you.

1
Ungrounded answer
nothing relevant retrieved, so the model invents. Fix: a "no good context → say so" path.
2
Stale context
retrieved the old policy. Fix: freshness in the source decision, dates in metadata.
3
Leaked context
retrieved a doc this user can't see. Fix: permission filter before retrieval, not after.
Calibrate · the rubric

Strong design vs vague design.

Strong

Every decision names a specific choice and a reason. A reader could build it. Failure paths are explicit.

Vague

"We'll use RAG with a vector database." No chunk unit, no ranking, no budget, no failure handling.

Strong

Access and freshness are decided at the source, so security and staleness are handled by design.

Vague

Retrieval grabs everything, filters permissions later (or never). One prompt away from a leak.

How to submit

Project 1, due Sun Jul 26.

One to two pages, or the filled template plus your pipeline diagram. It doesn't need to be built, it needs to be decided and defensible. This design is the backbone your later artifacts extend.

Include
the five decisions, the diagram, and your one success signal.
Skip
code, benchmarks, tooling brand-names. This is the design, not the build.
Bring back
it plugs straight into Week 5's eval harness. Keep it handy.
Recap · then Week 3

You have a pipeline you can defend.

Five decisions, each with a reason: sources, chunking, retrieval, ranking, assembly. Plus failure paths for ungrounded, stale, and leaked context. That's Project 1.

Tuesday · S5
Cost, latency & reliability: budgets, routing, and designing for failure. The densest week.
Tue 28 Jul
Submit Project 1
Context & Retrieval Design, by Sunday. We build on it all course.
due Jul 26