Skip to main content
AI Agent Tutorial: Concepts to Architecture

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.

Intermediate16 minBy ToolDix Editorial

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

AI Agent Tutorial practice loop
1

Frame

Name the outcome and constraints.

2

Build

Try one bounded workflow.

3

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.

ToolDix original diagram
This lesson's example agent, at a glance
Goal
Answer the user's question accurately.
Tool
One search tool, clearly scoped.
Loop
Call model, check done, else search, append, repeat.
Guardrail
Hard stop at 3 search calls.
Fallback
Answer with noted uncertainty if still stuck.

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 context list 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:

  1. 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").
  2. Search executes and returns: "requests 2.32.0, released 2024-01-01. Used for HTTP requests in Python..."
  3. Call 2: The model reads the search result. The version number is clear, so it outputs is_final_answer: true with the answer: "The latest version is 2.32.0 (as of my search)."
  4. 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:

  1. Call 1: The model decides this requires explanation of distinct algorithms. It searches: "LRU cache algorithm explanation".
  2. Result: Medium-length explanation of Least Recently Used caching.
  3. 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".
  4. Result: A comparison table is found.
  5. 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".
  6. Result: Use-case guide is returned, but the model is still slightly uncertain about one detail.
  7. 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."
  8. 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."
  9. 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:

  1. 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."
  2. 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.
  3. 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:

ProjectWhat it's for
LangChainGeneral-purpose agent and LLM application framework; large ecosystem of tool integrations
LangGraphGraph-based orchestration for stateful, multi-step or multi-agent workflows
LlamaIndexData ingestion, chunking, and retrieval -- strong fit for RAG-heavy agents
AutoGenMulti-agent systems modeled as conversations between agents
CrewAIRole-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.

Keep going

Read these next on ToolDix.

Original lessons that build on what you just read.