Easier to read with cream paper, a tint, more contrast or larger text? Choose your colours and text, once, for every page.

Free lesson

How to answer from retrieved sources

After this lesson: I can build a prompt from what was found and answer only from it.

70 min lesson4 commandsAI coding agent lab: use opencode to explain and fix code, and check its work

An answer with its sources

Retrieval-augmented generation (RAG) means finding passages first and then asking a language model to answer from them alone. It matters because a model that answers from memory can sound sure and be wrong, while one that shows its sources can be checked.

A library visitor asks when it opens on Saturday. The site finds two passages, asks a model and shows the answer with the passage it came from.

By the end of this lesson

Note

I can build a prompt from what was found and answer only from it.

Key words

WordWhat it meansExample
Contextthe passages you put in the prompt
Promptthe text you send to a model
Citationa marker that points to a passage[1]
Groundedbased only on the passages given
Refusalan answer that says it does not know

The cells students fill in are left blank in this preview.

Put the passages in the prompt

The model has not read your documents. So the prompt carries them. Number each passage so the model can point to one. Then add the question and the rules.

Keep the passages few and short. Every word you add is paid for, and a long prompt makes the answer harder to check.

Worked example: the parts of the prompt

PartExampleWhat it is for
RuleAnswer only from the passages belowKeeps the answer grounded
Citation ruleCite each passage you use like [1]Makes the answer checkable
Way outOtherwise reply exactly: I do not knowStops a made-up answer
Passages[1] On Saturday the library opens at 10 amThe only facts allowed
QuestionWhen does the library open on Saturday?What to answer

The cells students fill in are left blank in this preview.

Check what comes back

A prompt asks. It does not force. The model may cite a passage that does not exist, add a fact that was not given or forget to cite at all.

So the program checks. It finds the citation markers and looks each one up. An answer that cites nothing is not shown as sourced.

Worked example: what to do with each reply

The replyWhat the program does
Cites [1] and passage 1 existsShow it with the source of passage 1
Cites [7] and there are two passagesDo not trust it
Cites nothingDo not show it as sourced
I do not know.Show that, with no sources

The cells students fill in are left blank in this preview.

Ask once and check the reply

The next Runs write two files, then make one short call. The first file, model.js, sends a prompt to the lab's model. If the model does not answer, it prints Recorded sample and uses a reply written in the script.

The second file, answer.js, builds the prompt, asks, finds the citations and says whether the reply refused. Nothing personal goes in a prompt.

Write the model helper

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' 'async function ask(messages, sample, max) {' '  try {' '    const res = await fetch(base + \"/v1/chat/completions\", {' '      method: \"POST\",' '      headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" },' '      body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 200 }),' '      signal: AbortSignal.timeout(30000),' '    });' '    if (res.ok === false) throw new Error(\"status \" + res.status);' '    return (await res.json()).choices[0].message.content;' '  } catch (error) {' '    console.log(\"Recorded sample: no model answered (\" + error.message + \").\");' '    const list = [].concat(sample);' '    const turn = messages.filter(function (m) { return m.role === \"assistant\"; }).length;' '    return list[Math.min(turn, list.length - 1)];' '  }' '}' 'module.exports = { ask };' > model.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Write the answer script, part 1

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' '// The passages a retriever found, written here so the Run costs one short call.' 'const passages = [' '  { source: \"hours-weekend\", text: \"On Saturday the library opens at 10 am and closes at 2 pm. It is closed on Sunday.\" },' '  { source: \"hours-week\", text: \"The library opens at 9 am and closes at 6 pm from Monday to Friday.\" },' '];' 'const question = \"When does the library open on Saturday?\";' 'function buildPrompt(question, passages) {' '  const lines = [\"Answer only from the passages below. Cite each passage you use like [1]. If the passages do not hold the answer, reply exactly: I do not know.\", \"\"];' '  passages.forEach(function (one, i) { lines.push(\"[\" + (i + 1) + \"] \" + one.text); });' '  lines.push(\"\", \"Question: \" + question);' '  return lines.join(\"\n\");' '}' > answer.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Write the answer script, part 2

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' '// Recorded sample, used only when no model answers.' 'const sample = \"It opens at 10 am on Saturday [1].\";' 'function citedSources(reply) {' '  const found = [];' '  (reply.match(/\[(\d+)\]/g) || []).forEach(function (mark) {' '    const one = passages[Number(mark.slice(1, -1)) - 1];' '    if (one && found.indexOf(one.source) < 0) found.push(one.source);' '  });' '  return found;' '}' 'async function main() {' '  const prompt = buildPrompt(question, passages);' '  console.log(prompt);' '  const reply = await ask([{ role: \"user\", content: prompt }], sample, 120);' '  console.log(\"Answer:\", reply);' '  const cited = citedSources(reply);' '  console.log(\"Sources:\", cited.length > 0 ? cited.join(\", \") : \"none cited\");' '  console.log(\"Says I do not know:\", /i do not know/i.test(reply) ? \"yes\" : \"no\");' '}' 'main();' >> answer.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Run it and read the answer

Command
docker exec -w /home/node/project agent node answer.js

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

One possible output

LineWhat it may show
The promptThe rules, two numbered passages and the question
AnswerIt opens at 10 am on Saturday [1].
Sourceshours-weekend
Says I do not knowno

The cells students fill in are left blank in this preview.

A common mistake

Warning

Many people think that telling a model to use only the passages makes it do so. The rule lowers the chance of a made-up answer. It does not remove it. Keep the check, and change the question to one the passages cannot answer to see the way out work.

About this lab and its source: AI coding agent lab: use opencode to explain and fix code, and check its work

Teaching a class? Set this module for your students with a teacher guide, marked assignments and a lab for every student.