What is Retrieval-Augmented Generation (RAG)?
Retrieval-Augmented Generation (RAG) is an AI design pattern for LLM apps and agents, in the Context and knowledge group. Fetch relevant passages and hand them to the model along with the question.
The HR team at Lucia's company launched an assistant to answer employee questions. It answered confidently about vacation, expense reimbursement and remote work.
The problem was where those answers came from. Not the company's policy. Just what the model knew in general.
An employee asked how many remote days per week they could take. The bot said three, which is common in the market. The company's policy said two. And the policy had changed in March. The model didn't even know the old version.
The first fix was pasting the whole handbook into the prompt. It worked for a while, until the handbook reached three hundred pages. Every question got slow and expensive, and the bot started mixing up the reimbursement rule with the travel rule.
And this kind of error has consequences outside engineering. A Canadian tribunal ruled that an airline is responsible for what its chatbot says, after the bot described a refund rule that wasn't the real policy. In this lesson, you'll learn how to make the model answer from the right, current information, without sending everything on every call.
The way out is to separate knowledge from the model. Your documents live in a search index, split into chunks. On every question, the system retrieves the few most relevant chunks, and builds the model's input from them, each one tagged with an identifier.
Then the prompt asks for three things.
Answer only from the passages.
Cite where each claim came from.
And say not found, when the answer isn't there.
It's an open book exam. Before answering, the student opens the book to the right page. Retrieval decides what the model reads. The model decides how to answer. And when the remote work policy changes again, someone updates the index, and the next answer is already right. Nobody touches the prompt.
Let's follow one question. It arrives from the user.
Retrieval looks up the chunks closest to the question in the index. By embeddings, by keywords, or both.
Your code builds the context. Each passage gets an identifier and its source.
The model answers only from those passages, and cites the identifiers it used.
Then your code checks that every cited identifier was actually in the context it sent.
And the answer goes out with sources. Or with an honest not found.
The index itself is built beforehand, in a separate process. Load the documents, split them into chunks, generate embeddings, and store them. And that process runs again whenever a document changes.
Back to the employee's question. How many remote days per week can I take? Retrieval returns the closest chunks. At the top, section two of the remote work policy: starting March, remote work is limited to two days a week.
The answer comes back structured. Up to two days per week, in effect since March. Source: section two of the policy. Your code checks that this source was among the passages it sent. It was, so the answer goes out with a link to the policy.
Now the math, with illustrative numbers. Three hundred pages at about five hundred tokens each is around one hundred fifty thousand tokens, on every call.
With retrieval, five chunks of four hundred tokens is two thousand tokens of context.
At a thousand questions a day, that's one hundred fifty million input tokens, versus two million.
Here's one for you. The model answered citing benefits section nine. But that passage was not among the five you sent. It does exist in the index. What should the system do?
Treat it as an answer with no valid source, and fall back to not found. Existing in the index isn't enough. The model didn't read that passage on this call. So it either filled in from memory, or invented the reference.
And asking again with twenty passages instead of five won't fix it. The problem isn't missing context. It's an answer going beyond the context. That check in code is cheap, and it's what keeps the citations honest.
Retrieval isn't always the answer. If your base fits comfortably in the context, sending all of it, with prompt caching, is simpler, and avoids retrieval errors. Building search for ten pages is its own smell.
If the question asks for an exact value from a system, like how much do I still owe on my loan, that's a query, not a search. Similarity returns similar passages, not a freshly computed balance. Give the model a tool that asks the bank's system. Retrieval stays for written policies and rules.
And if the question needs reasoning over the whole base, like how many contracts expire this year, retrieval brings back some passages, never all of them. That calls for a structured query, or batch processing.
Now the common mistakes. Cutting the text every thousand characters. The rule up to two days per week lands half in one chunk and half in the next. Neither answers the question alone. Split by structure. Section, article, clause.
Chunks without a heading. A passage that says the limit is two days doesn't say which policy it belongs to. Put the section title in front.
Two versions of the same policy in the index. The old one and the new one compete in retrieval. Tag the effective date, and filter out what's been replaced.
And tuning the prompt when the error is in retrieval. If the right passage never arrived, no instruction can save the answer. Look at the retrieved passages in the trace first.
Evaluate retrieval and answering separately. Most problems here are retrieval problems.
For retrieval, ask: for each question, did the right passage show up in the top five?
For faithfulness: is every claim in the answer supported by a cited passage?
And for abstention: when the answer isn't in the base, like does the company pay for a gym, does the system say it doesn't know?
The split is what makes diagnosis possible. Say your eval shows the right passage arrives fifty-eight percent of the time, and faithfulness is ninety-seven percent. The team wants a bigger model. But the numbers say the model is fine. When the passage arrives, it sticks to it. In four out of ten questions, it just never receives the answer. Work on chunking, hybrid search, or rewriting the query, before you touch the model.
And security. Every indexed document becomes text the model reads. If someone can put a document in the base, they can put instructions in front of the model.
So control who writes to the index. A wiki anyone can edit is a way in.
Enforce permissions at retrieval time. An intern must never receive passages from the board's folder. Filter before you retrieve, not after you answer.
And treat retrieved passages as data, not as orders.
On the page, every version does the same four things. Retrieve the closest passages. Build the context, with an identifier on each one.
Ask for a structured answer, with the text, the sources, and a found flag.
And check, in code, that every cited source was really sent. If not, the answer is not found.
Python and TypeScript call the provider directly, with a small local index for testing. LangChain lets you swap the model that answers. But in all of them, retrieval stays yours. It decides what the model is allowed to cite.
So here's the idea to take with you. Don't ask the model to remember your company. Hand it the right page, ask it to cite what it used, and check the citation in code. And when the page isn't there, not found is a correct answer.
In the next lesson, we'll turn to the first smell of the track, the one Lucia's team almost built: prompt bloat. See you there.
Geração Aumentada por Busca (RAG)
Retrieval-Augmented Generation (RAG) is an AI design pattern for LLM apps and agents, in the Context and knowledge group. Fetch relevant passages and hand them to the model along with the question.
Before answering, the system retrieves the most relevant passages from your knowledge base and hands them to the model along with the question, asking it to answer only from them. RAG (retrieval-augmented generation) is for assistants that must answer about your own documents, which the model has never seen or which change often.
An open-book exam: before answering, the student opens the book to the right page.
The HR team at Lucia’s company launched an assistant to answer employee questions. It answered confidently about vacation, expense reimbursement, and remote work. The problem: it answered from what the model “knew” in general, not from the company’s policy.
An employee asked how many remote days per week they could take. The bot said three, which is common in the market. Company policy said two. The policy had changed in March, and the model did not even know the old version.
The first attempt was pasting the whole handbook into the system prompt. It worked for a while, until the handbook reached 300 pages. Every question got slow and expensive, and the bot started mixing up the reimbursement rule with the travel rule.
This kind of error has consequences outside the engineering team. In Moffatt v. Air Canada (2024), the airline’s chatbot told a customer he could claim the bereavement fare after traveling. The policy published on the website said otherwise. British Columbia’s Civil Resolution Tribunal ruled that the company is responsible for what its bot says and ordered it to pay the difference.
The question this pattern answers: how do you get the model to answer with the right, current information without sending everything on every call?
Separate knowledge from the model. Documents live in a search index, split into chunks. On every question, the system retrieves the few most relevant chunks and builds the model’s input from them, tagged with identifiers.
The prompt asks for three things: answer only from the passages, cite where each claim came from, and say “not found” when the answer is not there. Retrieval decides what the model reads; the model decides how to answer.
Updating knowledge becomes updating the index, without touching the prompt. When the remote work policy changes again, someone reindexes the document and the next answer is already right.
The name comes from Lewis et al. (2020), who combined a retriever with a text generator. The version you will build today is simpler: an ordinary index, an off-the-shelf model, and a well-written prompt.
flowchart LR
Q(["1. Question"]):::io --> S["2. Retrieve passages"]:::tool
IDX[("Document index")]:::store --> S
S --> C["3. Build context with IDs"]:::tool
C --> M(["4. Answer with citations"]):::llm
M --> V{"5. Cited valid passages?"}
V -->|"yes"| OUT(["6. Answer with sources"]):::io
V -->|"no"| N(["6. Not found"]):::io
The index is built beforehand, in a separate process: load documents, split into chunks, generate embeddings, and store them. That process runs again whenever a document changes.
Follow the employee’s question from start to finish. It arrives like this: “How many remote days per week can I take?”.
Retrieval returns the five closest chunks. Here are the top three, with their similarity scores:
remote-policy#2 0.82 "Starting March 2026, remote work is limited to two days per week..."
remote-policy#1 0.74 "This policy applies to full-time employees in every department..."
benefits#7 0.61 "The home office allowance is $120 per month, paid with payroll..."
Code builds the context with the IDs in brackets and sends it to the model. The answer comes back structured:
{ "text": "You can work remotely up to two days per week, a rule in effect since March 2026.", "sources": ["remote-policy#2"], "found": true }
Code checks that remote-policy#2 was among the IDs it sent. It was, so the answer goes out with a link to section 2 of the policy.
Now the token math, with illustrative numbers. Suppose 500 tokens per handbook page: 300 pages add up to 150,000 tokens on every call. With RAG, five chunks of 400 tokens add up to 2,000 tokens of context. At a thousand questions a day, that is 150 million versus 2 million input tokens.
schema Answer { text: string, sources: list of string, found: bool }
function ask(question):
# 2. retrieve only what matters
chunks = index.search(question, k=5)
trace.span("retrieve", chunks.ids, chunks.scores)
# 3. context with identifiers
context = join(f"[{c.id}] ({c.source}) {c.text}" for c in chunks)
# 4. answer only from the passages
answer = llm(RAG_PROMPT, { context, question }) -> Answer
# 5. check citations in code
if not answer.found or not answer.sources ⊆ chunks.ids:
return "I could not find that in the documents."
return answer
| Aspect | What happens | How it shows up in the trace |
|---|---|---|
| Calls | One retrieval and one model call | retrieve span before the model span |
| Latency | Retrieval is usually fast; the model dominates | short retrieve duration next to generation |
| Cost | Only the passages enter the context, not the whole base | input tokens proportional to k and chunk size |
| Failure: retrieval missed | The right passage did not come back; the model answers with what it has | retrieved IDs do not include the eval’s expected passage |
| Failure: answer beyond the passages | The model fills in with general knowledge | claim with no cited ID, or an ID that was not in the context |
| Failure: stale index | Document updated, index not | index version older than the document’s |
A note on k: more passages are not always better. The “Lost in the Middle” study (Liu et al., 2023) showed that models make worse use of information placed in the middle of a long context. Prefer a few good passages over many mediocre ones.
Evaluate retrieval and answering separately. Most RAG problems are retrieval problems.
An eval case:
{ "question": "How many remote days per week can I take?", "expected_chunk": "remote-policy#2", "expected_answer_contains": "two days" }
Include questions with no answer in the base, such as “does the company pay for a gym?” when no policy mentions it. They measure whether the system knows how to say it does not know. To build the set, see Golden Dataset.
The split helps diagnosis. If recall@5 is 95% and the answer is wrong, the problem is the prompt or the model. If recall@5 is 60%, changing the prompt will not help.
retrieve span with IDs, scores, and the index version.Every indexed document becomes text the model reads. If someone can put a document in the base, they can put instructions in front of the model. Greshake et al. (2023) demonstrated this indirect injection on real systems: the malicious instruction lived in retrieved content, not in the user’s message.
Three precautions:
In Brazil, the LGPD (the country’s data protection law, similar to GDPR) also applies: indexing customer tickets brings personal data into the index. Decide what goes in, how long it stays, and who can retrieve it.
| Pros | Cons |
|---|---|
| Answers reflect the current knowledge in your base | Quality depends on retrieval, which needs its own tuning |
| Updating knowledge does not require touching the prompt | One more piece of infrastructure: index, pipeline, updates |
| Citations let people check the source | Questions that need the whole base do not work well |
| Cheaper than sending the whole base | Document permissions must be enforced during retrieval |
The tabs do the same thing: retrieve passages, build the context with IDs, ask for a structured answer with sources, and check the citations. Python and TypeScript call Anthropic. LangChain swaps the model that answers. Retrieval stays yours: it decides what the model is allowed to cite.
Python. Use this when you want the whole pipeline in plain sight, with a local index for testing.
import anthropic
from pydantic import BaseModel
from sentence_transformers import SentenceTransformer, util
client = anthropic.Anthropic()
embedder = SentenceTransformer("sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
# index built beforehand: each chunk with id, source, and text
CHUNKS = load_chunks("policies/") # your loading and chunking function
VECTORS = embedder.encode([c["text"] for c in CHUNKS])
RAG_PROMPT = """Answer the question using only the passages provided.
List the IDs of the passages you used in sources.
If the answer is not in the passages, set found=false and do not make anything up."""
class Answer(BaseModel):
text: str
sources: list[str]
found: bool
def ask(question: str, k: int = 5) -> Answer | None:
# 2. retrieve the k closest chunks
scores = util.cos_sim(embedder.encode(question), VECTORS)[0]
top = [CHUNKS[i] for i in scores.argsort(descending=True)[:k].tolist()]
# 3. context with identifiers
context = "\n\n".join(f"[{c['id']}] ({c['source']})\n{c['text']}" for c in top)
# 4. answer only from the passages
answer = client.messages.parse(
model="claude-opus-5-5",
max_tokens=2048,
system=RAG_PROMPT,
messages=[{"role": "user", "content": f"<passages>\n{context}\n</passages>\n\nQuestion: {question}"}],
output_format=Answer,
).parsed_output
# 5. check citations in code
ids = {c["id"] for c in top}
if not answer.found or not answer.sources or not set(answer.sources) <= ids:
return None # 6. "not found"
return answerWhat you gained: every stage in plain sight and testable. What you lost: an in-memory index does not scale; swap in a vector database as the base grows.
TypeScript. Use this when the index already lives in a vector database and the backend is Node.
import Anthropic from "@anthropic-ai/sdk";
import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod";
import { z } from "zod";
const client = new Anthropic();
const RAG_PROMPT = `Answer the question using only the passages provided.
List the IDs of the passages you used in sources.
If the answer is not in the passages, set found=false and do not make anything up.`;
const Answer = z.object({ text: z.string(), sources: z.array(z.string()), found: z.boolean() });
// search queries your vector database: returns { id, source, text }[]
export async function ask(question: string, k = 5) {
// 2. retrieve the k closest chunks
const top = await search(question, k);
// 3. context with identifiers
const context = top.map((c) => `[${c.id}] (${c.source})\n${c.text}`).join("\n\n");
// 4. answer only from the passages
const { parsed_output: answer } = await client.messages.parse({
model: "claude-opus-5-5",
max_tokens: 2048,
system: RAG_PROMPT,
messages: [{ role: "user", content: `<passages>\n${context}\n</passages>\n\nQuestion: ${question}` }],
output_config: { format: zodOutputFormat(Answer) },
});
// 5. check citations in code
const ids = new Set(top.map((c) => c.id));
if (!answer?.found || answer.sources.length === 0 || !answer.sources.every((s) => ids.has(s))) {
return null; // 6. "not found"
}
return answer;
}What you gained: the index lives in the database you already run. What you lost: retrieval quality depends on that database’s configuration, which is outside this page.
Python + LangChain. Use this when retrieval is already yours and only the model call needs to change provider.
from langchain.chat_models import init_chat_model
from pydantic import BaseModel
class Answer(BaseModel):
text: str
sources: list[str]
found: bool
RAG_PROMPT = """Answer the question using only the passages provided.
Cite the ids of the passages you used in sources.
If the answer is not in the passages, set found to false and do not invent one."""
# "provider:model". Retrieval is not part of that string.
answerer = init_chat_model("openai:gpt-4o-mini", temperature=0).with_structured_output(Answer)
def ask(question: str, k: int = 5) -> Answer | None:
top = search(question, k) # 2. embeddings, keywords, or both
# 3. context with identifiers
context = "\n\n".join(f"[{c['id']}] ({c['source']})\n{c['text']}" for c in top)
# 4. answer only from the passages
answer = answerer.invoke([
("system", RAG_PROMPT),
("human", f"<passages>\n{context}\n</passages>\n\nQuestion: {question}"),
])
# 5. check citations in code
ids = {c["id"] for c in top}
if not answer.found or not answer.sources or any(s not in ids for s in answer.sources):
return None # 6. "not found"
return answersearch is your index. LlamaIndex fits when the index is the product. Here it would hide step 5, which is the pattern.
What you gained: the answer changes provider without touching retrieval or the check. What you lost: the index, the chunking, and who may see each passage stay yours.
Checked against the LangChain 1.x docs in October 2026.
| Library | In this pattern | Still yours |
|---|---|---|
| Provider SDK | The Python and TypeScript tabs | Retrieval, ids, and the citation check |
| LangChain | with_structured_output on the answer |
The search function and step 5 |
| LlamaIndex | The index and the query engine, when the corpus is the product | The check that a cited id was in the context |
| LangGraph | A “search again” branch when the first pass comes back empty | The rule for how many searches fit in the budget |
Answer the 6 questions to complete this lesson. Watching alone does not complete it: the check is what counts toward the trail and the certificate.
Retrieval-Augmented Generation (RAG) is an AI design pattern for LLM apps and agents, in the Context and knowledge group. Fetch relevant passages and hand them to the model along with the question.
In the real-world scenarios Internal knowledge base chat, Support bot with human escalation, and Health assistant with guardrails.