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.
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).
# 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.
# 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.
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.
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
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:
- 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.
- Write well-specified schemas for both (Section 2), including parameter types and clear descriptions.
- Implement the tool-use loop (Section 1) so the model can call either tool, see the result, and decide its next step.
- Add a hard step limit and log every tool call made during a run (Section 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.
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?
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?
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?
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.
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.