AI Garden

RAG and fine-tuning architectures — from vanilla RAG to agentic RAG, with the measurement.

The whiteboard needs a wider screen — here are the notes in order.

schema

Roadmap

This board reads left to right as one argument: problem → cheapest fix → cheap wins → expensive wins → measurement.

Rendering diagram…

One rule

Only climb a step once you have measured the one below it. Jumping to agentic RAG without ever trying a different chunk size is putting the most expensive fix on top of the cheapest problem.

The default order: Prompt → RAG → Advanced RAG → Agentic → Fine-tune → Distill. Every step sits on the measured version of the one before it.

#rag#roadmap
note

Why RAG? Hallucination

Everything a model knows is frozen in its training data. So it cannot do three things at once: be current, know your domain, cite a source.

Rendering diagram…
Not currentknowledge frozen at training time
Out of domainnever saw your documents
No sourcescannot say where it came from
Confident anywaythe tone never changes

RAG in one sentence: put what the model does not know in front of it before it answers. Retrieval-augmented generation — retrieve first, then generate.

Fine-tuning is also an answer, just not the first one: every new fact means labelling, cleaning and GPU time again. For a small corpus that is overkill.

#rag#hallucination
schema

RAG or fine-tune?

The shortest split: RAG is for knowledge, fine-tuning is for form. Baking facts that change weekly into weights means retraining every week.

RAGFine-tune
Knowledge livesoutside the modelin the weights
Updating iteasy — reindexhard — retrain
Inference costhigher, context is fulllower, prompt is short
Context limityes, the windownone
Citing sourcesnaturalimpossible
Good atfacts, fresh datatone, schema, behaviour
Rendering diagram…
#rag#fine-tuning#decision
schema

The vanilla RAG pipeline

Vanilla RAG is two separate pipelines, and not confusing them is half the job: one runs offline and builds the index, the other runs on every request.

Rendering diagram…
two tracks, two contracts
Sourcedocs, tickets
Chunksplit + overlap
Embedvector
Storeindex
Query
Retrievetop-k
Promptcontext + question
Answerwith sources

Everything vanilla does not have

No router, no multi-step planning, no feedback. The model retrieves once and answers once. The real work is setting the contract between retrieval and generation: what you hand the model, and what you expect back.

#rag#pipeline
snippet

The reference baseline stack

The reference baseline — a working, plain, deliberately unclever vanilla RAG service. This is the zero point every measurement is compared against.

API & orchestrationTypeScript + Express
RAG integrationLangChain
Vector storeChroma, local
Embedding & generationOllama, temperature 0
ConfigurationZod + YAML
one process, three dependencies
one Node processHTTPq, k (Zod)similaritySearchembed · chatClientcurl · eval harnessExpressPOST /rag/queryLangChainretrieval chainChromavector store · localOllamaembed + chat · t=0Backend2Database1External1Model1

Two tracks

track 1 — offline index build
Read JSON
FilterpdfText
TextSplitter
Metadata
Write to Chroma
track 2 — POST /rag/query
Zod validate
similaritySearchq, k
Format chunks
ChatOllama
JSON response

temperature = 0 is not an accident: during evals the variance in an answer should come from retrieval quality, not from sampling.

#langchain#chroma#ollama
note

Where naive RAG breaks

When naive RAG breaks it looks like a model bug from the outside. It is almost always a retrieval bug.

Low precisionnot every retrieved chunk is relevant
Low recallnot every relevant chunk is retrieved
Stale datathe index drifted from the source
One shotno chance to correct a bad retrieval

Two different diseases

  • Low precision fills the context with noise. The model may see the right chunk and still lose it in the middle — lost in the middle. The answer comes out close but wrong.
  • Low recall means the model never sees the material to synthesise from. The answer is either incomplete or invented.

More retrieved tokens does not mean better performance. Raising top-k usually costs precision and makes the answer worse.

#rag#retrieval#failure
note

Chunking: boundary > size

Tuning chunk size is the cheapest measurable win — but the thing that matters is not size, it is the boundary. A 512-token chunk cut through the middle of an idea is worse than a clean 1024.

ChunkGainLoss
Small (128–256)high precisioncontext is severed
Medium (512–1024)the balance for most corpora
Large (2000+)context intactblurred embedding, noise

Try in this order

  1. Split on headings, not on a fixed token count. Carry the heading chain into every chunk.
  2. 10–15% overlap. Keep the sentence that lands on a boundary on both sides.
  3. Contextual retrieval. Prepend a 50–100 token line — generated by a cheap model — saying where in which document this piece sits, then embed that.

There is no single global chunking strategy. A contract, a table and a chat log do not survive the same splitter — the real work is classifying document type at ingest.

#chunking#ingest
schema

Metadata filtering

Metadata is context you can inject into every chunk: year, document title, page, section, owner. Raw semantic search confuses these at low precision.

Rendering diagram…

The 2021 inside the question is not a meaning, it is a filter. Vector similarity sees the 2020 and the 2021 document as nearly identical.

Rendering diagram…

What to store

Structuralyear, page, section, doc type
Derivedchunk summary, summary of neighbours
Reverse HyDEwhich questions this chunk answers
#metadata#precision
note

Hybrid search and reranking

Dense vectors capture meaning and miss exact matches: error codes, SKUs, person names, version numbers. BM25 is good at exactly those.

retrieve → fuse → rank → cut
Densetop-50
BM25top-50
RRFfuse
Rerankcross-encoder
Top-5

Why this order

  • Retrieve wide, hand over narrow. Fetching top-50 and cutting to top-5 is both cheaper and better than dumping a raw top-20 into the context.
  • RRF fuses two lists without any attempt to normalise their scores; it adds up ranks instead.
  • A cross-encoder reads the question and the chunk together. It catches the relationship a bi-encoder misses — at the cost of latency, which is why it comes last and over few candidates.

Ask for chunk ids in the answer. A RAG system that cites nothing does not remove hallucination — it just makes it invisible.

#bm25#rerank#hybrid
open question

Chunk, parent, or summary?

Still unresolved for me: where the retrieval unit should sit relative to the embedding unit.

Embed chunkcheap, loses context
Embed summarybetter recall, more storage

Options I keep circling:

  1. Embed the chunk, return the chunk. Simple, and what everyone starts with. Breaks on documents where meaning lives across sections.
  2. Embed the chunk, return the parent. Small vectors, wide context. Costs context budget fast.
  3. Embed a generated summary, return the original. Retrieval quality goes up; you now own a generation step in your ingest pipeline that can drift from the source.

Suspicion: the right answer is document-type dependent, which means the real work is classifying documents at ingest, not tuning one global strategy.

#rag#embeddings
schema

Small-to-big retrieval

Intuition: embedding a big block of text is a bad idea. The vector blurs and the one sentence the question asks about drowns in noise.

The fix: embed the small thing, hand the big thing to the answer.

three variants
Embed sentence
Match
Expand window k=2
Wide context to LLM
Embed child chunk
Match
Fetch parent chunk
Synthesise from parent
Embed summary/metadata
Match
Fetch the original
Synthesise from original

The measured difference

Retrieverhit rateMRR
Base (plain chunk)0.7960.605
Chunk references0.8920.740
Metadata references0.9160.747

Source: LlamaIndex advanced-retrieval benchmarks. The absolute numbers move with the corpus; the ordering usually holds.

#retrieval#llamaindex
note

Query transforms

The sentence a user types is not a sentence written for search. Touching the query before retrieval is the biggest win available without touching the index.

Rewriteresolve the conversation: "what about that one?"
HyDEgenerate a hypothetical answer, embed that
Multi-querysearch three phrasings, fuse the results
Decompositionsplit a multi-hop question into sub-questions
Routingwhich source: vectors, SQL, the web
Rendering diagram…

Every transform is another LLM call — latency plus tokens. Measure first: how many of your queries are actually ambiguous? If most are one-shot, put this behind a router.

#hyde#query-rewriting
schema

Close the retrieval loop

Vanilla RAG is one shot: if it retrieved the wrong thing, it cannot recover. What actually raises quality is turning retrieval into a closed loop.

Rendering diagram…

Three checks that close it

  • Relevance check. Are the retrieved chunks about the question? If not, search again before generating. This is the core of Corrective RAG.
  • Groundedness check. Is every claim in the answer present in the shown context? If not, narrow it or say "I do not know".
  • A stopping condition. At most N rounds. Otherwise the loop runs forever and you find out at the end of the month.
#crag#retrieval#loop
schema

The agentic RAG loop

Agentic RAG is not a fixed "retrieve and generate" pipeline; it is an orchestrator in which the model runs its own search process.

Rendering diagram…

How it differs from traditional RAG

Traditional (reactive)Agentic (proactive)
Flowone way, deterministiccyclical, dynamic
Searchesoneas many as needed
Data qualitynever questionedevaluated
Missing infogoes unnoticedtriggers a new strategy

It does not work without a solid middle column. Without metadata, hybrid search and reranking, the agentic layer only makes bad retrieval expensive.

#agentic#rag
schema

Agentic design patterns

Agentic RAG is not one architecture, it is a family of patterns. Do not build them all — add one at a time, knowing which failure it fixes.

Routerread the intent, send it to the right source
Adaptivescore difficulty: easy → LLM, hard → agent
Corrective (CRAG)bad retrieval → search the web or rewrite
Self-RAGthe model decides whether to retrieve at all
Multi-agentplanner, researcher, synthesiser, validator
Rendering diagram…

Which one for which pain

  • Questions go to different sources → Router.
  • Most questions are easy, a few are hard → Adaptive (you pay for agentic quality only on the hard ones).
  • Retrieval sometimes comes back empty → Corrective.
  • Answers need several documents combined → Multi-agent.
#router#crag#multi-agent
note

Vanilla vs agentic: the cost

Agentic RAG wins on accuracy and costs real engineering effort in latency, tokens and failure surface. The decision is that trade.

DimensionVanilla RAGAgentic RAG
Latencylow, ~1–2 shigh and variable
Token costlow, one queryhigh, history re-read each round
Complexitya simple pipelinestate management + tool wiring
Accuracy on hard taskslow, context breakshigh, cross-synthesis
Failure surfacenarrow: search and generatewide: planning, tools, loops
Vanilla — typical request1 LLM calls
Agentic — typical request6 LLM calls

If you cannot measure the accuracy gain, the latency and tokens you pay are real and the gain is an assumption.

#cost#latency
schema

An agent is a loop

An agent is a loop with a stopping condition. Most of the engineering is in the stopping condition.

Rendering diagram…

Where loops go wrong

No budgetruns until the bill notices
Silent failuretool errors swallowed, agent guesses
Tool soup40 tools, none described well
No memory of failureretries the same call forever

What to give it instead

  • A hard step budget and a token budget, both surfaced to the model.
  • Tool errors returned as text the model can read and act on, never hidden.
  • Fewer, wider tools with honest descriptions of when not to use them.
  • A scratchpad it can write to, so step N+1 can see what step N learned.
#agents#tools
note

Context is a budget

A large context window is a budget, not a bucket. Everything you put in it competes for the model's attention and for your latency target.

System prompt + tools4,000 tok
Retrieved chunks12,000 tok
Conversation history9,000 tok
Headroom for the answer7,000 tok

Rules I keep coming back to

  • Put the stable part first — system prompt, tool definitions, long reference documents. Stable prefixes are what prompt caching can reuse.
  • Put the volatile part last — the user turn, freshly retrieved chunks. A single edit near the top invalidates the cache for everything after it.
  • Summarise history on a threshold, not every turn. Rolling summaries that rewrite themselves each turn destroy the cache and drift.

Measure before trimming. Half the "context is too big" problems are really one tool returning an unpaginated JSON blob.

#context#caching#cost
schema

When to fine-tune

Fine-tuning is for form, not for facts. Burning knowledge that changes weekly into weights means retraining on every change.

Rendering diagram…

The two legitimate jobs

  1. Distillation. Move a strong model's behaviour into a small, cheap, fast one. What you gain is cost and latency.
  2. Locking in the residue. The tone, output schema and refusal patterns prompting cannot hold. The long tail that never reaches 100%.

Do not break the order: Prompt → RAG → Fine-tune → Distill. Reaching for a fine-tune before measuring RAG is renting GPUs without knowing the problem.

#fine-tuning#decision
note

Fine-tuning methods

Training from scratch is almost no product team's job. In practice the choice is which parameter-efficient method.

MethodWhat it doesWhen
Full SFTupdates every weightrarely — costly, forgetful
LoRAlow-rank adapter on a frozen modelthe default starting point
QLoRALoRA over a 4-bit basefitting onto one GPU
DPOaligns on preference pairsyou have good/bad answer pairs
GRPORL against a reward functioncorrectness is programmatically checkable
Distillationcopies a big model into a small onecost and latency pressure
the practical path
Curate200-500 examples
QLoRA SFT
DPOif you have pairs
Evalbefore promoting

An adapter does not replace retrieval. A fine-tuned model still invents the document it never saw — just in a nicer format.

#lora#qlora#dpo
experiment

Fine-tuning the embedding

The least-discussed way to improve retrieval: fine-tune the embedding, not the model. On a corpus full of domain jargon this is the cheapest win there is.

a training set out of unlabelled data
Raw chunk
Ask an LLMwhat question does this answer
(question, chunk) pair
Train the embedding

A general-purpose embedding model has never seen your company's acronyms, part numbers or internal vocabulary. Synthetic question generation closes that gap without a single hand-written label.

Why it works

  • The training pairs come from your corpus, not from a public benchmark.
  • Improving the retriever improves every query in the pipeline — unlike a prompt change, it compounds.
  • The same synthetic set is reusable as ground truth for retrieval evals.
#embeddings#synthetic-data
experiment

Retrieval eval, in isolation

Before measuring RAG end to end, measure it in isolation: for a given question, were the retrieved chunks the right ones? Answer quality depends on that answer.

Rendering diagram…

The dataset

Input is a question, output is the ground-truth document ids that answer it. Write 20 by hand, then grow it by generating synthetic questions from chunks.

MetricWhat it tells you
Hit rateis the right chunk in the top-k at all
recall@khow many of the relevant chunks came back
MRRhow high the first correct result sits
NDCGis the order right too — the metric for reranking

Doing RAG without a benchmark is turning knobs without knowing which change improved what.

#eval#mrr#ndcg
experiment

End-to-end eval

Isolated evals audit retrieval; end-to-end evals audit the answer the user sees. Both are needed, neither substitutes for the other.

the e2e harness
Question
RAG pipeline
Answer + context
Evaluator
MetricThe question it asks
Faithfulnessis every claim present in the shown context
Answer relevancydoes the answer address the question
Context precisionhow much of the retrieved context was needed
Context recallhow much of the needed context arrived

Two modes

  • Label-free. No reference answer: faithfulness, relevancy, tone, toxicity. Cheap enough to run on every commit.
  • Labelled. Ground-truth answers exist: correctness, coverage. Expensive to write, and the sharpest catcher of regressions.
#eval#faithfulness#ragas
experiment

Evals before prompts

Prompt tuning without evals is just vibes with extra steps. The eval does not need to be sophisticated; it needs to exist before the prompt changes.

the smallest useful harness
20 casesreal inputs
Runfixed seed
Graderubric or assert
Diffvs last run

Starting set

KindCountWhat it catches
Golden10regressions on the happy path
Adversarial5prompt injection, refusal bait
Boring5empty input, huge input, wrong language

Grading with a model

Use one when the answer is genuinely open-ended, and then:

  • Grade one dimension at a time. A single "is this good" score is noise.
  • Give the judge the rubric and a reference answer.
  • Track judge/human agreement on a sample. A judge you have never audited is a random number generator with good manners.