Skip to content

Step 4 · The loop — an agent

What this step adds: the loop. After this file runs, you have built an agent — the rest of the course is about making it trustworthy.

The agent loop: call the model; if it requests a tool, run the tool, append the result to the conversation, and call the model again. Repeat until the model answers in text instead of asking for a tool.

Why a loop and not a fixed sequence? Because you can’t know in advance how many rounds it needs. Maybe one lookup answers the question; maybe the first result raises a second question. The number of iterations is the model’s decision — which is exactly what a while loop is for.

Create step4.py:

import ollama
def get_transaction(txn_id):
records = {"T-1002": {"amount": 4200, "country": "Nigeria", "card_present": False}}
return records[txn_id]
schema = {
"type": "function",
"function": {
"name": "get_transaction",
"description": "Look up a transaction's amount, country and card_present flag by its id.",
"parameters": {
"type": "object",
"properties": {"txn_id": {"type": "string"}},
"required": ["txn_id"],
},
},
}
messages = [{"role": "user", "content": "Is transaction T-1002 suspicious? Explain why."}]
while True:
response = ollama.chat(model="qwen2.5:3b", messages=messages, tools=[schema])
messages.append(response.message)
if response.message.tool_calls:
for call in response.message.tool_calls:
result = get_transaction(call.function.arguments["txn_id"])
print("RAN TOOL, got:", result)
messages.append({"role": "tool", "content": str(result)})
else:
print()
print("MODEL:", response.message.content)
break

Three lines carry the whole design:

  • messages.append(response.message) — the model’s own request goes into the history. The model is stateless; this list is its only memory. Skip this line and the tool result arrives as an answer to a question nobody asked.
  • messages.append({"role": "tool", ...}) — the result goes back in with the role tool, so the model can tell data it requested from things the user said.
  • break — the exit. No tool calls means the model has what it needs and has answered.
Terminal window
uv run step4.py

Expect:

RAN TOOL, got: {'amount': 4200, 'country': 'Nigeria', 'card_present': False}
MODEL: Transaction T-1002 appears suspicious: it is a large amount ($4,200),
made card-not-present, from Nigeria...

Wording varies run to run. What doesn’t vary: the facts in the answer came from your function. In step 1 the model invented them; now it looked them up. Compare the two runs — that difference is the entire value of tools.

Ask the loop a question that doesn’t need the tool and the model answers immediately — one pass, no tool call, straight to break. Ask something needing data and it fetches first. The model is directing its own process: deciding whether to act, acting, reading the result, deciding it’s done. Your code supplies the mechanism; the model supplies the control flow.

That’s the definition worth keeping: an agent is a loop in which the branch is chosen by a model rather than by your code.

Everything this agent concludes is prose. “Appears suspicious” — according to whom, by what rule? If the model phrases it differently tomorrow, has the decision changed? You can’t build a process on paragraphs. Step 5 separates what the model is good at from what should never be left to it.

Step 5 — judgment vs code →