Week 23: Agents, Tool Use & Orchestration

Every prior week produced text. This week produces action: a model that decides to call a function, reads the result, and decides what to do next — the mechanism behind everything marketed as an "AI agent." It's built entirely from Week 21's structured outputs, looped, with real guardrails against the ways a loop like that can go wrong.

Phase 7 of 8 Week 23 of 26 ~4 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Implement a full tool-calling loop from scratch
  • Design tool schemas a model can use reliably, and explain what MCP standardizes
  • Guard an agent loop against infinite loops and unintended tool misuse

1. Function Calling & Tool-Use Loops

Function calling extends Week 21's structured outputs: instead of the model returning arbitrary JSON, you describe real functions it can invoke, and the model returns a structured request to call one — which your code actually executes, feeding the result back in.

tool_loop.py
def get_weather(city: str) -> str:
    return f"72F and sunny in {city}"   # a real function your code can call

tools = [{
    "name": "get_weather",
    "description": "Get the current weather for a city",
    "parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]},
}]

messages = [{"role": "user", "content": "What's the weather in Austin?"}]

while True:
    response = call_llm(messages, tools=tools)

    if response.tool_call is None:
        break   # model produced a final answer -- no more tools needed

    result = get_weather(**response.tool_call.arguments)   # actually execute the requested tool
    messages.append({"role": "assistant", "content": None, "tool_call": response.tool_call})
    messages.append({"role": "tool", "content": result})   # feed the result back in

This loop — call the model, check if it wants a tool, execute the tool, feed the result back, repeat — is the entire mechanical core of an "agent." Everything else in this week (schemas, memory, multi-agent orchestration) is refinement and safety around this one loop, not a fundamentally different mechanism.

2. Designing Tool Schemas a Model Can Use Reliably

A tool's schema is effectively documentation the model reads to decide when and how to call it — vague or ambiguous descriptions produce unreliable tool use in exactly the same way a vague prompt produces unreliable text (Week 21).

a well-specified vs. poorly-specified schema
# Poorly specified -- ambiguous about units, format, and edge cases
{"name": "get_temp", "description": "gets temp", "parameters": {"loc": {"type": "string"}}}

# Well specified -- explicit about format, units, and constraints
{
    "name": "get_current_temperature",
    "description": "Returns the current temperature for a named city. Only supports major world cities.",
    "parameters": {
        "type": "object",
        "properties": {
            "city": {"type": "string", "description": "Full city name, e.g. 'Austin' or 'Tokyo'"},
            "unit": {"type": "string", "enum": ["celsius", "fahrenheit"], "description": "Temperature unit to return"},
        },
        "required": ["city", "unit"],
    },
}

Precise names, descriptions, and constrained parameter types (an enum for a fixed set of options, rather than free-text) meaningfully reduce how often a model misuses a tool, passes malformed arguments, or calls the wrong tool for the job — the same discipline as Week 21's structured-output schemas, applied to actions rather than just data shape.

3. The Model Context Protocol (MCP)

Before MCP, every application wiring an LLM to external tools (a database, a filesystem, a web API) had to write custom integration code for each combination of model and tool — a combinatorial mess. The Model Context Protocol standardizes how a tool (or data source) exposes itself to any compatible model or agent framework, the same way a USB standard lets any compliant device plug into any compliant port.

the practical shape of an MCP server
# An MCP server exposes:
# - Tools: functions the model can call (like Section 1's get_weather)
# - Resources: data the model can read (files, database rows, API responses)
# - Prompts: reusable prompt templates the server provides

# A model/agent framework that speaks MCP can connect to ANY MCP server
# without custom integration code written for that specific server

The practical benefit for you: a tool built once as an MCP server can be reused across different agent frameworks and models, rather than re-implemented per integration — directly reducing the custom "glue code" that used to dominate agent development.

4. Multi-Agent Systems & Agent Memory

A single agent handling every part of a complex task can become unwieldy — too many tools, too long a system prompt, too much for one context window to track. A multi-agent system splits responsibilities across specialized agents (a "researcher" agent, a "writer" agent, a "reviewer" agent) coordinated by an orchestrator, each with a narrower, more focused job.

Agent memory is how an agent retains information across steps or across separate conversations — ranging from simply keeping the full message history (Section 1's messages list) to a more deliberate design using Week 22's RAG: store important facts as embeddings, and retrieve only what's relevant to the current step rather than replaying an ever-growing, context-window-busting transcript.

Multi-agent isn't automatically better

Splitting a task across agents adds coordination overhead and more places for something to go wrong (Section 5). It's worth it when a single agent's context or tool set genuinely becomes unmanageable — not as a default architecture for every task.

5. Failure Modes: Infinite Loops, Tool Misuse & Guarding Against Them

An agent loop (Section 1) has no inherent stopping guarantee — a model can call the same tool repeatedly without making progress, or call a tool with the wrong arguments in a way that keeps failing and retrying. Production agent code needs explicit guards the naive loop doesn't have.

guarded_tool_loop.py
MAX_STEPS = 10
step = 0

while step < MAX_STEPS:                     # hard cap -- never loop forever
    response = call_llm(messages, tools=tools)
    if response.tool_call is None:
        break

    if response.tool_call.name == "delete_all_records":
        confirm = ask_human_for_confirmation(response.tool_call)   # gate consequential actions
        if not confirm:
            break

    result = execute_tool(response.tool_call)
    messages.append(...)
    step += 1
else:
    log_warning("Agent hit max steps without a final answer")

Three guards worth having by default: a hard step limit (preventing runaway loops and runaway cost), human confirmation before any consequential or irreversible action (directly connected to Week 21's prompt-injection defenses — an injected instruction can't cause real damage if a human must approve it first), and logging every tool call for later review, since debugging an agent that misbehaved is far easier with a full record of what it actually did.

6. Hands-on Exercise

Hands-on

Build a guarded, tool-using agent that completes a multi-step task

Build an agent that can look up information and perform a simple calculation using two real tools.

Requirements:

  1. Implement two real tools: e.g. one that looks up a value (a mock "database," a small dictionary, or a real API), and one that performs arithmetic on the result.
  2. Write well-specified schemas for both (Section 2), including parameter types and clear descriptions.
  3. Implement the tool-use loop (Section 1) so the model can call either tool, see the result, and decide its next step.
  4. Add a hard step limit and log every tool call made during a run (Section 5).
  5. Run a task that genuinely requires both tools in sequence (e.g. "look up X, then compute Y using it"), and confirm the agent completes it correctly within your step limit.
Hint

Deliberately test what happens when you set the step limit very low (e.g. 2, when the task needs 3+ steps) — confirming your agent stops gracefully and logs a warning rather than crashing is exactly the kind of guard-rail behavior worth verifying, not just implementing.

7. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

What is the core mechanical loop that makes an "AI agent" different from a plain chat model?

An agent runs a loop: call the model, check whether it requested a tool call, actually execute that tool, feed the result back into the conversation, and repeat until the model produces a final answer instead of another tool call. A plain chat model just produces text in a single pass with no ability to take real actions in between.

Q2

Why does a vague tool description lead to unreliable tool use, the same way a vague prompt leads to unreliable text output?

The model only knows what a tool does and how to call it from its schema's name, description and parameter definitions — there's no other source of truth it can consult. An ambiguous schema (unclear units, vague parameter meaning, no constraints on valid values) gives the model insufficient information to decide correctly when and how to call it, just as an ambiguous prompt gives it insufficient information to produce the intended text.

Q3

What practical problem does the Model Context Protocol solve?

Without a standard, every combination of model/agent framework and external tool needs custom integration code. MCP standardizes how tools, resources, and prompts are exposed to any compatible client, so a tool built once as an MCP server can be reused across different agent frameworks without rewriting integration code for each one.

Q4

Name two concrete guardrails a production agent loop should have that a naive implementation lacks.

Any two of: a hard step limit to prevent infinite or runaway loops, requiring human confirmation before consequential/irreversible tool calls, and logging every tool call for later debugging and review. A naive loop has no built-in stopping guarantee and no protection against a model repeatedly misusing a tool.