Building Your First Agent: A Worked Example
A worked example that assembles every component from this course -- goal, tools, loop, memory, and guardrails -- into one small agent, plus a survey of open-source frameworks to build on.
Learning objectives
- Assemble a minimal but complete agent from the components covered in this course
- Trace one full run of the example: which lesson each design decision came from
- Choose an open-source framework to start building in, based on what your project needs
ToolDix original visual
Frame
Name the outcome and constraints.
Build
Try one bounded workflow.
Review
Keep evidence, revise, and share.
The task
Build a small research agent: given a question, it should decide whether it needs to search for current information, call a search tool if so, and answer -- stopping after at most three search calls.
Assembling the pieces
Every design choice below traces back to an earlier lesson in this course. We'll build a research agent that demonstrates goal definition, tool design, agentic loops, reasoning, and guardrails all working together.
The system prompt: goal and tool definition
The system prompt establishes the agent's purpose and defines its available tools clearly:
SYSTEM_PROMPT = """You are a research assistant. Goal: answer the user's question accurately.
Tools:
- search(query: string) -> list of {title, url, snippet}
Use this when the question needs current information or a fact
you're not confident about. Do not use it for general knowledge
you already know well.
Rules:
- Make at most 3 search calls per question.
- Before answering, briefly state your reasoning for whether you
need to search.
- If search results don't resolve the question after 3 calls,
say what's still uncertain rather than guessing.
"""
Key design choices:
- Goal stated first (from Lesson 1: What Is an Agent?): The model knows it must answer questions accurately.
- One scoped tool, with a "don't use" clause (Lesson 3: Core Components): The search tool is clearly defined, and the prompt explicitly tells the model when not to use it — for knowledge it already has. This reduces unnecessary API calls.
- Hard limit on calls (Lesson 8: Tokens and Context Windows): 3 search calls is a firm guardrail, regardless of what the model thinks is necessary.
The orchestration loop
The core loop manages the conversation state and enforces stopping conditions:
context = [SYSTEM_PROMPT, user_question]
search_calls = 0
while search_calls < 3:
# Call the model with current context
response = call_model(context)
# Check for final answer (model decides to stop)
if response.is_final_answer:
return response.text
# Execute the tool call (search in this case)
result = search(response.tool_call.query)
# Append both the call and its result to context
context.append(response.tool_call)
context.append(result)
# Increment counter and loop
search_calls += 1
# Fallback: if we exhausted our budget, ask for best-effort answer
return call_model(
context + ["Answer now with what you have, noting any remaining uncertainty."]
)
This loop embodies several principles:
- The agentic loop (Lesson 5: How an Agent Works): model → observe → act → loop.
- State management: The
contextlist is the agent's working memory, growing with each tool result. - Stopping conditions (Lesson 5): The model can stop by returning
is_final_answer: true, and the loop stops after 3 calls regardless. The fallback call at the end (lines 24–26) ensures the agent never loops forever—a concrete guardrail from Lesson 3.
Tracing the design back to earlier lessons
Each part of the agent comes from specific earlier lessons in this course:
| Component | Lesson | |-----------|--------| | Goal stated up front | Lesson 1: What Is an AI Agent? | | One scoped tool with "don't use" clause | Lesson 3: Core Components; Lesson 10: Prompt Engineering | | The while-loop checking for final answer | Lesson 5: How an Agent Works | | "State reasoning before answering" | Lesson 11: Reasoning and Planning Strategies | | Hard limit of 3 calls | Lesson 8: Tokens and Context Windows; Lesson 3: Guardrails | | Fallback answer with uncertainty flag | Lesson 5: Stopping Conditions |
Handling tool results and context growth
Once a tool call is made, the result must be parsed and injected back into context in a way the model can understand:
def handle_search_result(query: str, result: dict) -> str:
"""Format search results in a way the model can reason about."""
snippets = result.get('results', [])
if not snippets:
return f"No results found for '{query}'."
formatted = f"Search results for '{query}':\n"
for i, item in enumerate(snippets[:5], 1): # Top 5 results
formatted += f"{i}. {item['title']}\n {item['snippet']}\n"
return formatted
# In the main loop:
result_text = handle_search_result(query, search_result)
context.append({
"role": "tool",
"tool_name": "search",
"content": result_text
})
This pattern ensures:
- Results are structured clearly so the model understands what was retrieved.
- Token budget is respected: We cap results to the top 5, avoiding bloated context.
- Traceability: Each tool call and result are logged, making debugging straightforward.
Design comparison: single-tool vs. multi-tool agent loop
Before walking through examples, it's worth understanding the tradeoff between simplicity and capability. The single-tool agent above is minimal, but production systems often face pressure to add more tools. Here's a comparison:
| Aspect | Single-Tool Agent | Multi-Tool Agent | |--------|-------------------|------------------| | Setup time | ~2 hours | ~1-2 days (tool integration) | | Avg API calls per task | 1.2 | 2.5–4.0 | | Decision branching complexity | Low (search or answer) | High (which tool? when?) | | User expectation | "Search if needed" | "Intelligently pick the best tool" | | Debugging difficulty | Low (few paths) | High (combinatorial paths) | | Token cost per request (illustrative) | ~$0.008–$0.015 | ~$0.020–$0.045 | | Suitable for | Fact lookup, narrowly-scoped tasks | Cross-domain reasoning, complex workflows |
Key insight: Each additional tool doesn't linearly increase complexity—it adds exponential decision branching. A three-tool agent can easily spawn 2^3 = 8 behavioral paths depending on which tools it calls in which order. Test and monitor carefully before expanding beyond two tools.
Walkthroughs: two example runs
Example 1: Simple fact lookup (1 search call)
Question: "What's the latest version of the Python requests library?"
Run trace:
- Call 1: The model reads the question and system prompt. It recognizes this is a factual question likely to have changed since training. It decides to search and emits:
tool_call(name="search", query="latest version of python requests library"). - Search executes and returns: "requests 2.32.0, released 2024-01-01. Used for HTTP requests in Python..."
- Call 2: The model reads the search result. The version number is clear, so it outputs
is_final_answer: truewith the answer: "The latest version is 2.32.0 (as of my search)." - Loop returns immediately. Total: 1 search call, 2 model calls.
This is the happy path: the model makes one targeted search and finds what it needs.
Example 2: Complex analysis (3 search calls)
Question: "What are the tradeoffs between three specific caching strategies: LRU, LFU, and ARC?"
Run trace:
- Call 1: The model decides this requires explanation of distinct algorithms. It searches:
"LRU cache algorithm explanation". - Result: Medium-length explanation of Least Recently Used caching.
- Call 2: Having read the LRU overview, the model realizes it needs specifics on the other two. It searches:
"LFU LRU ARC cache algorithms comparison". - Result: A comparison table is found.
- Call 3: The model reads the comparison but wants to understand one edge case better. It searches:
"ARC cache adaptive replacement vs LRU practical use cases". - Result: Use-case guide is returned, but the model is still slightly uncertain about one detail.
- Loop has now reached
search_calls == 3. The while loop terminates, and the code calls the model one final time with the instruction:"Answer now with what you have, noting any remaining uncertainty." - Call 4 (fallback): The model synthesizes all three search results and outputs: "LRU is simple and works well for... LFU optimizes for frequency and is better for... ARC adapts between both but is more complex. I'm confident about X and Y, but Z would require deeper investigation."
- Total: 3 search calls + 1 fallback call = 4 model calls.
This demonstrates the guardrail in action: the agent never loops forever. After 3 searches, it's forced to give a best-effort answer, even if it hasn't reached complete confidence. This is a hard stopping condition (from Lesson 3) that prevents unbounded resource use.
Case Study: E-commerce Product Lookup Agent
A mid-sized e-commerce company built a customer service agent to answer product questions. They started with a single search tool that queries their product database. Within the first week, they noticed:
- Happy path: ~70% of customer questions resolved in 1 search call (e.g., "Is the XL shirt available in blue?").
- Edge case: ~15% required 2 calls (customer asks "how does this compare to the competitor's version?" → agent searches their catalog, then searches competitor data).
- Out of scope: ~15% needed human escalation (product recommendations, complaints, custom requests).
After instrumenting the loop with timeout handling and tracking, they discovered that slow product database queries (>3 seconds) caused user frustration. They added a timeout of 5 seconds per search call. If a search timed out, the agent would return "I couldn't find that info; let me escalate to an expert." This led to happier users (faster fallback) and clearer escalations for the support team.
Lesson: A simple, single-tool agent that gracefully degrades is often more robust than a complex multi-tool agent that tries to handle every case.
Implementation considerations and error handling
In a real system, several details become important:
Tool timeout and retry logic
def search_with_timeout(query: str, timeout_seconds: int = 10) -> dict:
"""Call search API with timeout and retry once on failure."""
import time
for attempt in range(2):
try:
return search_api(query, timeout=timeout_seconds)
except TimeoutError:
if attempt == 0:
time.sleep(1) # Brief backoff before retry
continue
# If retry also fails, return an empty result instead of crashing
return {"results": []}
except Exception as e:
# Log unexpected errors; don't let a bad search block the agent
logger.error(f"Search failed: {e}")
return {"results": []}
Without timeout handling, a slow or broken search API can hang the agent indefinitely. With retries and fallback-to-empty, the agent continues and the model can work with what it has (or decide it needs a different search).
Monitoring and token accounting
def run_research_agent(question: str) -> tuple[str, dict]:
"""Run agent and return answer plus execution metrics."""
context = [SYSTEM_PROMPT, question]
search_calls = 0
stats = {"model_calls": 0, "total_tokens": 0, "searches": []}
while search_calls < 3:
response = call_model(context)
stats["model_calls"] += 1
stats["total_tokens"] += response.usage.total_tokens
if response.is_final_answer:
return response.text, stats
query = response.tool_call.query
result = search_with_timeout(query)
stats["searches"].append({"query": query, "results_count": len(result['results'])})
context.append({"role": "assistant", "content": response.tool_call})
context.append({"role": "tool", "content": format_search_result(result)})
search_calls += 1
fallback_response = call_model(context + ["Answer with what you have; flag uncertainty."])
stats["model_calls"] += 1
stats["total_tokens"] += fallback_response.usage.total_tokens
return fallback_response.text, stats
Returning execution metrics (token count, number of searches, latency) lets you:
- Track cost — token count × model pricing.
- Identify slow runs — which questions trigger many searches?
- Debug failures — what was the state when the agent hit limits?
Edge case: Context explosion and the "golden path" problem
One subtle failure mode appears when the agent's reasoning becomes circular. For example:
# Problematic scenario:
# User: "What's the weather like in places I'm planning to visit?"
# Agent interprets "places I'm planning to visit" as ambiguous.
# Call 1: Search "user's planned destinations" → no results (it's not in public search)
# Call 2: Re-search "upcoming travel plans for user" → still no public data
# Call 3: Search "how to find user travel plans" → generic guides, not helpful
# → Forced fallback answer: "I couldn't find your travel plans..."
Edge case: When the agent's goal and available tools are mismatched, it burns through its call budget on dead-ends. To mitigate:
- Add a "skip this question" branch to the system prompt: "If after one search you don't find relevant information, stop and tell the user directly rather than trying multiple variations."
- Pre-filter questions at the input validation stage: "Do you have access to the user's travel plans in your search index?" If not, reject upfront.
- Version the system prompt and A/B test: Does version A ("try up to 3 times") or version B ("stop after 1 empty result") have better user satisfaction?
What this example intentionally leaves out
This is a minimal, single-agent, single-tool loop. It has no memory across separate sessions (Lesson 18: Memory Systems), no retrieval pipeline (Lesson 7: RAG), and no multi-agent delegation (Lesson 20: Multi-Agent Systems), because the task as scoped doesn't need them. Adding those components before a task demonstrably needs them only adds failure surface — a principle from the architecture-patterns lesson: match complexity to actual requirements.
When should you add more?
- Add memory if users ask follow-up questions that reference earlier conversation context.
- Add retrieval if the agent needs to search a private knowledge base (not public web search).
- Add multi-agent if one agent can't reason across multiple domains or needs parallel exploration.
Each addition comes with a cost: more state to manage, more places for errors, more testing. The single-tool loop is a good baseline. Expand from there only as needed.
Open-source frameworks to build on
You rarely need to write the orchestration loop by hand in production. These open-source projects handle the loop, tool-calling format, and (in some cases) multi-agent coordination for you:
| Project | What it's for |
|---|---|
| LangChain | General-purpose agent and LLM application framework; large ecosystem of tool integrations |
| LangGraph | Graph-based orchestration for stateful, multi-step or multi-agent workflows |
| LlamaIndex | Data ingestion, chunking, and retrieval -- strong fit for RAG-heavy agents |
| AutoGen | Multi-agent systems modeled as conversations between agents |
| CrewAI | Role-based multi-agent "crews" with built-in task delegation |
Common mistake
Reaching for a full multi-agent framework on day one, before building and testing a single-agent loop against your actual task. Starting with the minimal version -- like the example above -- makes it far easier to see exactly where a more structured pattern would genuinely help, instead of guessing at complexity you may never need.
Sources and license context
These references informed the lesson. ToolDix adds its own explanation, workflow, and practice rather than reproducing source material. Every link below leaves ToolDix and opens the publisher's own site in a new tab.
- Anthropic: Building Effective Agents (opens anthropic.com in a new tab)External · anthropic.com (Publisher terms apply)
- LangChain documentation: overview (opens docs.langchain.com in a new tab)External · docs.langchain.com (Publisher terms apply)
Keep going
Read these next on ToolDix.
Original lessons that build on what you just read.