I have been designing systems for a while where Artificial Intelligence goes from being a mere stochastic parrot to something that actually does things. The technical difference between sending a request to an API and building an agent is not trivial.
Bringing it down to earth: The decision loop
It is popularly said that an AI agent "reasons and makes autonomous decisions". If we bring this down to the reality of engineering, an agent is nothing more than a design pattern. Specifically, a while loop.
A traditional script executes steps sequentially: reads from a database, transforms, writes. An agent evaluates the current state, decides which tool to use (if any is needed), executes the tool, observes the result, and evaluates again. It stays trapped in this reasoning cycle (ReAct: Reason + Act) until it determines it has the final answer or until it hits an iteration limit. There is no magic, there is a deterministic state machine governing non-deterministic calls.
Memory: The context problem
I've seen many programmers clog up an LLM's context window right out of the gate. They cram the entire conversation history, all the logs, the entire database schema into it, and then act surprised when the model hallucinates or the API charges them a fortune.
An agent in production needs short-term and long-term memory. - Short term: The context of the current task. What we are trying to solve right now. - Long term: Knowledge stored in vector databases or other persistent systems.
The real technique here is windowing or progressive summarization. You don't pass the agent the last 50 messages. You pass it the last 3, and a compressed summary of the previous 47.
Practical Example: A minimal agent with controlled retention
Let's see a Python skeleton of how to structure an agent using a graph approach (something similar to what LangGraph does, which has saved my life in more than one heavy deployment).
import operator
from typing import TypedDict, Annotated, Sequence
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
# Definimos el estado global del agente.
# Annotated con operator.add indica que los mensajes se concatenan, no se sobrescriben.
class AgentState(TypedDict):
messages: Annotated[Sequence[BaseMessage], operator.add]
summary: str
def agent_node(state: AgentState):
"""
El nodo principal de decisión.
Aquí llamaríamos al LLM pasándole el resumen y los mensajes recientes.
"""
# Filtramos para quedarnos solo con los últimos 3 mensajes
recent_messages = state["messages"][-3:]
context = f"Resumen previo: {state.get('summary', 'Sin contexto previo.')}"
# Simulación de la respuesta del modelo
response = AIMessage(content=f"Evaluando con contexto: {context}. Acción decidida.")
return {"messages": [response]}
def memory_manager_node(state: AgentState):
"""
Este nodo se encarga de evitar que el contexto explote.
Si hay más de 5 mensajes, comprime los antiguos en un resumen.
"""
messages = state["messages"]
if len(messages) > 5:
# Aquí llamaríamos a un modelo ligero para resumir.
new_summary = "El usuario pidió una tarea y el agente empezó a investigar."
# Nos quedamos con el resumen y purgamos los mensajes viejos
# Devolviendo una estructura que nuestra máquina de estado entienda como un "reinicio" de la lista
return {"summary": new_summary, "messages": messages[-2:]}
return state
# El flujo real (simplificado) sería:
# 1. agent_node()
# 2. ejecuta_herramienta() si aplica
# 3. memory_manager_node() para mantener limpio el contexto
# 4. Repetir hasta terminar
Structuring state explicitly is what saves you from the agent getting lost in its own noise. Don't rely on the library to manage the context under the hood. Do it yourself. That is how you keep control when the system starts to scale.