Notes
The problem
Here is the problem. The model never saw your private documents. Its knowledge stops at its training date. And when it does not know, it often fills the gap with something plausible, which is the definition of a hallucination. It also cannot point to a source. You could retrain the model every time a policy changes, but that is slow and expensive. There is a much simpler fix.
- • never saw your private docs
- • knowledge frozen at training
- • fills gaps with plausible
- guesses (hallucination)
- • cannot cite a source
- retraining for every policy
- change is slow and costly
The idea
The idea is an open book exam. Take the question, search your own documents for the most relevant passages, put those passages into the prompt, and ask the model to answer only from them and cite which one it used. Update a document and the very next answer reflects it, with no training at all. Two halves, then: an offline pipeline that prepares documents for search, and an online path that answers questions.
- 1 take the question
- 2 search your documents
- 3 put the best passages
- into the prompt
- 4 ask the model to answer
- only from them, and cite
- update a doc → the next
- answer changes. No training.
Ingest
First the offline half, the ingest pipeline. Load documents from wherever they live. Clean them, stripping navigation menus and footers, fixing tables and keeping headings. Split them into chunks. Turn each chunk into a vector with an embedding model. Then store the vector, the text and metadata such as source, date and who may read it. Run this again whenever a document changes, ideally only for the changed parts.
- load: PDFs, pages, tickets
- clean: strip menus, footers,
- fix tables, keep headings
- chunk: split into passages
- embed: one vector per chunk
- index: vectors + text +
- metadata (source, date,
- team, access list)
Chunking
Chunking matters more than people expect. A chunk should hold one idea. Too big and its vector becomes a blurry average that matches nothing well. Too small and the passage lacks the context to answer. Typical sizes are a few hundred tokens, with a little overlap so a sentence cut at a border is not lost. Split on the document's own structure first, its headings and sections, and prefix each chunk with its title so it makes sense on its own.
- size: ~200-800 tokens
- overlap: ~10-20% so ideas
- cut at a border survive
- split on structure first:
- headings, sections, rows
- prefix each chunk with its
- title and heading path
- too big: blurry matches
- too small: missing context
Embeddings
An embedding model turns a piece of text into a list of numbers, a vector, usually a few hundred to a few thousand long. Texts with similar meaning land close together, measured by cosine similarity. So cancel my plan sits near end my subscription, even though they share no words. That is what makes semantic search work. One rule: embed documents and queries with the same model, and re-embed everything if you switch models.
- text → vector of ~384 to
- 3,072 numbers
- similar meaning → nearby
- vectors (cosine similarity)
- "cancel my plan" is close to
- "end subscription" even with
- no shared words
- same model for docs + queries
Vector search
At query time, embed the question and find the nearest chunk vectors. Comparing against every vector is too slow at millions of chunks, so vector indexes use approximate nearest neighbour structures, like H N S W graphs, that find almost all the true neighbours in a few milliseconds. You do not need an exotic database. Postgres with the pgvector extension handles millions of vectors, and most search engines now support vectors too.
- exact search compares every
- vector: too slow at millions
- ANN index (HNSW, IVF):
- ~95-99% recall, ms latency
- options: pgvector in
- Postgres, a search engine,
- or a dedicated vector DB
- filter by metadata too
Hybrid search
Vectors are great at meaning but weak at exact strings: error codes, product names, part numbers. Classic keyword search, such as B M twenty five, is the opposite. So run both and merge the results. A simple, robust merge is reciprocal rank fusion, where each document scores by how high it ranks in each list. Hybrid search almost always beats either one alone, and it is cheap to add.
- vectors: good at meaning,
- weak at exact codes, names,
- part numbers, rare words
- keywords (BM25): the reverse
- run both, merge the lists
- (reciprocal rank fusion:
- score = sum of 1/(60+rank))
- hybrid usually beats either
Reranking
Search returns candidates, not answers. A reranker improves the order. Stage one is fast and wide, hybrid search for the top fifty. Stage two is a cross encoder model that reads the question and each candidate together and scores how well that passage answers it. Keep the top three to eight. Reranking adds maybe a tenth of a second, and it is often the single biggest quality improvement you can make.
- stage 1: fast, wide
- hybrid search → top 50
- stage 2: slow, precise
- cross-encoder scores each
- (question, chunk) pair
- → keep top 3-8
- adds ~50-200 ms
- often the biggest quality win
The prompt
Now build the prompt. Tell the model to answer only from the sources, to cite them by number, and to say so when the sources do not contain the answer. Then list the passages, each with an id, a title and a date, so it can tell an old policy from a new one. Put the question last. Citations let users check the answer and let you check the model. Permission to say I do not know is what cuts hallucinations most.
Answer using ONLY the sources.
Cite like [2]. If the sources
do not answer it, say so.
[1] Refunds (EU), 2026-08:
"...within 14 days..."
[2] Refunds (US), 2026-05:
"...within 30 days..."
Question: EU refund window? Better queries
The question as typed is often a poor search query. A follow up like and for the E U means nothing alone, so have a model rewrite it into a standalone query using the conversation. In an agent, go further and make search a tool. The agent can search, read the results, notice something missing, refine the query and search again before answering. That handles multi part questions a single lookup would miss.
- follow-up: "and for the EU?"
- → rewrite with history:
- "refund window for EU
- customers"
- agentic RAG: search is a
- tool; the agent may search,
- read, refine and search
- again before answering
Freshness and access
Two things keep RAG correct over time. Freshness: re-ingest when a document changes, triggered by change events rather than a weekly batch, and delete chunks when the source is deleted, or the model will happily quote a retired policy. And access control: store who may read each chunk and filter at query time by the asking user. Never retrieve restricted text and hope the prompt keeps it hidden.
- freshness: re-ingest on
- change events, not weekly
- deletes: remove chunks when
- the source is removed
- access: store who may read
- each chunk; filter at query
- time by the asking user
- never rely on the prompt
- to hide restricted text
Measuring it
Measure the two halves separately, or you will not know what to fix. For retrieval, build a set of questions with the passages that answer them, and measure recall at k: is the right chunk in the top five? For generation, measure faithfulness, meaning every claim is supported by the retrieved sources, along with answer relevance and whether the citations are correct. A model can grade these at scale once you have checked it against human labels.
- retrieval (needs labels):
- recall at k: is the right
- chunk in the top 5?
- MRR: how high does it rank?
- generation:
- faithfulness: claims backed
- by the sources?
- answer relevance, citation
- accuracy
Failure modes
When answers are wrong, look at retrieval first. The right document was a scanned P D F that never got its text extracted. The right chunk was found but ranked ninth. An old and a new version of a policy were both retrieved. The answer was spread over ten chunks. Or the model ignored the sources and answered from memory. Log the retrieved chunks with every answer, so you can tell these apart quickly.
- • right doc never chunked
- (scanned PDF, table lost)
- • right chunk ranked 9th
- • stale and new versions
- both retrieved
- • answer spread over many
- chunks
- • model ignores sources and
- uses its own memory
RAG or not
When should you use RAG? When the knowledge is large, changes often, or needs citations and per user access control. If the whole source fits comfortably in the context window, like one contract, just paste it in. Fine tuning is good for teaching a style, a format or a narrow skill, but it is a poor way to add facts and cannot be updated quickly. Most products start with RAG and a well written prompt.
- RAG: large or changing
- knowledge, need citations
- and access control
- long context: one or a few
- docs that fit; just paste
- fine-tuning: style, format,
- narrow skills; not a good
- way to add facts
- often: RAG + a good prompt
Say it like this
How to say it. Chunk by heading at around five hundred tokens with the title on each chunk. Hybrid keyword and vector search for the top fifty, then a cross encoder reranks to five. The prompt says answer only from the sources, cite them, and say when they do not cover it. Results are filtered by the user's permissions. We track recall at five and faithfulness on a labelled set before every release.
- "Chunk by heading, ~500
- tokens. Hybrid search, top
- 50, cross-encoder rerank to
- 5. Prompt says answer only
- from sources and cite. ACL
- filter per user. We track
- recall@5 and faithfulness on
- a labelled set each release."