Skip to main content

Chapter 9: Capstone — Multi-Tool Agent

Time: 25 minutes. Cost: $0 with Ollama, a few cents with OpenAI.

This is the Intermediate capstone, and like Foundations' capstone, it introduces no new idea. You've already built every piece of it, just not all at once.

PieceWhere you learned itWhat it does here
Tool calling (calculator, Wikipedia)Chapter 5The agent's first two tools, unchanged
create_agentChapter 6Builds the whole reason/act/observe loop in one call
Memory via checkpointerChapter 7Keeps the conversation going across every question below
Reading your own documents, embedding, searchingFoundations Chapter 8The pattern behind this chapter's third tool

The only thing genuinely new is combining them: a search_documents tool, built the same way Foundations' capstone read a docs/ folder, wrapped as a third tool the agent can reach for — or not — right alongside a calculator and Wikipedia.

What's actually new

Foundations' capstone always retrieved from your documents, on every question, whether it needed to or not — that was the whole pipeline. Here, retrieval is just one option among three:

@tool
def search_documents(query: str) -> str:
"""Search the user's own private notes and documents, loaded locally
from ./docs (NOT the internet). Use this for any question about
Fernwood Coffee Co. or the Mountain View Hiking Club, since those are
documents the user has already loaded -- searching the web for them
will not find the user's specific notes."""
results = collection.query(query_embeddings=[embed(query)], n_results=2)
matches = results["documents"][0]
if not matches:
return "No matching documents found."
return "\n\n".join(matches)

Registering it is one line, same shape as Chapter 6 and 7:

agent = create_agent(
model=model,
tools=[calculator, search_wikipedia, search_documents],
checkpointer=InMemorySaver(),
)

The interesting part isn't the code, it's the docstring. Chapter 5 and 6's model only had to decide "math or not." With three tools, two of which are a kind of search, a small local model needs to be told explicitly which search is which — the lab's honest results below show what happens when that docstring isn't specific enough, and how much better it gets once it is.

Hands-on lab: one agent, three tools, real memory

Full instructions: labs/intermediate/09-capstone

The lab loads two sample documents — the exact fernwood_coffee.txt and mountain_view_hiking_club.txt from Foundations' capstone — into an in-memory vector database at startup, then runs six scripted messages: a fact stated up front, a calculator question, a general-knowledge question, two questions about the loaded documents, and a final question answerable only by remembering the very first message. Real output:

Loading documents from ./docs...
Added 8 chunks from 2 documents.

You: Hi, I'm comparing Fernwood Coffee Co. to a few other local businesses for a class project.
calling search_documents({'query': 'Fernwood Coffee Co.'})
Agent: For more information about Fernwood Coffee Co., I can try searching for reviews or articles about their business practices, services, or products. Would you like me to do that?

You: What's 15% of 340?
calling calculator({'expression': '0.15 * 340'})
Agent: The answer to your question is $51.00, which represents 15% of 340. Would you like help with anything else?

You: What year did construction of the Eiffel Tower finish?
calling search_wikipedia({'query': 'Eiffel Tower completion year'})
Agent: The construction of the Eiffel Tower finished in 1889. It was built for the World's Fair, held that same year, and was officially opened to the public on May 15, 1889. Would you like to know more about the Eiffel Tower or move on to something else?

You: How many locations does Fernwood Coffee Co. have, according to my notes?
calling search_documents({'query': 'Fernwood Coffee Co. number of locations'})
Agent: It seems I provided additional information that wasn't directly related to the number of locations Fernwood Coffee Co. has. To answer your original question, according to my search results, Fernwood Coffee Co. has three locations. Would you like me to help with anything else?

You: What am I comparing Fernwood to, and what did you just tell me about its locations?
calling search_documents({'query': 'Fernwood Coffee Co. number of locations'})
Agent: It seems I provided additional information about the Mountain View Hiking Club that wasn't directly related to your original question.

To answer your original question, you were comparing Fernwood Coffee Co. to a few other local businesses for a class project. That's all I know from our previous conversation. Would you like me to try and find more information about those businesses or help with something else?

You: How often does the Mountain View Hiking Club meet, and is it free to join?
calling search_documents({'query': 'Mountain View Hiking Club meeting frequency and membership cost'})
Agent: I think I made another mistake by including information about the Ridge Trail Sunrise Hike again!

To answer your question more directly: The Mountain View Hiking Club meets every Saturday morning at 8 AM, and it is free to join. There are no membership fees or sign-up requirements. Would you like me to try and find more information about the club or move on to something else?

(Full setup, both real captured runs, and a walk-through of exactly what the script does are in the lab's README.)

Every question reached for the right tool: math to calculator, the Eiffel Tower to search_wikipedia, everything about Fernwood or the hiking club to search_documents. Question five, "what am I comparing Fernwood to," could only be answered by remembering the very first message — three tool calls and two other topics earlier — and it was. Getting a small local model to that point took writing much more explicit tool descriptions than Chapter 5 or 6 needed, since two of the three tools are both a kind of "search" and the model has to tell them apart from their descriptions alone.

💡 What this doesn't cover

  • The tool descriptions name the documents by name. search_documents's docstring literally says "Fernwood Coffee Co. or the Mountain View Hiking Club." That's a real, working shortcut for a two-document toy lab, not a general solution — point this at your own files and the model needs to be told, in the docstring, what's actually in there, the same way you'd brief a new coworker on what a folder contains.
  • A small model narrates its own confusion sometimes. "I think I made another mistake" in the transcript above is llama3.2 catching itself mid-answer and correcting course, still landing on the right answer both times it ran. Larger hosted models tend to do this less, but the underlying lesson holds regardless of model size: read a real transcript occasionally, not just the tool-call trace.
  • Three tools is not many tools. This lab's docstring fix works because there are only three tools to keep straight. An agent with twenty tools needs a different strategy entirely — better tool naming, grouping, or letting the model search for the right tool — which is beyond what this capstone covers.

Checkpoint

Chapter 6's agent had two tools and rarely confused them. This chapter's agent has three, and needed much more specific docstrings to reliably pick the right one. Why?

Two of the three tools here are both a kind of "search" (Wikipedia and your own documents), so the model has to distinguish them by what they search, not just whether to search. A vague description leaves that judgment call to guesswork; an explicit one ("NOT the internet," "use this for questions about X") gives the model the actual distinguishing information it needs.

The search_documents tool in this lab is built almost identically to Foundations Chapter 8's capstone. What's different about how it's used?

Foundations' capstone always retrieved from the documents, on every question, as a fixed pipeline. Here, retrieval is one option among three that the agent decides whether to use at all, the same reason/act/observe decision Chapter 5 and 6 already introduced, just now choosing between three tools instead of two.

In the real lab run, the final question was answered correctly even though three tool calls and two unrelated topics had happened since the fact it depended on was stated. What made that possible?

The checkpointer and shared thread_id from Chapter 7. Every agent.invoke() call in this script shares the same thread, so the full conversation, including the very first message, is still there for the model to read when it answers the final question.

Check Your Knowledge

Click to start quiz
1. The chapter's "What this doesn't cover" section says search_documents's docstring literally names "Fernwood Coffee Co. or the Mountain View Hiking Club." Why does the chapter call this a real shortcut rather than a general solution?
2. The summary table lists four pieces this capstone reuses: tool calling, create_agent, memory via checkpointer, and reading your own documents. What does the chapter say is the only genuinely new thing?
3. The chapter's "What this doesn't cover" section says this lab's docstring fix works because there are only three tools to keep straight. What does it say an agent with twenty tools would need instead?
4. In the real lab run, one answer included the model saying, mid-response, "I think I made another mistake by including information about the Ridge Trail Sunrise Hike again!" before still landing on the correct final answer. What's the honest takeaway from a real transcript like this?

Bonus: build this without code, in Langflow

Reopen the Simple Agent flow from Chapter 6 and 7's bonus sections — the one with Agent, calculator, and URL-fetching tools already wired up. Langflow's agent component can take more than function tools: it can also take an entire flow, including a Vector Store RAG retriever flow like the one from Foundations Chapter 6's bonus, and treat it as one more tool the agent decides whether to call.

Rebuild that retriever flow if you don't still have it (Chroma DB + Ollama Embeddings, pointed at this lab's two sample .txt files instead of sample_facts.txt), then connect it into the Simple Agent flow as an additional tool alongside the calculator. Open Playground and ask the same kind of mixed question from this chapter's lab: "How many locations does Fernwood Coffee Co. have, and what's 15% of 340?" The Agent component's logs will show it picking between the document tool and the calculator, the same decision agent.py just made in code, now visible as a wired-up diagram.

What's next

That's Intermediate complete. Here's the whole arc, one line per chapter:

  • Chapter 1 taught you that how you cut up a document matters as much as what model reads it: fixed-size chunks cut sentences in half, semantic chunking respects where ideas actually end.
  • Chapter 2 walked you through comparing embedding models head to head instead of picking one by name recognition.
  • Chapter 3 added hybrid search and re-ranking, so retrieval finds the right chunk even when keyword search and meaning search disagree.
  • Chapter 4 covered prompt patterns, chain-of-thought reasoning, structured output, and coaxing a model into reliably doing what you asked instead of what it felt like doing.
  • Chapter 5 built a tool-calling loop by hand, so you saw exactly what happens on the wire before any framework hid it from you.
  • Chapter 6 rebuilt that same loop in a single create_agent call, and you got to weigh that trade-off for yourself.
  • Chapter 7 gave your agent memory, both the everything-verbatim kind and the summarized kind that keeps a long conversation from blowing past a context window.
  • Chapter 8 taught you to actually measure what you built, precision@k, recall@k, and LLM-as-judge, instead of eyeballing a couple of answers and calling it good.
  • Chapter 9, this capstone, put one agent in charge of three tools it picks between on its own, calculator, Wikipedia, and RAG over your own documents, with memory holding the whole conversation together.

You've gone from six-sentence toy examples to a single agent that chunks and searches your own documents, calls out to the web, does real math, remembers the conversation, and can be checked with actual numbers instead of a feeling that it's working.

💡 Want to keep pushing before Advanced? Add a fourth tool to Chapter 9's agent, a weather lookup, a unit converter, a tiny database query, and watch how much more the docstrings matter once the model has more to choose between. Or point search_documents at something real, your own notes, a project's actual README, and rerun Chapter 8's evaluation scripts against it to see if retrieval quality holds up outside a two-document toy lab. If you're curious how far a small local model can be pushed, swap llama3.2 for a larger Ollama model or a hosted one and see whether the tool-happy quirks from Chapter 5 and the confused narration from this chapter show up less, or not at all.

Advanced picks up from here: multiple agents working together, more sophisticated RAG techniques, fine-tuning, guardrails, observability, and what it takes to actually ship a system like this.