Retrieval as a tool the agent calls
In this lesson the agent gets a tool that looks things up. The concepts course showed retrieval as a search that runs before the model answers, so that the right passage is in the prompt when the model reads the question, and it showed what a keyword search gets wrong. Here we build that search as a tool, run it two ways against a twelve-document company handbook, and measure each way against a small test set. The basic version calls the tool once per question. The loop version lets the agent call it, read the result, judge it and call it again, and the lesson ends with the rule for choosing between them.
Every example runs against a fixture with a fake model, so you can
predict the output and check yourself. The complete program is
site/examples/building-agents/retrieval-as-a-tool/agent.py in the
repository, with the twelve texts under docs/ next to it. Copy the
folder, and each step below is python3 agent.py <step>. The fake model
answers with the sentence of the retrieved passage that shares the most
keywords with the question. That’s a deterministic replacement for a
real model reading the passage, and it makes every run print the same lines. The fixture reads
its documents with a glob over docs/*.txt, and a stray file in the
folder doesn’t change the output.
The search is a tool
Section titled “The search is a tool”A tool is a function plus a description, as
Building your first agent showed. The
search here is one more entry in the same TOOLS dictionary. It splits
the query into keywords, drops common words, scores each document by how
many keywords it contains, and returns the best one. The skip argument
names documents already read. A second search with skip set finds a
different document.
def search_docs(query: str, skip: Optional[list[str]] = None) -> dict: terms = keywords(query) scores = { name: sum(1 for term in terms if term in set(words(text))) for name, text in CORPUS.items() if name not in (skip or []) } if not scores: # every document was skipped, or docs/ is empty return {"ok": True, "name": None, "passage": ""} best = max(scores, key=lambda name: scores[name]) # the first document wins a tie if scores[best] == 0: return {"ok": True, "name": None, "passage": ""} return {"ok": True, "name": best, "passage": CORPUS[best]}
TOOLS = { "search_docs": { "fn": search_docs, "description": ( "Search the handbook. Returns the best-matching passage. " "Args: query (str), skip (list of str, optional): names of documents already read." ), },}With a real model, the description is what the model reads to decide
when to call the tool, and the passage goes back to the model as a
tool_result block in the next user message [1]
[2]. In this fixture the passage goes into a
prompt built by build_prompt, with a frame above it, and the question
below it. The frame tells the model where the text came from and what to
do when it doesn’t answer the question, and the last section of this
lesson shows what happens without it.
The prompt the model gets
Section titled “The prompt the model gets”Run this, and compare what you see with the output below.
result = TOOLS["search_docs"]["fn"]("How many days of annual leave do I get?")print(build_prompt(result["passage"], "How many days of annual leave do I get?"))The passages below were retrieved from the company handbook for this question. Answer from them. Quote the sentence you relied on. If the passages do not cover the question, answer exactly: not found. Passages: Annual leave. Every employee accrues 25 days of annual leave per year. Unused days carry over until 31 March. Requests go to your manager at least two weeks ahead. Question: How many days of annual leave do I get?
Output verified in CI from site/examples/building-agents/retrieval-as-a-tool/prompt.py.
Basic retrieval against a test set
Section titled “Basic retrieval against a test set”The basic version is the fixed pipeline from the concepts course: search once, put the passage in the prompt, ask [3]. The harness calls the tool before the model sees the question, and the model doesn’t decide anything about retrieval.
def answer_basic(question: str) -> dict: result = TOOLS["search_docs"]["fn"](question) reply = fake_model(build_prompt(result["passage"], question)) return {"name": result["name"], "answer": reply, "rounds": 1}A test set decides whether this is good enough. This one has six questions in the handbook’s own words, except the second, which asks about holiday where the handbook says leave. The concepts course showed what a keyword search does with that.
TEST_SET = [ "How many days of annual leave do I get?", "How much holiday can I take?", "What is the training budget per year?", "How many days per week can I work from home?", "How many days ahead are flights booked?", "Who do I report a lost laptop to?",]The twelve handbook files are data-team.txt, expenses.txt,
laptops.txt, leave.txt, office-hours.txt, onboarding.txt,
payments-team.txt, remote-work.txt, security.txt,
service-desk.txt, training.txt and travel.txt. Each file holds a
few sentences on the topic its name gives. The list_sources function
runs the basic version on each question. For each question it prints a
line in the form <n>. <file>, where the file is the source of the
answer. A question without an answer gets <n>. not found. The last line
is answered: <count> of 6.
Which questions does the basic version answer?
Section titled “Which questions does the basic version answer?”The lesson's fixture runs a keyword search once per question over a twelve-document company handbook and answers with the best sentence of the retrieved passage. The files are data-team.txt, expenses.txt, laptops.txt, leave.txt, office-hours.txt, onboarding.txt, payments-team.txt, remote-work.txt, security.txt, service-desk.txt, training.txt and travel.txt, and each holds a few sentences on the topic its name gives. The six questions are: 1. How many days of annual leave do I get? 2. How much holiday can I take? 3. What is the training budget per year? 4. How many days per week can I work from home? 5. How many days ahead are flights booked? 6. Who do I report a lost laptop to? Five are in the handbook's own words, and the second asks about holiday where the handbook says annual leave. The fixture prints one line per question in the form <n>. <file>, with the file the answer came from, or <n>. not found when there is no answer, and a last line answered: <count> of 6.
Predict the output first, one <n>. <file> or <n>. not found line
per question and the answered: <count> of 6 line. Then run this.
list_sources(TEST_SET)1. leave.txt 2. not found 3. training.txt 4. remote-work.txt 5. travel.txt 6. laptops.txt answered: 5 of 6
Output verified in CI from site/examples/building-agents/retrieval-as-a-tool/basic.py.
Which question uses a word that no document contains, and what does the search return then?
The run_test_set function prints each question with the file it
retrieved, or nothing retrieved, and its answer, and the same count at
the end.
The answers of the basic version
Section titled “The answers of the basic version”Run this, and compare what you see with the output below.
run_test_set(TEST_SET)1. How many days of annual leave do I get? leave.txt: Every employee accrues 25 days of annual leave per year. 2. How much holiday can I take? nothing retrieved: not found 3. What is the training budget per year? training.txt: Every employee has a training budget of 1500 euros per year. 4. How many days per week can I work from home? remote-work.txt: Staff may work from home up to three days per week. 5. How many days ahead are flights booked? travel.txt: Flights are booked through the travel desk at least 14 days ahead. 6. Who do I report a lost laptop to? laptops.txt: Report a lost laptop to the service desk the same day. answered: 5 of 6
Output verified in CI from site/examples/building-agents/retrieval-as-a-tool/basic_answers.py.
Five of six, and the miss is the question in a colleague’s words. A better index fixes that one: a similarity search that scores meaning, or a synonym list that maps holiday to leave. Nothing about the loop is needed for it, and the next section is about the one failure a better index doesn’t fix.
One question that needs two searches
Section titled “One question that needs two searches”Question seven asks who approves a replacement laptop for the payments team. The laptops text says the cost center owner of the team approves it and sends the reader to the team pages, and the payments team’s text names its cost center owner. The answer is in two documents, and the basic version reads one.
The basic version on question seven
Section titled “The basic version on question seven”Run this, and compare what you see with the output below.
show(7, MULTI_HOP, answer_basic(MULTI_HOP))7. Who approves a replacement laptop for the payments team? laptops.txt: The cost center owner of your team approves a replacement laptop before that, see the team pages.
Output verified in CI from site/examples/building-agents/retrieval-as-a-tool/multi_hop_basic.py.
The answer is a pointer, and the person asking still has no name. Retrieval Augmented Generation (RAG) is the full name behind the letters in the agentic RAG loop, and the loop puts the agent in charge of the search: it forms a query, reads what comes back, judges whether it answers the question, and searches again with a new query when it doesn’t [3]. Question seven is a multi-hop question, where the first result says what to search for next, and that’s the case the loop is for.
def answer_with_loop(question: str, max_rounds: int = 3) -> dict: query = question read: list[str] = [] for round_number in range(1, max_rounds + 1): result = TOOLS["search_docs"]["fn"](query, skip=read) if result["name"] is None: return {"name": None, "answer": "not found", "rounds": round_number} read.append(result["name"]) sentence = best_sentence(query, result["passage"]) print(f"round {round_number}: search_docs(query={query!r}) -> {result['name']}") if is_pointer(sentence): # Follow the pointer: keep the question's words and add the sentence's. query = " ".join(keywords(question + " " + sentence)) continue if covers(query, sentence): return {"name": result["name"], "answer": sentence, "rounds": round_number} query = question return {"name": None, "answer": "not found", "rounds": max_rounds}The judging step is where a real model reads the passage and decides. In
the fixture, is_pointer is that judgment written as a rule: a sentence
that says “see” another page is a pointer, and the agent builds the next
query from the question’s keywords and the pointer’s. Documents already
read are skipped. The second search returns a document the loop hasn’t
read. max_rounds bounds the loop, for the same reason every loop in
this course has a step limit.
How many search rounds does the loop take?
Section titled “How many search rounds does the loop take?”The lesson's fixture has a retrieval loop of at most three rounds. Each round searches a twelve-document handbook, skips documents already read, and follows a sentence that points at another page by adding its keywords to the query. Question seven asks who approves a replacement laptop for the payments team, and the laptops text points at the team pages.
Predict the number on the rounds line, then run this.
result = answer_with_loop(MULTI_HOP)show(7, MULTI_HOP, result)print(f"rounds: {result['rounds']}")round 1: search_docs(query='Who approves a replacement laptop for the payments team?') -> laptops.txt round 2: search_docs(query='approves center cost laptop owner pages payments replacement team') -> payments-team.txt 7. Who approves a replacement laptop for the payments team? payments-team.txt: The cost center owner of the payments team is Maria Duarte, head of platform. rounds: 2
Output verified in CI from site/examples/building-agents/retrieval-as-a-tool/multi_hop_loop.py.
How many documents hold a piece of the answer, and does the loop ever read the same one twice?
Two rounds, and the second query is the question plus the words of the
pointer. The data team’s text also names a cost center owner, and it
scores four on that second query against five for the payments team’s
text. The word payments, carried over from the question, is the
difference. A real loop costs a model call per round on top of the search, so this
question costs about twice what a basic question does.
A question the handbook doesn’t answer
Section titled “A question the handbook doesn’t answer”Question eight asks whether you can bring a dog to the office. No text
covers it, but two texts contain the word office. The search
returns one of them, and the model gets a passage about opening hours.
A model that isn’t told to ground its answer can answer from a passage
that doesn’t cover the question, and the fix is an instruction that tells it to
base the answer on the retrieved text [3]. The fixture’s fake_model
at min_shared=1 behaves like the model without that instruction: any
overlap between question and passage becomes an answer.
Without the frame, and with it
Section titled “Without the frame, and with it”Run this, and compare what you see with the output below.
print("without the frame:")show(8, NO_ANSWER, answer_basic(NO_ANSWER, min_shared=1))print("with the frame:")show(8, NO_ANSWER, answer_basic(NO_ANSWER))without the frame: 8. Can I bring my dog to the office? office-hours.txt: Core hours, when everyone in the office is reachable, are 10 to 15. with the frame: 8. Can I bring my dog to the office? office-hours.txt: not found
Output verified in CI from site/examples/building-agents/retrieval-as-a-tool/no_answer.py.
The first answer is a sentence about core hours, offered as the dog
policy. The frame is the string FRAME at the top of the fixture. It
says the passages were retrieved for this question, asks the model to
quote the sentence it relied on, and names the answer to give when the
passages don’t cover the question: not found. A real model reads that
text. The fake model obeys it through covers, which asks for at least
MIN_SHARED keywords of the question in the answering sentence, and
that check is what turns the second run into not found. An agent that
fails to find an answer should say that it couldn’t find one [3]. The way
to know yours does is a test set with questions like this one, whose
answer is in no document, with not found as the expected answer.
What does the frame add?
Section titled “What does the frame add?”The lesson builds a prompt from a frame, a retrieved passage and the question. The frame says where the passages came from, asks for the sentence the answer relied on, and names the reply to give when the passages do not cover the question.
The same passage was retrieved twice and the answers differ. What did the frame change?
What did the model do with the opening-hours passage when nothing told it where the text came from?
When basic retrieval is enough
Section titled “When basic retrieval is enough”Basic retrieval is the right first version. Search once, put the top passages in the prompt, answer, and it handles direct questions whose answer is in one document, at one model call per question [3]. The loop costs a model call per round plus the search and needs a bound, and on the five questions the basic version already answered it would have given the same five answers for more money. Add the loop when a test set shows the basic version failing on a question a better index can’t fix, as question seven did, and not before.
That order matters because the test set is what tells the two failures apart. Question two failed because of the index, and a synonym or a similarity search fixes it. Question seven failed because the answer is in two places, which a second search fixes and a better index doesn’t. Question eight didn’t fail at the search step at all, and the frame fixes it in both versions. Without the six questions, every failure looks like a reason to add the loop.
Which failure calls for the loop?
Section titled “Which failure calls for the loop?”The lesson's test set had three kinds of failure: a question in other words than the documents use, a question whose answer is spread over two documents, and a question no document answers. A basic retrieval step searches once, and the loop searches again after reading the first result.
A team is deciding whether to add the retrieval loop. Which of these failures on their test set is the reason to add it?
Which failure would a second, better-aimed search fix, and which would a better index or a better prompt fix?
A question in other words
Section titled “A question in other words”A basic retrieval step searches a handbook once per question. On the test set, a question about holiday retrieves nothing, because the handbook only says annual leave.
Is this failure a reason to add the retrieval loop?
Would searching again with the same index find a word that no document contains?
What goes in the frame?
Section titled “What goes in the frame?”The lesson puts a frame around retrieved text before the question, so that the model answers from the passages and says so when it can't.
Which two lines belong in the frame around the retrieved text?
Which lines tie the answer to the passages, and which let the model leave them?
Which fix for which failure?
Section titled “Which fix for which failure?”The lesson's test set showed three kinds of retrieval failure, and each has its own fix.
Match each failure to its fix.
Is the search missing words, is the answer in two places, or is there no answer at all?
Twenty out of twenty
Section titled “Twenty out of twenty”A team's retrieval test set has 20 questions, and the documents answer all of them. The agent passes all 20. In production it gives confident answers to questions the documents don't cover.
The agent passes the whole test set and invents answers in production. What do you change first?
Which kind of question is missing from the test set?
Exercise
Copy the retrieval-as-a-tool folder from the repository. Add the
question “How many days of parental leave do I get?” to TEST_SET, run
python3 agent.py basic_answers, and read what the basic version answers. The
handbook has no parental leave policy. The leave text shares two
keywords with the question, and two is what covers asks for. That’s enough for the
answer to go through. Then raise MIN_SHARED to 3 and run the step again. Doing
this once shows you how a wrong passage clears a weak coverage check,
and what a stricter one costs.
A good result: before the change, the new question gets the annual leave
sentence as its answer and the count says 6 of 7. After the change, it
gets not found, the count says 5 of 7, and the other six lines are the
same as before. Run python3 agent.py multi_hop_loop as well and check
that question seven still takes two rounds. How many of the six original
questions would a threshold of 4 break, and what does that say about
thresholds as a replacement for a model’s judgment?
Stretch: Add the question 'How many days of unpaid leave can I take per year?' and run python3 agent.py basic_answers again. It shares three keywords with the leave text and still gets the annual leave sentence. Write down what a real model needs from the frame to answer not found here, and why a keyword count can't do it.
Recap
- Retrieval is a tool like any other in the
TOOLSdictionary. The basic version has the harness call it once before the model, and the agentic version lets the model call it, judge the result, and call it again [3]. - Start with basic retrieval and a test set. Add the loop only for the failures a test set shows and a better index can’t fix, such as an answer spread over two documents [3].
- Frame retrieved text: say where it came from, ask for the sentence relied on, and name the answer to give when it doesn’t cover the question. A model without a grounding instruction can answer from a passage that doesn’t cover the question [3].
- An agent that doesn’t find an answer says so [3]. A test set for
retrieval holds questions with no answer in the documents, and the
expected answer for them is
not found.
You can now
- Adds retrieval or memory and knows when basic RAG is enough
References
Section titled “References”- Anthropic. Define tools. Claude Platform documentation. Reference.
Claude docs define-tools - Anthropic. Handle tool calls. Claude Platform documentation. Reference.
Claude docs handle-tool-calls - Addy Osmani, Ivar Soares Urdalen, Leo Simons. Agentic RAG: the retrieve, evaluate, refine loop; when basic RAG is enough. Agent Engineer Course. Course.
AEC-08