AI EngineeringZero to ProductionHome·About·Contact
Agents & Tools · Part 4.1 · Beginner

Agent & tool foundations

An agent is just a language model placed in a loop where it can call tools, read the results, and decide what to do next — instead of answering once and stopping. This beginner chapter builds that mental model from scratch: what actually separates an agent from a plain chat, what a tool is (a function the model can call, described by a JSON schema), and how the agent loop works step by step. You will hand-build a tiny working agent in plain Python — using a fake, deterministic model so it runs offline — then see exactly how the same shape maps onto a real Anthropic tool-use call. You will finish able to explain what an agent is and reason about when you actually need one.

⏱️ ~80 min🤖 Agents🎯 Beginner🐍 Runnable Python
🌱 What runs here, and what needs a keyThe agent loop, tool dispatch, and JSON-schema tools in this chapter are plain Python and run right here in your browser (no key, no install). We replace the LLM with a tiny fake, deterministic decider so the whole loop runs offline — a real model drops into exactly the same slot, and we say precisely where. Only the final real API example needs your own key; that code is complete and correct, but you run it in your own environment. Any numbers or transcripts shown are illustrative.

Learning objectives

  • Explain what an agent is — an LLM in a loop with tools — and how it differs from a one-shot chat.
  • Say why loops + tools are what give a model agency (the ability to act, not just answer).
  • Describe a tool as a function plus a JSON schema (name, description, parameters).
  • Trace the agent loop: propose tool call → execute → feed result back → repeat → final answer.
  • Implement tool dispatch — map a requested tool name + args to a Python function, and handle unknowns.
  • Recognise the shape of a real Anthropic tool-use call and decide when an agent is worth it.

1 · What an agent actually is

A plain chat call is one-shot: you send a prompt, the model writes text, and it stops. It cannot look anything up, run a calculation, or check the current state of the world — it can only produce words from what it already knows. An agent removes that ceiling by putting the model in a loop and giving it tools it can call. Now the model can say “I need to run the calculator” instead of guessing, you run it, and the model continues with the real result in hand.

One-shot chatAgent
Shapeprompt → answer → stopprompt → (think → call tool → read result)* → answer
Can act?No — only produces text.Yes — calls functions and observes their output.
Knows current facts?Only what it was trained on.Can fetch them via tools at run time.
Who runs the loop?Nobody — there is no loop.Your code. The model only proposes; your program executes and decides when to stop.
The one-sentence definitionAn agent = an LLM in a loop that can call tools and observe the results, running until it produces a final answer. Everything else in this track is making that loop safer, smarter, and more reliable.

The word to internalise is agency: the model gains the ability to take actions in the world (search, compute, call an API) and then react to what those actions return. Loops + tools are what turn a text generator into something that can get things done.

2 · Tools = functions the model can call

A tool is just a function in your program that you have advertised to the model. The model never runs the function itself — it can't. Instead it emits a structured request that says “call the tool named calculator with these arguments,” and your code runs the real function and hands back the result. To advertise a tool you describe it with a JSON schema: a name, a human-readable description (this is how the model decides when to use it), and the parameters it accepts.

Here are two real tool schemas as plain Python dicts — a calculator and a weather stub:

python · tool schemas (runnable — click ▶ Open in terminal)
tool_schemas.pyimport json

# A tool schema tells the model the tool's name, what it's for, and its parameters.
CALCULATOR = {
    'name': 'calculator',
    'description': 'Evaluate a basic arithmetic expression like "2 + 3 * 4".',
    'parameters': {
        'type': 'object',
        'properties': {
            'expression': {'type': 'string', 'description': 'The arithmetic to evaluate.'},
        },
        'required': ['expression'],
    },
}

GET_WEATHER = {
    'name': 'get_weather',
    'description': 'Get the current weather for a city.',
    'parameters': {
        'type': 'object',
        'properties': {
            'city': {'type': 'string', 'description': 'City name, e.g. "Paris".'},
            'unit': {'type': 'string', 'enum': ['c', 'f'], 'description': 'Temperature unit.'},
        },
        'required': ['city'],
    },
}

print(json.dumps(CALCULATOR, indent=2))
print('tools advertised:', [CALCULATOR['name'], GET_WEATHER['name']])
{
  "name": "calculator",
  "description": "Evaluate a basic arithmetic expression like \"2 + 3 * 4\".",
  "parameters": {
    "type": "object",
    "properties": {
      "expression": {
        "type": "string",
        "description": "The arithmetic to evaluate."
      }
    },
    "required": [
      "expression"
    ]
  }
}
tools advertised: ['calculator', 'get_weather']
The description is a promptThe model chooses a tool almost entirely from its description. A vague description (“does math”) leads to wrong or missed calls; a precise one (“evaluate a basic arithmetic expression like 2 + 3”) makes the model reach for it at the right moment. Treat tool descriptions as carefully as you treat prompts.

3 · The agent loop, from first principles

Question from user Model proposes next step Tool call? yes → execute Run tool feed result back Answer no → done

The loop is mechanical once you see it. The model looks at the conversation so far and either proposes a tool call or gives a final answer. If it proposed a tool call, your code runs the tool, appends the result to the conversation, and loops back to the model. When the model answers instead of calling a tool, you stop.

Here is a complete tiny agent. We replace the LLM with a fake, deterministic decider so it runs offline: on its first turn it decides to call the calculator; on its second turn — now that it can see the result — it writes the final answer. A real LLM replaces this fake decider; the loop around it is identical.

python · tiny agent loop
tiny_agent.py# ---- 1. the tool: a real Python function ----
def calculator(expression):
    # A safe tiny evaluator for + - * / on numbers (no names, no calls).
    return eval(expression, {'__builtins__': {}}, {})

TOOLS = {'calculator': calculator}

# ---- 2. a FAKE model. A real LLM drops into exactly this slot. ----
# It reads the conversation and returns either a tool call or a final answer.
def fake_model(messages):
    last = messages[-1]
    if last['role'] == 'user':
        # First look at the question -> decide to use the calculator.
        return {'type': 'tool_call', 'name': 'calculator',
                'args': {'expression': '2 + 3 * 4'}}
    # We just saw a tool result -> now answer for real.
    return {'type': 'final', 'text': f"The answer is {last['content']}."}

# ---- 3. the agent loop (this part never changes when you swap in a real model) ----
def run_agent(question, max_steps=5):
    messages = [{'role': 'user', 'content': question}]
    for step in range(max_steps):
        decision = fake_model(messages)
        if decision['type'] == 'final':
            return decision['text'], messages
        # The model proposed a tool call: run it and feed the result back.
        name, args = decision['name'], decision['args']
        result = TOOLS[name](**args)
        messages.append({'role': 'assistant', 'content': f'call {name}({args})'})
        messages.append({'role': 'tool', 'content': result})
    return 'stopped: too many steps', messages

answer, transcript = run_agent('what is 2 + 3 * 4?')
print('TRANSCRIPT:')
for m in transcript:
    print(f"  {m['role']:9} | {m['content']}")
print('\nFINAL:', answer)
TRANSCRIPT:
  user      | what is 2 + 3 * 4?
  assistant | call calculator({'expression': '2 + 3 * 4'})
  tool      | 14

FINAL: The answer is 14.

That is a real agent. The model didn't compute 14 from memory — it called a tool, your code ran it, and the model finished using the true result. Swap fake_model for a hosted LLM and the loop code stays exactly as written.

Always bound the loopNotice max_steps. A real model can get stuck calling tools forever (or ping-ponging between two). Never write an unbounded agent loop — always cap the number of steps so a confused model fails loudly instead of running (and billing) without end.

4 · Tool dispatch — matching a request to a function

In the loop above, TOOLS[name](**args) is doing dispatch: taking the tool name and arguments the model asked for and routing them to the actual Python function. A registry (a dict from name → function) keeps this clean and makes adding a tool a one-line change. The one case beginners forget: the model can ask for a tool that doesn't exist (a typo, or a tool you removed). Handle it — don't crash.

python · dispatch
dispatch.pydef calculator(expression):
    return eval(expression, {'__builtins__': {}}, {})

def get_weather(city, unit='c'):
    # A stub — a real version would call a weather API here.
    return f'18 degrees {unit} and clear in {city}'

TOOLS = {'calculator': calculator, 'get_weather': get_weather}

def dispatch(name, args):
    fn = TOOLS.get(name)
    if fn is None:
        # Return an error string instead of raising — the model can read it and recover.
        return f"error: no tool named '{name}'. available: {sorted(TOOLS)}"
    try:
        return fn(**args)
    except Exception as e:
        return f'error running {name}: {e}'

print(dispatch('calculator', {'expression': '10 / 4'}))
print(dispatch('get_weather', {'city': 'Paris', 'unit': 'f'}))
print(dispatch('teleport', {'to': 'Mars'}))          # unknown tool
print(dispatch('calculator', {'expression': '1/0'}))  # tool raises
2.5
18 degrees f and clear in Paris
error: no tool named 'teleport'. available: ['calculator', 'get_weather']
error running calculator: division by zero

Two habits to keep from the start: look tools up in a registry (never a long if/elif chain), and turn failures into result strings the model can read rather than exceptions that kill the loop. Feeding “no tool named 'teleport'” back to the model lets it correct itself on the next turn — which is exactly the kind of self-recovery that makes agents useful.

5 · The real version — an Anthropic tool-use call

Everything above ran with a fake decider so you could see the whole loop. A real agent swaps fake_model for a hosted LLM. The shape is the same as our tiny loop — you send the tools and the conversation, the model replies with either text or a tool-call request, you run the tool, and you send the result back. Here is that same loop against the real Anthropic API. It is complete and correct, but it needs your API key to run, so we don't execute it here:

python · real tool use (needs YOUR key — not run here)
real_tool_use.py# The real agent loop. Complete and correct; needs YOUR API key to actually run.
# pip install anthropic ; export ANTHROPIC_API_KEY=sk-...
from anthropic import Anthropic          # runs in your own environment, not the sandbox

client = Anthropic()

def calculator(expression):
    return eval(expression, {'__builtins__': {}}, {})
TOOLS = {'calculator': calculator}

# Same tool schema as section 2, in the shape the API expects (input_schema).
tools = [{
    'name': 'calculator',
    'description': 'Evaluate a basic arithmetic expression like "2 + 3 * 4".',
    'input_schema': {
        'type': 'object',
        'properties': {'expression': {'type': 'string'}},
        'required': ['expression'],
    },
}]

messages = [{'role': 'user', 'content': 'what is 2 + 3 * 4?'}]
while True:
    resp = client.messages.create(
        model='claude-opus-4-8', max_tokens=1024, tools=tools, messages=messages,
    )
    if resp.stop_reason != 'tool_use':
        # The model gave a final answer -> print the text and stop the loop.
        print(next(b.text for b in resp.content if b.type == 'text'))
        break
    # The model asked for a tool. Run each requested tool and feed results back.
    messages.append({'role': 'assistant', 'content': resp.content})
    results = []
    for block in resp.content:
        if block.type == 'tool_use':                 # block.name / .input / .id
            output = TOOLS[block.name](**block.input)
            results.append({'type': 'tool_result', 'tool_use_id': block.id,
                            'content': str(output)})
    messages.append({'role': 'user', 'content': results})
Needs your own key and installThis calls a hosted model, so it needs pip install anthropic and your ANTHROPIC_API_KEY in the environment — it can't run in the browser sandbox. Notice the shape is our tiny loop with real names: the model stops with stop_reason == 'tool_use' and emits tool_use blocks (each with a .name, .input, and .id); you run the tool and reply with a tool_result carrying the matching tool_use_id. The concepts already ran above with the fake model — only the decider changed.

6 · When you need an agent — and when you don't

Agents are powerful, but the loop adds real cost: more model calls (higher latency and bill) and more ways to fail (a bad tool call, an infinite loop, a wrong result fed back). A plain prompt is one call with a predictable cost. Reach for an agent only when the task genuinely needs it:

Use a plain prompt when…Use an agent when…
The answer is already in the model's knowledge or in the prompt you send.The task needs live data or actions the model can't do itself (search, compute, call an API).
It's one step — summarise, classify, rewrite, extract.It takes multiple steps whose order depends on intermediate results.
You want low latency and a predictable, single cost.The value of getting it done autonomously justifies extra latency, cost, and failure surface.
You can fully specify the output up front.The path can't be fully planned in advance — the model must react as it goes.
The practical ruleStart with the simplest thing that works. If a single well-written prompt answers the question, use that. Add a loop and tools only when the task needs to act or take several dependent steps. An agent is a tool for a specific job, not a default.
✓ Knowledge check

What are the two ingredients that turn a plain LLM into an agent, and what does each add?

Show answer
A loop and tools. Tools let the model act (call functions that search, compute, or hit an API); the loop lets it react — read each tool's result and decide the next step — repeating until it produces a final answer. One-shot chat has neither.
✓ Knowledge check

The model requests a tool named lookup_order, but your registry has no such tool. What should your dispatch code do, and why not just crash?

Show answer
Return an error string (e.g. “no tool named 'lookup_order'”) as the tool result instead of raising. Feeding the error back lets the model read it and correct itself on the next turn; crashing ends the loop and throws away that chance to recover.

🪜 Practice — from a working toy to real intuition beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · Add a second toolBeginner

In tiny_agent.py, register a second tool greet(name) that returns f'Hello, {name}!', and add it to TOOLS. You don't need the model to call it yet — just confirm the registry holds both.

Show solution Define def greet(name): return f'Hello, {name}!' and set TOOLS = {'calculator': calculator, 'greet': greet}. Print sorted(TOOLS) to see both names. Dispatch is unchanged — the registry does the routing.
Exercise 2 · Make the fake model chooseIntermediate

Change fake_model so that if the question contains the word “weather” it proposes get_weather, otherwise it proposes calculator. Add a get_weather stub and run both kinds of question.

Show solution Branch inside the if last['role'] == 'user' block: if 'weather' in last['content'] return a get_weather tool call, else the calculator one. The loop and dispatch don't change — only the decider's choice does.
Exercise 3 · Log every stepIntermediate

Print each loop iteration: the step number, whether the model proposed a tool or a final answer, and (for tool calls) the tool name and args. This is the beginning of observability.

Show solution Inside run_agent's for step in range(...) loop, print step and decision['type']; for a tool call also print decision['name'] and decision['args']. Seeing the trace is how you debug agents.
Exercise 4 · Handle a two-tool taskAdvanced

Extend the fake model so a question like “weather in Paris, then double the temperature” calls get_weather first, then calculator on the result, then answers. You will need the fake model to look at how many tool results are already in the transcript.

Show solution Count tool-role messages in messages: 0 → call weather; 1 → call calculator on the returned number; ≥2 → final answer. This is a hand-wired version of what a real model does on its own — multi-step, each step depending on the last.
Exercise 5 · Guard against a runaway loopExpert

Make the fake model buggy on purpose — always return a tool call, never a final answer — and confirm your max_steps cap stops it cleanly. Then explain why this matters more with a real model.

Show solution With fake_model always returning a tool_call, run_agent hits the max_steps cap and returns “stopped: too many steps”. A real model can loop for real reasons (confusion, a tool that always errors) — the cap converts an infinite, billable loop into a clean, visible failure.
Exercise 6 · Validate arguments before dispatchProfessional

Before calling a tool, check that the model's args match the tool's schema (required keys present, no unexpected keys). Reject bad calls with an error result instead of running the function. Explain why validating at the boundary matters.

Show solution Read the tool's parameters schema: verify every name in required is present in args and (optionally) that no extra keys appear; if not, return an error string as the tool result. Validating at the boundary means a malformed model request fails safely and legibly instead of raising deep inside your function — the same discipline real tool frameworks enforce with strict schemas.

Context: A teammate has only ever used one-shot LLM prompts and asks you: “When should I actually build an agent instead of just writing a better prompt?”

Your task: Write a short note (5–8 sentences) that answers them using the mental model from this chapter.

Requirements:

  • Define an agent in one line — an LLM in a loop with tools — and contrast it with a one-shot prompt.
  • Name the concrete costs an agent adds (more model calls → latency and bill; more failure modes).
  • Give the decision rule: reach for an agent only when the task needs to act (tools) or take multiple dependent steps; otherwise a plain prompt wins.
  • Mention one safety habit from this chapter (bound the loop with a step cap, or turn tool failures into readable error results).

💡 Hint: You don't need code — this is about communicating the mental model. The next chapter (4.2) covers agent patterns — ReAct, plan-and-execute, reflection — that make the loop smarter.

✓ Checkpoint — you can move on when you can…

  • An agent is an LLM in a loop that can call tools and observe results, running until it gives a final answer — unlike a one-shot chat.
  • A tool is a function you advertise to the model with a JSON schema (name, description, parameters); the model requests it, your code runs it.
  • The agent loop is: model proposes a tool call → you execute → feed the result back → repeat → final answer. You own the loop; the model only proposes.
  • Dispatch routes a requested tool name + args to a real function via a registry, and unknown tools and failures return error strings the model can recover from — not crashes.
  • A real Anthropic tool-use call has the same shape: stop_reason == 'tool_use' + tool_use blocks in, tool_result blocks back — only the decider changed.
  • Use an agent only when the task needs to act or take multiple dependent steps; otherwise a plain prompt is cheaper and simpler.