Skip to content

Integrate retrieval

  • Lesson 10
  • 120 min
  • Module 3: Build one complete user journey

What you will be able to do

Replace the two stand-ins with the real parts: search by meaning in PostgreSQL, real answers of a model, citations checked in code, and a trace of every question that shows where its time goes.

Before you startWhat to know and to have readyAbout 120 minutes

What you need to know

What you need on your computer

  • Your project from the last lesson, with the pack loaded by the stand-ins. Stop the API for now (Ctrl+C in its terminal).
  • An internet connection for the first run: the embedding model, about 470 MB, downloads once into ~/.cache/huggingface.
  • No account and no key. The answers in this lesson are recorded answers of a real model.

At the end of the rehearsals, the real actor replaces the stand-in. The marks on the floor stay the same, and so do the cameras. But some things change: the actor is a little taller, so the lights move up. The crew changes one thing at a time and checks the scene after each change.

The capstone does the same. The interfaces stay: embed_queries and complete. The data behind them changes, and you check the slice after each of the two changes.

In .env, replace the hash stand-in with the RAG course’s model, intfloat/multilingual-e5-small. Keep the mock for the answers, so only one thing changes.

Terminal window
EMBEDDER=e5
ANSWER_PROVIDER=mock

Ask the Ontario question in the terminal. scripts.ask calls the same pipeline as the API, in one process:

Terminal window
python -m scripts.ask "How long does delivery to Ontario take?" --tenant bramble
Ines (staff) at Bramble Books asks: How long does delivery to Ontario take?
Passages found: none
Answer (not answerable): The documents do not answer this question.
none none | 28605 ms | tokens 0 in, 0 out | US$0.000000 | trace f6a04f8c45466eda7b5b6524b0b32b90

The model’s loading lines are left out here and below. No passage, so the answer model is not called, and the answer is “The documents do not answer this question.” Nothing is broken. The documents were indexed with hash-384, and vectors of two models cannot be compared. So the search reads only the chunks of the current model, and there are none. Index again with the real model:

Terminal window
python -m scripts.load_pack --reset
Deleted every shop's data.
Identity: 2 shops, 7 people, 6 memberships
larkfield: 36 documents uploaded, 0 already there
bramble: 15 documents uploaded, 0 already there
Indexing with intfloat/multilingual-e5-small@614241f ...
Indexed 51 documents in 6.2 s (failed 0).
bramble: 15 documents, 60 passages, 0 failed
larkfield: 36 documents, 185 passages, 0 failed

The first run downloads the model; later runs use the copy in the cache. The time changes from run to run: 7.1 s in the course’s next run, and about 30 seconds in a fresh environment in the course’s snapshot check. Now the same question finds passages. The search in doc_assistant/retrieve.py is the RAG course’s dense search, written as one SQL query. It applies four filters before it takes the best 5: the shop, the reader’s access level, the date of the question, and the embedding model. A passage that is filtered out can never take one of the five places. Ask the Ontario question again, with the mock still on:

Ines (staff) at Bramble Books asks: How long does delivery to Ontario take?
Passages found: 20804b124392 delivery-policy v1, ffefe0c5ab86 delivery-policy v1, e5e28bf8b6a1 faq v4, 94a83a5b1bff delivery-policy v1, ba8746ede8c3 privacy-notice v2
Answer (answerable): We ship within 2 business days.
- We ship within 2 business days. [Delivery times, Shipping, v1]
mock mock | 3822 ms | tokens 0 in, 0 out | US$0.000000 | trace b9b1f3c01c9332cf9e89a051938c3a27

The mock still answers with the first sentence of the best passage. But now the best passage is the delivery policy’s “Shipping” section, found by meaning, not by shared words. Notice the time, 3,822 ms. Each scripts.ask is a new process, so each one loads the embedding model again. Keep that number in mind for the traces below.

Now replace the mock with real answers:

Terminal window
ANSWER_PROVIDER=recorded

The course team asked a real model, chat-small, the course’s questions, and recorded each request and its answer in recordings/answers.jsonl. The recorded provider replays them: no key, no cost, and the same answer every time. A recording is found by the model, the prompt, the question and its date, not by the passages. The idea is the RAG course’s replay.

Terminal window
python -m scripts.ask "How long does delivery to Ontario take?" --tenant bramble
Ines (staff) at Bramble Books asks: How long does delivery to Ontario take?
Passages found: 20804b124392 delivery-policy v1, ffefe0c5ab86 delivery-policy v1, e5e28bf8b6a1 faq v4, 94a83a5b1bff delivery-policy v1, ba8746ede8c3 privacy-notice v2
Answer (answerable): Delivery to Ontario takes 4 to 8 days. Orders ship within 2 business days.
- Delivery to Ontario takes 4 to 8 days. [Delivery times, Shipping, v1]
- Orders ship within 2 business days. [Delivery times, Shipping, v1]
recorded chat-small | 3932 ms | tokens 719 in, 137 out | US$0.000140 | trace 171024ffa6e415fc217bdea1792456db

A real model’s answer, with two claims, each cited. The last line shows the replay and what the real call cost: 719 tokens in and 137 out, US$0.000140 at chat-small’s price list (US$0.10 and US$0.50 per million tokens). The prompt is prompts/answer_v1.md. It asks for claims with passage IDs, in a strict JSON schema, so code can check them.

Recordings exist for the course’s 62 tuning questions with their dates, and for nothing else. Ask any other question, and the mock answers, with a note:

Ines (staff) at Bramble Books asks: Do we sell e-books?
Passages found: 07e2059161e9 faq v4, 31d85d1940b3 faq v4, fafc42e3e194 special-orders v2, 5220ca4b5665 events-and-room-hire v1, 1e6a23c78c8f supplier-terms v1
Answer (answerable): Can I pick up my online order?
- Can I pick up my online order? [Frequently asked questions, Can I pick up my online order?, v4]
Note: No recording for this question: the mock made this answer from your passages.
mock mock | 3722 ms | tokens 0 in, 0 out | US$0.000000 | trace c40331669e8e4c26691b1e1cf0757815

The date is part of the question. Larkfield’s questions were recorded with the date 2026-10-09, and the course’s “today” is 2026-10-11 (ASSISTANT_TODAY). The same words with another date are another question:

$ python -m scripts.ask "What is the staff discount?" --tenant larkfield
Sam (staff) at Larkfield asks: What is the staff discount?
Passages found: 81007010cd4d staff-discount-and-perks v1, 7919a06cc2aa staff-discount-and-perks v1, 93cb6359874a privacy-policy v3, 6fddb68adafa privacy-policy v3, 2638b732560d support-team-routing v4
Answer (answerable): You get 20% off all products, except gift cards.
- You get 20% off all products, except gift cards. [Staff discount and perks, Staff discount, v1]
Note: No recording for this question: the mock made this answer from your passages.
mock mock | 3791 ms | tokens 0 in, 0 out | US$0.000000 | trace 5c7e8e285f6de1af99ee1b84550d32a1
$ python -m scripts.ask "What is the staff discount?" --tenant larkfield --as-of 2026-10-09
Sam (staff) at Larkfield asks: What is the staff discount?
Passages found: 81007010cd4d staff-discount-and-perks v1, 7919a06cc2aa staff-discount-and-perks v1, 93cb6359874a privacy-policy v3, 6fddb68adafa privacy-policy v3, 2638b732560d support-team-routing v4
Answer (answerable): Staff get 20% off all products except gift cards, up to $2,000 of purchases per calendar year. It cannot be combined with other promotions or discount codes.
- Staff get 20% off all products except gift cards, up to $2,000 of purchases per calendar year. [Staff discount and perks, Staff discount, v1]
- The discount cannot be combined with other promotions or discount codes. [Staff discount and perks, Staff discount, v1]
recorded chat-small | 3698 ms | tokens 958 in, 117 out | US$0.000154 | trace ddf0a76bf81ef75caf7388d2bd65deb1

A recording does not depend on the passages. So the provider could replay an answer made from staff-only passages to someone who may not read them, such as Omar, a read-only member from Module 4. The recorded provider has a rule against it: a recording is replayed only if every passage that it cites is among your passages. If one is not, the mock answers instead, and the note says why. In this module everyone acts as Sam, so you see the rule work in Module 4.

ANSWER_PROVIDER=live calls any endpoint with the OpenAI Chat Completions format, with LLM_BASE_URL and LLM_API_KEY from .env. The settings never print the key. Each call has a timeout (PROVIDER_TIMEOUT_S, 20 seconds), and a failure comes back in the one error shape, for example 504 provider_timeout. This lesson did not run live: the recorded answers are real calls of the same model. Prices change: check your provider’s current price page first.

You check a student’s footnotes. Does the cited page exist? Was the book on the reading list? Does the page say the number in the sentence? You cannot check whether the student understood the book, but you can check these things in seconds.

doc_assistant/cite.py does the same for every claim, as in Answer and cite. It checks five things. Each cited ID is a passage of this shop. The model got that passage. The claim cites something. Every number in the claim is in a cited passage. At least half of its words are there. Here is a tiny example. The passage is real, Bramble Books’ “Shipping” section. The four claims are made up to test the checks:

Passage 20804b124392:
[20804b124392] Delivery times | policy, version 1, in force from 2026-09-15, no end date | section: Shipping
Delivery times
Shipping
We ship within 2 business days. Delivery in British Columbia takes 2 to 4 business days, and 4 to 8 days to the rest of Canada. We do not ship outside Canada.
Delivery to Ontario takes 4 to 8 days. cites ['20804b124392'] -> ok
Delivery to Ontario takes 3 to 5 days. cites ['20804b124392'] -> ['number_not_found:3,5']
Delivery to Ontario takes 4 to 8 days. cites nothing -> ['no_citation']
Delivery to Ontario takes 4 to 8 days. cites ['c0712b8decbc'] -> ['missing_id:c0712b8decbc']

Read the four lines. The first claim is supported. The second has numbers that the passage does not have. The third cites nothing. The fourth cites a real passage, but from Larkfield’s delivery policy. In Bramble Books, that ID does not exist, so the check says missing_id, and the page shows nothing about it. These checks are clues, not proof: a claim can use the passage’s words and still say the opposite. A person decides those cases, and Module 5 measures them.

A relay race has four runners. The team’s time is 41 seconds, but the coach wants to know more: which runner was slow, and where the baton changed hands. So the coach takes a split time for each runner.

The problem without split times: Sam’s first question through the API took 3.6 seconds. Tomás is sure: “The model is slow. Let’s pay for a faster one.” But the answer was a replay, which takes almost no time. Without a measurement, you would change the wrong part.

A trace is the record of one request, with a span for each step: a name, a start, a length and a few facts. The pipeline makes one trace per question with OpenTelemetry, the standard tool for traces. Its spans are ask, then retrieve, generate, check and store inside it. In the API, FastAPI adds its own spans around them: the request (POST /v1/tenants/{tenant}/answers), then fastapi.dependencies, fastapi.endpoint and fastapi.serialization. doc_assistant/tracing.py writes them to traces/spans.jsonl, one JSON object per line, so you need no other service to read them. The spans never contain the question or the answer, only IDs, sizes and times. A trace may be kept longer and read by more people than the data.

Sam started the API with the real parts and asked two questions at Bramble Books: the Ontario question, then “Is delivery free for a 75-dollar order?”. After each, Sam read the trace. The long attribute list of the first line is shortened here:

Terminal window
python -m scripts.trace last

The first question:

trace 6c811aa755fae25bc492a985eed4c7ac: 3595 ms
POST /v1/tenants/{tenant}/answers 0 ms + 3595 ms |########################################| http.route=/v1/tenants/{tenant}/answers, http.response.status_code=200
fastapi.dependencies 3 ms + 2 ms |# | code.function.name=doc_assistant.answers.post_answer
fastapi.endpoint 5 ms + 3589 ms |####################################### | code.function.name=doc_assistant.answers.post_answer
ask 5 ms + 3589 ms |####################################### | tenant=bramble, question.chars=39, prompt=answer_v1, answerable=True
retrieve 5 ms + 3584 ms |####################################### | passages=5
generate 3590 ms + 0 ms | #| provider=recorded, model=chat-small, attempts=1, tokens.input=719, tokens.output=137
check 3590 ms + 2 ms | #| claims=2, claims.flagged=0
store 3592 ms + 2 ms | #|
fastapi.serialization 3594 ms + 0 ms | #| code.function.name=doc_assistant.answers.post_answer

The second question, a few seconds later:

trace 7c3ad66b436e9bce03c290390269e646: 16 ms
POST /v1/tenants/{tenant}/answers 0 ms + 16 ms |########################################| http.route=/v1/tenants/{tenant}/answers, http.response.status_code=200
fastapi.dependencies 0 ms + 2 ms |##### | code.function.name=doc_assistant.answers.post_answer
fastapi.endpoint 3 ms + 13 ms | ############################### | code.function.name=doc_assistant.answers.post_answer
ask 3 ms + 13 ms | ############################### | tenant=bramble, question.chars=39, prompt=answer_v1, answerable=True
retrieve 3 ms + 8 ms | ################## | passages=5
generate 12 ms + 0 ms | # | provider=recorded, model=chat-small, attempts=1, tokens.input=723, tokens.output=115
check 12 ms + 2 ms | #### | claims=2, claims.flagged=0
store 13 ms + 2 ms | ##### |
fastapi.serialization 16 ms + 0 ms | #| code.function.name=doc_assistant.answers.post_answer
Three groups of horizontal bars on a time axis. First question after the start: the request takes 3,595 ms, and retrieve takes 3,584 ms of it; generate takes 0.1 ms, check 1.5 ms and store 1.7 ms, all at the far right. Second question at the same scale: every bar is a thin line at the left, the request 16 ms. Second question zoomed to 16 ms: retrieve 7.7 ms, generate 0.1 ms, check 1.6 ms, store 2.2 ms. A note says that generate is a replay: the recorded calls took 1,463 ms and 1,420 ms when the course team made them.
Two questions to the same API process, drawn to scale. The first question loads the embedding model inside its retrieve step. The model call is a replay, so generate is almost zero.
Text description of the diagram

The first question took 3,595 ms in the API, and its retrieve step took 3,584 ms of them: the API loaded the embedding model at its first search. Generate took 0.1 ms, because the answer was replayed; check took 1.5 ms and store 1.7 ms. The second question took 16 ms in all. At the same scale its bars are thin lines; zoomed, retrieve takes 7.7 ms, generate 0.1 ms, check 1.6 ms and store 2.2 ms. The two recorded model calls took 1,463 ms and 1,420 ms when the course team made them. A live call would add about 1.4 seconds to generate.

So the slow part was retrieve: the API loads the embedding model at the first search, not at start-up. The answer carries both times. latency_ms is the real time of the question in the API: 3,587 and 11 ms. generation_ms is the time of the recorded call when the course team made it.

When to use traces. On every request that has more than one slow step, from the first day. They cost little: one line per span in a file. When not to. Do not put personal data in a span. What they cost. A file that grows with every question. Delete it, or send the spans to a collector with a retention limit; the planned course MLOps and LLMOps: Operating AI in Production covers collectors.

Two rows of boxes joined by arrows, above a wide box for PostgreSQL with pgvector. Upload path: the page, choose a file and upload; POST documents, checks, 202 in 12 ms; the documents row, uploaded; the worker thread parses, chunks and embeds with e5; chunks and vectors, indexed, and the whole pack of 51 documents in 7.1 s. Question path, second question 16 ms in the API: the page, type a question and ask; POST answers, checked, 3 to 500 characters; retrieve with 4 filters and the top 5 in 8 ms, 3.6 s the first time, highlighted; generate, recorded chat-small, 719 tokens in, 137 out, US$0.000140; check citations in code, 2 ms; store the answer and its evidence, 2 ms, and the page shows it. Dashed arrows from retrieve and store to the database: every query names the shop.
Module 3's vertical slice, from the browser to the stored answer, with the times of Sam's runs. The top row is the upload path; the bottom row is the question path.
Text description of the diagram

Upload path: the API checks the file and answers 202 in 12 ms, and the row waits as uploaded. The worker parses, chunks and embeds it with e5, and saves it as indexed (the whole pack: 51 documents in 7.1 seconds). Question path: the API checks the question. Retrieve keeps the top 5 after four filters, in 8 ms (3.6 seconds the first time). Generate replays chat-small’s answer (719 tokens in, 137 out, US$0.000140). The citation checks take 2 ms, and storing takes 2 ms. Every query to PostgreSQL names the shop.

Grace’s journey on the page now uses the real parts. The question has the date of the recordings:

The Ask a question form with the question What is the staff discount? and the date 2026-10-09. Below, the answer: Staff get 20% off all products except gift cards, up to $2,000 of purchases per calendar year. It cannot be combined with other promotions or discount codes. Under Evidence, two numbered claims, each with the citation Staff discount and perks, Staff discount, version 1. The last line: recorded: chat-small, 14 ms, US$0.000154, and a trace ID.
A recorded answer of a real model on the page, with its claims and citations. The provider line says recorded.
The Ask a question form with the question Do we offer a student discount? and the date 2026-10-09. Below, in a box with an amber left border, the heading No answer in the documents and the sentence: The documents do not say whether we offer a student discount. The last line: recorded: chat-small, 12 ms, US$0.000092, and a trace ID.
The empty state with a real answer: the documents do not answer, and the assistant says so.

Common mistake

Every answer says 'No recording for this question'

Symptom: the answers on the page come from the mock, with the note “No recording for this question: the mock made this answer from your passages.”

Cause: the question, its date, the prompt or the model is not exactly as recorded. On the page, an empty date field means today, 2026-10-11, and Larkfield’s questions were recorded for 2026-10-09.

Fix: set the date to 2026-10-09 for Larkfield’s questions. Ask the questions exactly as in evaluation/dev.jsonl. For any other question, the mock is the honest answer without a model; use live if you have your own endpoint.

Common mistake

The first question takes seconds, and later ones do not

Symptom: the first answer after a start takes 3 to 4 seconds. The trace shows the time in retrieve.

Cause: the embedding model loads at the first search. This is expected, and the trace proves it.

Fix: none is needed for the slice. If the first user must not wait, load the model at start-up: one query to the embedder in the API’s lifespan. The cost is a slower start. Write the choice down.

  • Standard track. Make both changes, one at a time, and check after each. Ask three of the recorded questions on the page, and one of your own, which gets the mock with its note. Read the trace of each.
  • Own-idea track. Replace your stand-ins one at a time. Index your documents with a real embedding model, and check that the search filters by tenant, access and date before the top k. For answers, record real answers of a small model for your tuning questions only, never for your held-out questions. Or keep the mock until Module 5, and say so.

Evidence to keep. In docs/evidence/m03-retrieval.txt: the load_pack output with the real model, one scripts.ask output with a cited answer, the trace of a first and a second question, and the output of the citation checks on one constructed bad claim. Label every mock or recorded answer as such.

Checklist

  • The index was made with the same model that searches it.
  • The search filters by shop, access level and date before it takes the top k.
  • Every answer says which provider made it: mock, recorded or live.
  • Every claim’s citations are checked in code, and a citation of another shop is missing_id.
  • Every question has a trace, with no question text or answer in it.

Guided practice: one small flow, end to end

Section titled “Guided practice: one small flow, end to end”

Sam does one small flow from the upload to the trace, before adding anything. load_pack --reset deleted every shop’s data, the holiday hours too. So Sam uploads holiday-hours.md again, now with the real parts, and asks Grace’s question.

  1. Start the API again in its terminal (fastapi dev doc_assistant/main.py), so that it reads the new .env. Upload holiday-hours.md to Larkfield, on the page or with the curl command of Backend foundation.
  2. Watch its status: it goes from uploaded to indexed, with its number of passages.
  3. Ask “Is the help desk open on 26 December?”.
  4. Run python -m scripts.trace last, and check that its trace ID is the one under the answer.
Show what Sam saw

The course’s run, with curl for the upload: its status line, then the document 3 seconds later, shortened.

HTTP/1.1 202 Accepted
{"doc_key": "holiday-hours", "version": "1", "status": "indexed", "chunk_count": 2, "error": null}

The answer, shortened to its main fields:

{
"answer": "The help desk is closed on 25 December, 26 December and 1 January.",
"provider": "mock",
"model": "mock",
"input_tokens": 0,
"output_tokens": 0,
"cost_usd": 0.0,
"latency_ms": 12,
"generation_ms": 0,
"trace_id": "c2fe0f20d60f387a39946f27c9f3ae9c"
}
trace c2fe0f20d60f387a39946f27c9f3ae9c: 18 ms
generate 13 ms + 0 ms | provider=mock, model=mock, attempts=1, tokens.input=0, tokens.output=0

The trace ID of the answer and of scripts.trace last are the same, so this trace belongs to this answer. Look at the provider: mock. Nobody recorded a real answer to this question, so the mock answered, and the page shows the note “No recording for this question”. This time the mock’s first sentence is right: the e5 search ranked “Closed days” first. That is luck, not quality. One small flow works end to end, and its weak point is written down: answers to new questions need the live provider.

On your own project, complete one small flow end to end before you add any optional feature. The flow has four parts: one document of a new kind (for example a PDF with a table), and one question that only it answers. Then the answer with its citation on the page, and its trace. Then write in your README, under “Not yet”, three features that you will not add in this module, and why.

Show a worked solution

This is Sam’s worked solution on the standard track. The new kind of document is Bramble Books’ room-hire PDF, whose prices are only in a table. It is already in the pack, and its question was recorded.

$ curl -s -H "Content-Type: application/json" -d '{"question": "Can a community group use the reading room for free?"}' http://127.0.0.1:8000/v1/tenants/bramble/answers
{
"answer": "Yes. Community groups can use the reading room for free on Tuesdays from 6 pm to 9 pm.",
"provider": "recorded",
"model": "chat-small",
"input_tokens": 760,
"output_tokens": 75,
"cost_usd": 0.0001135,
"latency_ms": 11,
"generation_ms": 1227,
"trace_id": "e8836b56a46ac7edea822d97338e3b07"
}

The answer above is shortened to its main fields, and the trace to the lines from ask down:

trace e8836b56a46ac7edea822d97338e3b07: 18 ms
ask 5 ms + 13 ms | ############################ | tenant=bramble, question.chars=52, prompt=answer_v1, answerable=True
retrieve 5 ms + 9 ms | ################## | passages=5
generate 14 ms + 0 ms | # | provider=recorded, model=chat-small, attempts=1, tokens.input=760, tokens.output=75
check 14 ms + 2 ms | ### | claims=1, claims.flagged=0
store 16 ms + 2 ms | ### |

In the full answer, the one claim cites events-and-room-hire, section “Prices”: the table in the PDF. Sam’s README then gets a “Not yet” section:

## Not yet
- Sign-in and roles: every request acts as Sam (Module 4 adds them; until then, run it only on your computer).
- A reranker: not measured yet; Module 5 decides with evidence.
- Answers to new questions with a real model: they need the live provider and a key; the mock answers them now, labelled.

On the own-idea track, your flow and your list are different; the shape is the same: one flow with its evidence, and a short list of what waits, each with its reason.

Completion check: a new user can do the task

Section titled “Completion check: a new user can do the task”

Ask a person who has not seen your project to do the journey with the synthetic data. They use only your README: start the project, upload a document, see it indexed, ask a question and read the cited answer. Do not help, and write down each place where they stop. You pass the check when they finish without your help. If they stopped, fix the README at each place, and ask a second person.

Show what a good record looks like

This record is an example, made up to show the format.

Who: Amira (developer, had not seen the project). Computer: Windows 11, Python 3.12.
Started: README "First run", step 1.
Stop 1: step 2 - "pip install" took 6 minutes; she thought it had frozen.
Fix: the README now says "about 900 MB; it can take several minutes".
Stop 2: the page showed "No recording for this question" for her own question.
Fix: the README now says which questions are recorded, and that others get the mock.
Finished: upload (indexed, 4 passages), question, cited answer. 25 minutes.
Second person after the fixes (Mei): finished with no stop.

Check your understanding

After EMBEDDER=e5, the Ontario question found no passage until Sam ran load_pack --reset. Explain why, and why the search does not simply use the old vectors.

Show the answer

The old chunks had vectors from the hash stand-in, and the question’s vector came from e5. Vectors of two models are in different spaces, even with the same length (384), so their distance means nothing. The search therefore reads only the chunks whose document was indexed by the current model (d.embedder = %(embedder)s), and there were none. Re-indexing made e5 vectors for every chunk. Searching the old vectors would return passages in a random order, which is worse than none.

Check your understanding

Predict: Sam asks 'What is the staff discount?' on the page with an empty date field. Which provider answers, and why?

Show the answer

The mock, with the note “No recording for this question”. An empty date means the course’s today, 2026-10-11, but the recording of this Larkfield question was made for 2026-10-09. The date is part of the replay key, because a policy can change between two dates. With the date 2026-10-09, the recorded answer of the real model is replayed.

Check your understanding

The first question took about 4 seconds. Tomás wants a faster model. Use the trace to answer him.

Show the answer

The trace splits the 4 seconds: almost all of it is in retrieve, which loads the embedding model at the first search. generate took almost no time, because the answer was a replay; the second question took a few milliseconds of retrieval. A faster answer model would change nothing here. If the first user must not wait, load the embedding model at start-up instead.

Check your understanding

Module completion check: a new user does the journey with the synthetic data. Which three pieces of evidence show that they did it, and which output of the slice must be labelled?

Show the answer

For example: the document’s row with status indexed and its number of passages; the answer on the page with its claims and citations; and the trace of that question, whose ID matches the one under the answer. Every answer must show its provider, so nobody reads a mock answer as the product’s quality. The record of where the new user stopped, and how the README was fixed, completes the evidence.

Recap

  • Replace stand-ins one at a time, and check the slice after each change.
  • An index must be searched with the same model that built it. Re-index after a change of model.
  • The search filters by shop, access level, date and model before it takes the top 5.
  • Recorded answers are real answers, replayed by model, prompt, question and date. Other questions get the mock, with a note. A recording is replayed only if your passages contain every passage that it cites.
  • cite.py checks every claim in code: the ID exists in this shop, it was given to the model, the numbers and most words are in the passage.
  • A trace with a span per step shows where the time goes. The first question loads the embedding model; the replayed model call takes almost no time.

Module 3 in one paragraph. You built a vertical slice: a checked backend with one error shape, uploads as jobs with states, and a page that shows every state. Then the real parts replaced the stand-ins, one at a time: dense search, recorded answers of a real model, citations checked in code, and a trace per question. A new user can do the task with the synthetic data. But every request acts as Sam, in any shop.

Next, Module 4 makes the slice safe: sign-in, memberships, roles and row-level security.

Next: Permission enforcement