Mastering AI Agent Engineering in 9 Days: Lesson 1 – The Core Agent Loop
This tutorial introduces the foundational Agent Loop that powers modern AI agents, explains why it is needed, breaks down its four core components, compares workflow‑based and agent‑based designs, and provides a minimal Python implementation with code, pitfalls, and a concrete RSS‑news use case.
Agent Loop Overview
Answering a question with a large language model (LLM) is a single input‑output pass, whereas an AI Agent must decompose a task, call tools, observe results, and iterate until the goal is reached. This iterative process is called the Agent Loop. The concept originates from the 2022 ReAct paper (Reasoning + Acting) and was popularized by AutoGPT, which showed that letting the model both reason and act yields far better performance than pure reasoning.
Problem addressed by the Agent Loop
A vanilla LLM is a self‑regressive language model that can only generate tokens it has seen during training; it cannot fetch external data, call APIs, manipulate files, or execute code. The Agent Loop bridges this gap by repeatedly invoking tools, allowing the model to reach beyond its training data.
Analogy: the LLM is the "brain" and the Agent Loop is the "hand" – the hand cannot move without the brain’s instructions, and the brain cannot act without the hand.
Example workflow for summarizing today’s Hacker News AI‑Agent articles:
Call the Hacker News API to get today’s hot items.
Filter for AI‑Agent‑related posts.
Fetch each article’s summary.
Prompt the LLM to generate a Chinese summary.
Output the final result.
Four core components of an Agent Loop
Message Container
The messages list holds the stateful conversation:
System Prompt : defines the agent’s role and rules.
User Message : the original task.
Assistant Messages : previous model replies.
Tool Messages : results returned by tools.
SYSTEM_PROMPT = """You are an AI Agent. When you need to complete a user task, you must call the terminate tool to return the result.
Do not answer the user directly; always use the terminate tool."""Tool Definition (Function Calling)
Before a tool can be called, the model must be given a structured description of its name, parameters, and purpose. This follows OpenAI’s Function Calling mechanism.
{
"type": "function",
"function": {
"name": "terminate",
"description": "End the Agent loop and return the final answer",
"parameters": {
"type": "object",
"properties": {
"final": {
"type": "string",
"description": "The final answer to return to the user"
}
},
"required": ["final"]
}
}
}Tool Executor
When the model requests a tool, the runtime must parse the tool name and arguments, execute the corresponding function, capture the result, and inject it back into the message container for the next reasoning step.
# Pseudo‑code
call = tool_calls[0]
name = call["function"]["name"] # "terminate"
args = json.loads(call["function"]["arguments"]) # {"final": "..."}
should_stop, output = execute_tool(name, args)
messages.append({
"role": "tool",
"tool_call_id": call["id"],
"content": output,
})Common pitfall: the model returns arguments as a JSON string; they must be parsed with json.loads(). If the JSON does not match the schema, the executor should raise an error and retry (e.g., using Pydantic for validation).
Termination Condition
The loop must stop, otherwise it can run forever. Typical conditions are:
Calling the terminate tool.
Reaching a maximum step count (e.g., max_steps=8).
The agent decides the task is complete.
Unexpected errors such as tool failures or token‑budget exhaustion.
for step in range(1, self.max_steps + 1):
# model call, tool execution, result injection …
if should_stop:
print(output)
return
raise RuntimeError(f"Agent exceeded max steps {self.max_steps} without terminating")Missing max_steps often leads to overnight runs that exhaust token quotas.
Workflow stream vs. Agent stream
Workflow stream
High determinism – the process is fully controllable.
Easy to debug and test.
Suited for tasks with clear, fixed steps.
Lacks flexibility for open‑ended tasks.
Branch explosion dramatically increases code complexity.
Agent stream
Great flexibility; can handle open‑domain problems.
No need to pre‑define every branch.
Ideal when the goal is known but the exact procedure is not.
Lower determinism; unexpected behaviours may appear.
Harder to debug because the model’s “thought process” is opaque.
Higher token consumption due to repeated calls.
In practice, mature projects often blend both: use a workflow for the main pipeline and agents for sub‑tasks that require flexibility.
Mini RSS‑News Agent implementation
Directory layout
exercise/01_mini_agent_loop/
├── env.py # environment variable loading
├── openai_compat.py # OpenAI‑compatible API wrapper
├── agent.py # core Agent Loop logic
├── tools.py # tool definitions and executor
└── main.py # entry pointtools.py
from typing import Any
def terminate_schema() -> dict[str, Any]:
"""Return the JSON schema for the terminate tool"""
return {
"type": "function",
"function": {
"name": "terminate",
"description": "End the Agent loop and return the final answer.",
"parameters": {
"type": "object",
"properties": {
"final": {
"type": "string",
"description": "The final answer to return to the user",
}
},
"required": ["final"],
},
}
}
def execute_tool(name: str, arguments: dict[str, Any]) -> tuple[bool, str]:
"""Execute a tool call. Returns (should_stop, output)"""
if name == "terminate":
final = str(arguments.get("final", "")).strip()
return True, final
raise RuntimeError(f"Unknown tool: {name}")agent.py
from dataclasses import dataclass
import json
@dataclass
class MiniManus:
max_steps: int = 8
def run(self, *, task: str) -> None:
# 1. Initialise message container
messages = [
{"role": "system", "content": self._system_prompt()},
{"role": "user", "content": task},
]
tools = [terminate_schema()]
# 2. Enter Agent Loop
for step in range(1, self.max_steps + 1):
# 3. Call the model (OpenAI‑compatible API)
resp = chat_completions(cfg=cfg, messages=messages, tools=tools)
# 4. Parse model response
msg = resp["choices"][0]["message"]
tool_calls = msg.get("tool_calls") or []
if tool_calls:
# 5. Execute the tool
call = tool_calls[0]
name = call["function"]["name"]
args = json.loads(call["function"]["arguments"])
should_stop, output = execute_tool(name, args)
# 6. Inject tool result back into messages
messages.append({
"role": "tool",
"tool_call_id": call["id"],
"content": output,
})
# 7. Check termination
if should_stop:
print(output)
return
# Fallback: exceeded max steps
raise RuntimeError(f"Agent exceeded max steps {self.max_steps} without terminating")
def _system_prompt(self) -> str:
return """You are an AI Agent. When you need to complete a user task, you must call the terminate tool to return the result.
Do not answer the user directly; always use the terminate tool."""Running the example:
$ uv run python 01_mini_agent_loop/main.py --task "Explain Agent Loop in three sentences"
Agent Loop is the AI agent's repeated "perceive‑think‑act" cycle.
The agent analyses the task, calls tools, observes results, and iterates until the goal is met.
This loop enables autonomous decomposition of complex tasks and dynamic strategy adjustment.Key pitfalls: missing max_steps leads to endless runs; tool arguments are returned as JSON strings and must be parsed; schema mismatches should raise errors and trigger retries.
Key takeaways
Agent Loop = repeated model inference + tool execution + state accumulation.
Four essential components: message container, tool schema, tool executor, termination condition.
Workflow streams provide determinism; Agent streams provide flexibility; most projects combine both.
The minimal implementation fits in a few dozen lines and forms the foundation for any AI‑Agent system.
Open‑source repository: https://github.com/HUANGLIWEN/mini-manus
AI Tech Publishing
In the fast-evolving AI era, we thoroughly explain stable technical foundations.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
