KevsRobots Learning Platform
75% Percent Complete
By Kevin McAleer, 7 Minutes
Page last updated August 23, 2026

A RAG tool that answers confidently but cannot show its working is worse than no tool at all - because you will believe it.
This lesson is about the difference between sounding grounded and being grounded.
When the answer says:
You swapped the DRV8833 for a TB6612FNG because it was running hot [1], and the N20 motors still stalled on carpet afterwards [2].
…you can click through to robots/SMARS.md and daily/2026-08-20.md and check. Ten seconds, and you know whether to trust it.
Without those markers you have a paragraph that might be a faithful summary of your notes, might be the model’s general knowledge about motor drivers, or might be a blend. There is no way to tell by reading it, and the blend is the dangerous one.
Our CLI prints the answer, then the sources it was given:
# a preview of cmd_ask - it lands in vault_rag/cli.py in lesson 15
for piece in stream_answer(question, hits):
print(piece, end="", flush=True)
print("\n\nSources:")
for number, hit in enumerate(hits, start=1):
print(f" [{number}] {hit.path} ({hit.score:.0%})")
You swapped the DRV8833 for a TB6612FNG [1]. Your notes do not say why -
the daily note from that week only mentions the motors stalling on carpet [2].
Sources:
[1] robots/SMARS.md (52%)
[2] Daily.md (20%)
Note that we list everything we sent, not only what got cited. That is deliberate. Seeing that source 4 was retrieved and not used tells you as much as seeing the ones that were - it usually means retrieval pulled in something irrelevant, which is a signal worth having.
The model can hallucinate a citation number. It is rare with a clear prompt, but “rare” is not “never”, and this is cheap to check:
# vault_rag/answer.py (add `import re` at the top, then this at the bottom)
import re
CITATION_RE = re.compile(r"\[(\d+)\]")
def check_citations(text: str, hit_count: int) -> tuple[set[int], set[int]]:
"""Return (cited, invalid) source numbers found in the answer."""
cited = {int(n) for n in CITATION_RE.findall(text)}
invalid = {n for n in cited if n < 1 or n > hit_count}
return cited - invalid, invalid
Wire it in after the answer arrives:
# also destined for cmd_ask in vault_rag/cli.py (lesson 15)
cited, invalid = check_citations(answer_text, len(hits))
if invalid:
print(f"\nWarning: answer cited sources that do not exist: {sorted(invalid)}")
unused = set(range(1, len(hits) + 1)) - cited
if unused:
print(f"Retrieved but unused: {sorted(unused)}")
Two useful signals for a few lines of code:
TOP_K is too high or your chunking needs work. Lesson 17 turns this into a proper measurement.This is the hard part. A language model’s default behaviour is to be helpful, and “I don’t know” does not feel helpful. You have to make it explicitly acceptable.
Three things that work, in order of effect:
Give it a preferred alternative. “Say so plainly and suggest what note the user might need to write” replaces the unhelpful answer with a different helpful one. The model has somewhere to go.
Ban the fallback explicitly. “Never invent a fact to fill the gap” is a direct instruction, and direct instructions land better than implied ones.
Never call when there are no hits. Our answer() returns early on an empty hit list. There is no such thing as a well-grounded answer with no grounding.
There is a fourth trick that is easy to miss: let weak results through. It is tempting to filter out low-scoring hits so the model only sees good material. But a genuinely weak result set is information - it means your notes probably do not cover this. Hand it over and let the model say “these excerpts are about motors generally, but nothing here mentions the stall current you are asking about”. That is the right answer.
Do not assume it works. Test it:
# try_it.py - a scratch script in the project root
questions = [
"what did I write about the Voyager 2 mission?", # not in the vault
"what is the capital of France?", # general knowledge
"what did I decide about the motor driver?", # in the vault
]
for question in questions:
hits = search(question, db_path="testdb")
print(f"\n=== {question}")
print(answer(question, hits))
You want the first two to decline, and the third to answer with citations. The second is the sharper test - the model absolutely knows the capital of France, and a properly grounded assistant will still say “your notes do not cover this”.
If it answers “Paris”, your system prompt is not strong enough. Make the ONLY line more emphatic and try again.
Even a well-grounded answer can be wrong in a specific way: it can be a faithful summary of a note that was wrong. If you wrote “the DRV8833 handles 2A per channel” and that was a misreading of the datasheet, the tool will repeat it back to you with a citation, and the citation makes it feel more true.
RAG grounds answers in your notes. It does not grade your notes. That is a genuine limitation and worth holding in mind - the citation tells you where a claim came from, not that the claim is correct.
The practical upshot: use the citation. Click through. The point of the link is that you read it.
check_citations to your flow and run ten real questions. Any invalid numbers? How many unused sources on average?Why: Instructions about sources make the model self-conscious about sources. Naming the unwanted phrasing fixes it.
[1] for everything, including claims from source 3.format_sources really is numbering from 1 and the ids appear in the prompt.Why: If the numbering is unclear the model falls back to citing the first source for everything.
Why: Over-refusal is nearly always a retrieval failure wearing a generation costume. Fix the retrieval.
(1) or Source 1 instead of [1].like [1] or [2][3].CITATION_RE depends on exact.
You can use the arrows ← → on your keyboard to navigate between lessons.
Comments