Theory
The best way to understand agents is to build one from scratch. Julien Chaumond from Hugging Face did exactly that and distilled it into a key insight:
Once you have an MCP client, an agent is literally just a while loop on top of it.
Strip away the frameworks and you're left with three pieces:
- An LLM inference client: something that can send a list of messages and receive a response
- A set of tools: functions with a name, description, and JSON schema for their parameters
- A while loop: the agentic loop that keeps calling the LLM, executing tool calls, and feeding results back until the task is done
The anatomy of a tool
A tool is just a function described in a way the LLM can understand:
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current temperature for a given location.",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City and country e.g. Bogotá, Colombia"
}
}
}
}
}
You pass a list of these to the LLM alongside your messages. The LLM decides when to call one and with what arguments. You execute it, capture the result, append it to the message history as a tool role message, and loop.
The while loop
The core of any agent is this pattern:
while true:
response = llm.chat(messages, tools=available_tools)
if response has no tool calls:
break // task is done or the agent is stuck
for each tool_call in response:
result = execute(tool_call)
messages.append(tool_result(result))
That's it. Everything else (memory management, MCP integration, multi-agent coordination) is built on top of this loop.
Why this matters for engineers
Knowing the raw loop helps you:
- Debug agent failures: when an agent loops forever or stops too early, you can trace exactly which message caused it
- Evaluate frameworks: any framework (LangChain, CrewAI, BMAD, etc.) is an abstraction over this loop: you can ask what it adds and whether the complexity is worth it
- Understand tool design: because the LLM picks tools based on their description, a well-named, well-described tool will be called correctly; a vague one won't
- Reason about cost and latency: every iteration of the loop is an LLM call; knowing this helps you design agents that exit cleanly rather than running indefinitely
Practice
Run this in a repository you already know, not a toy project. The point is to feel where the practice helps and where it gets in the way on code that has history.
Reimplement the agentic loop from scratch in your language or framework of choice. You don't need MCP support. The goal is to feel how the loop works, not to build a production tool.
What to build:
- Define 2–3 simple tools (e.g.,
get_current_time,add_numbers,reverse_string). Each tool is just a real function plus a JSON schema description. - Write a function that calls an LLM (OpenAI-compatible API, Anthropic, Ollama, etc.) with a list of messages and a list of tool schemas.
- Parse the response: if it contains tool calls, execute them and append the results to the message list. If it contains a plain text response, print it and stop.
- Wrap steps 2–3 in a while loop that runs until the LLM stops calling tools.
- Test it with a prompt that requires using at least one of your tools (e.g., "What is 17 + 38?").
What to observe:
- How many loop iterations did it take?
- What did the raw messages array look like at each step?
- What happened when you gave it a goal that needed no tools?
- What happened when you gave it a goal it couldn't achieve with your tools?
This exercise is deliberately low-level. The point is not to ship something; it's to internalize the loop so you can reason about any framework built on top of it.