Los frameworks como LangChain y pydantic-ai esconden el bucle de un agente para que no tengas que escribirlo. Eso está muy bien para producir rápido, pero hay un momento en el aprendizaje en que conviene levantar el capó y ver el motor. Porque cuando entiendes qué hacen esas librerías por dentro, dejas de tratarlas como magia y empiezas a depurarlas con criterio.
Así que en este caso vamos a construir un flujo de agente a mano, con el SDK oficial de Anthropic y nada más. Al terminar, el diagrama del ciclo del agente —decidir, ejecutar, observar, repetir— dejará de ser teoría.
Preparar el terreno
pip install anthropic
export ANTHROPIC_API_KEY="tu-clave-aqui"
El escenario
Haremos un pequeño asistente de soporte que responde: "¿Cuánto debo reembolsar al cliente del pedido 1024?" Para eso no basta con que el modelo hable: necesita datos que no tiene. Le daremos dos herramientas:
buscar_pedido(id)— devuelve el monto y el estado de un pedido.consultar_politica()— devuelve la regla de reembolsos vigente.
El agente tendrá que usar ambas, en orden, y combinar sus resultados para responder.
Paso 1: las herramientas (código normal de Python)
Una herramienta no tiene nada de mágico: es una función común. Aquí las simulamos con datos de ejemplo; en la vida real consultarían una base de datos o una API.
def buscar_pedido(id: int) -> dict:
pedidos = {
1024: {"monto": 50000, "estado": "entregado con daños"},
1025: {"monto": 30000, "estado": "en tránsito"},
}
return pedidos.get(id, {"error": "pedido no encontrado"})
def consultar_politica() -> str:
return "Los pedidos entregados con daños se reembolsan al 100%."
# Un despachador que conecta el nombre que pide el modelo con la función real.
def ejecutar(nombre: str, args: dict):
if nombre == "buscar_pedido":
return buscar_pedido(**args)
if nombre == "consultar_politica":
return consultar_politica()
return {"error": f"herramienta desconocida: {nombre}"}
Paso 2: describirle las herramientas al modelo
El modelo no ve tu código Python; ve una descripción de cada herramienta: su nombre, para qué sirve y qué argumentos recibe (con un esquema JSON). De la calidad de esas descripciones depende que el modelo las use bien.
herramientas = [
{
"name": "buscar_pedido",
"description": "Busca el monto y estado de un pedido por su ID.",
"input_schema": {
"type": "object",
"properties": {"id": {"type": "integer", "description": "ID del pedido"}},
"required": ["id"],
},
},
{
"name": "consultar_politica",
"description": "Devuelve la política de reembolsos vigente.",
"input_schema": {"type": "object", "properties": {}},
},
]
Paso 3: el bucle, que es el corazón del asunto
Aquí ocurre todo. El patrón es siempre el mismo: llamamos al modelo; si pide una herramienta (stop_reason == "tool_use"), la ejecutamos, le devolvemos el resultado y volvemos a llamarlo. Repetimos hasta que el modelo deje de pedir herramientas y entregue su respuesta final.
from anthropic import Anthropic
cliente = Anthropic() # lee ANTHROPIC_API_KEY del entorno
mensajes = [
{"role": "user", "content": "¿Cuánto debo reembolsar al cliente del pedido 1024?"}
]
while True:
respuesta = cliente.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
tools=herramientas,
messages=mensajes,
)
# Si el modelo NO pidió una herramienta, ya tiene su respuesta final: salimos.
if respuesta.stop_reason != "tool_use":
break
# Guardamos el turno del modelo (incluye qué herramientas pidió).
mensajes.append({"role": "assistant", "content": respuesta.content})
# Ejecutamos cada herramienta pedida y juntamos los resultados.
resultados = []
for bloque in respuesta.content:
if bloque.type == "tool_use":
salida = ejecutar(bloque.name, bloque.input)
resultados.append({
"type": "tool_result",
"tool_use_id": bloque.id, # ata el resultado a la petición
"content": str(salida),
})
# Devolvemos los resultados como un nuevo turno del usuario y repetimos.
mensajes.append({"role": "user", "content": resultados})
# Al salir del bucle, el último turno es la respuesta en lenguaje natural.
for bloque in respuesta.content:
if bloque.type == "text":
print(bloque.text)
Qué pasó por dentro
Si imprimieras cada vuelta, verías el flujo desplegarse solo:
- Vuelta 1 — el modelo no inventa el monto: pide
buscar_pedido(id=1024). Le devolvemos{"monto": 50000, "estado": "entregado con daños"}. - Vuelta 2 — con el estado en mano, pide
consultar_politica(). Le devolvemos la regla del 100%. - Vuelta 3 — ya tiene todo. Cruza los datos y responde: "Debes reembolsar $50.000: el pedido llegó con daños y la política cubre el 100%."
Nadie programó "primero busca el pedido, luego la política". El modelo decidió el orden, cuándo tenía suficiente información y cuándo parar. Eso es exactamente lo que lo hace un agente y no un simple script.
Un detalle que no es opcional: los límites
Este bucle, tal cual, corre hasta que el modelo diga basta. En producción siempre se le pone un tope de vueltas (por si entra en un ciclo) y, para acciones irreversibles —cobrar, borrar, enviar—, una aprobación humana antes de ejecutar. Un agente que puede actuar necesita frenos; dárselos es parte del trabajo, no un extra.
Para llevar
Un flujo de agentes es, sin adornos, un bucle: llamar al modelo, ejecutar la herramienta que pida, devolverle el resultado y repetir hasta que responda. Escribirlo a mano con el SDK de Anthropic —revisando
stop_reason == "tool_use"y encadenandotool_result— deja ver lo que LangChain y pydantic-ai automatizan. El modelo decide qué hacer; tu programa ejecuta y pone los límites. Con esta base, ningún framework de agentes volverá a parecerte una caja negra.
