Trail content
9 minEnglish narration, Portuguese subtitlesPodcast (RSS)
Chapters
  1. 0:00Opening
  2. 1:13The idea
  3. 2:05How it works
  4. 2:54A real case
  5. 3:55A challenge
  6. 4:42When not to use it
  7. 6:26In production
  8. 7:55The code
  9. 8:34Wrap-up
Transcript

Opening

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 idea

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.

How it works

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.

A real case

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.

A challenge

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.

When not to use it

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.

In production

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.

The code

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.

Wrap-up

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.

PatternContext and knowledge

Retrieval-Augmented Generation (RAG)

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.

Element
11/70
Level
Intermediate
Read
10 min
Updated
On this page 18
  1. 01Intent
  2. 02Problem
  3. 03Solution
  4. 04Structure
  5. 05Pseudocode
  6. 06Real-world scenarios
  7. 07Applicability
  8. 08When not to use
  9. 09How to implement
  10. 10Variations
  11. 11Cost, latency, and failure modes
  12. 12How to evaluate
  13. 13In production
  14. 14Security
  15. 15Pros and cons
  16. 16Code
  17. 17Framework support
  18. 18Check your understanding
  19. ··Quick answers

Intent

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.

In one sentence
An open-book exam: before answering, the student opens the book to the right page.

Problem

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?

Solution

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.

Structure

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
  1. The question arrives from the user.
  2. Retrieval looks up the chunks closest to the question in the index, by embedding, keyword, or both.
  3. Code builds the context: each passage with an ID and its source.
  4. The model answers only from the passages and cites the IDs it used.
  5. Code checks that the cited IDs exist in the context it sent.
  6. The answer goes out with sources, or with an honest “not found”.

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.

Worked example

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.

Check +20 XP on first tryIn the example above, the model answered citing benefits#9, an ID that was not among the five passages sent. What should the system do?

Pseudocode

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

Real-world scenarios

  • Internal knowledge base chat. Policies, processes, and technical docs in a single index. Citations become links to the original document, and employees check the rule at the source.
  • Support bot with human escalation. The bot searches help center articles before answering. When no passage is relevant, the conversation goes to a person instead of turning into invention.
  • Health assistant with guardrails. The base is restricted to content approved by professionals. “Not found” is part of safety: outside the base, the assistant advises seeing a doctor.
  • Legal assistant. In Mata v. Avianca (2023), US lawyers filed a brief with court decisions invented by ChatGPT and were fined. A legal assistant with RAG only cites decisions that are in the index, and code checks every citation before the answer goes out.

Applicability

  • The knowledge you need is yours: policies, manuals, contracts, tickets, code.
  • Content changes and answers must follow the current version.
  • The base is too large to fit, whole, in every call.
  • You need to show where the answer came from.

When not to use

  • The base fits comfortably in the context. Sending it all, with prompt caching, is simpler and avoids retrieval errors. See the RAG for Small Data smell and the Replace RAG with Direct Context refactoring.
  • The question asks for an exact value from a system (balance, order status, a bill’s due date). That is a query, and the path is Tool Use.
  • The question requires reasoning over the whole base (“how many contracts expire this year?”). Similarity search brings back some passages, not all of them. The path is a structured query, as in the Natural language to SQL scenario, or batch processing with Map-Reduce.
Check +20 XP on first tryA bank wants its app assistant to answer ‘how much do I still owe on my loan?’. The team proposes indexing every customer's statements in a RAG. What is the better path?

How to implement

  1. Collect the real questions the assistant will get. They guide everything else.
  2. Pick the sources and clean the documents: remove menus, footers, and duplicates.
  3. Split by structure (section, article, clause), not by a fixed number of characters. Store the title, source, and effective date with each chunk.
  4. Generate embeddings and store them in an index. Start simple: a database with a vector extension handles many cases.
  5. Measure retrieval alone before wiring in the model: for each question, is the right passage among the top results?
  6. Write the prompt with the three rules: passages only, cite IDs, admit when nothing was found.
  7. Check citations in code and treat “not found” as a valid answer.
  8. Build the index update process and record the index version on every trace.

Common beginner mistakes

  • Cutting every 1,000 characters. The rule “up to two days per week” lands half in one chunk and half in the next. Neither answers the question alone.
  • Chunks without a heading. The passage “the limit is two days” does not say which policy it belongs to. Prepend the section title, or see Contextual Chunking.
  • Two versions of the same policy in the index. The 2024 and 2026 versions compete in retrieval. Tag the effective date and filter out superseded documents.
  • Tuning the prompt when the error is in retrieval. If the right passage never arrived, no instruction saves the answer. Look at the retrieved IDs in the trace first.

Variations

  • Hybrid Search. Combines embeddings and keywords. Helps with acronyms, codes, and proper names.
  • Reranking. Retrieves many candidates and uses a model to re-sort them and keep the best.
  • Query Rewriting. Rewrites the question before retrieval, useful in conversations where “and for interns?” depends on context.
  • Agentic RAG. An agent decides when to search, what to search for, and whether to search again.
  • Citation Grounding. Requires every claim to point at the exact passage that supports it, sentence by sentence.

Cost, latency, and failure modes

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.

How to evaluate

Evaluate retrieval and answering separately. Most RAG problems are retrieval problems.

  • Retrieval: for each question, did the right passage show up in the top k? (recall@k)
  • Faithfulness: is every claim in the answer supported by a cited passage?
  • Abstention: for questions with no answer in the base, does the system say “not found”?
  • Usefulness: does the answer solve the asker’s question?

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.

Check +20 XP on first tryThe HR assistant's eval shows 58% recall@5 and 97% faithfulness. The team wants to switch to a bigger model. What do the numbers point to?

In production

  • A retrieve span with IDs, scores, and the index version.
  • Daily “not found” rate. A sudden rise can mean a new topic or a broken index.
  • A list of questions with low retrieval scores, to discover missing documents.
  • User feedback (helpful or not) tied to the cited passages, to find bad documents.

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. 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:

  • Control who writes to the index. A wiki any employee can edit is a way in.
  • Enforce permissions at retrieval time. An intern must not receive passages from the board’s folder. Filter before retrieving, not after answering.
  • Treat retrieved passages as data. Read the Instructions from Untrusted Data smell and the Prompt injection in practice fundamental.

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 and cons

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

Code

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 answer

What 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 answer

search 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.

Framework support

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

Check your understanding

Check +20 XP on first tryThe HR assistant says mileage reimbursement is $0.50 per mile. The current policy says $0.65, and the old value comes from a 2023 document still in the index. What is the right fix?

Check +20 XP on first tryYour base is 40 pages of FAQ, about 25,000 tokens, and changes once a quarter. A colleague wants to set up a vector database with a chunking pipeline. What do you suggest first?

Check +20 XP on first tryWhy does the pattern ask code to check the cited IDs instead of trusting the prompt's instruction?

Quick answers

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.

Which smells does the Retrieval-Augmented Generation (RAG) pattern help cure?

Prompt Bloat.

Where does Retrieval-Augmented Generation (RAG) show up in practice?

In the real-world scenarios Internal knowledge base chat, Support bot with human escalation, and Health assistant with guardrails.

Go further

Relations

Smells it helps cure 1
Refactorings that lead here 1
Part of the tracks 1
Search