Usas ChatGPT todos los días. Pegas texto, recibes texto, te sientes productivo. Pero en cada conferencia tech de 2026 repiten una palabra como disco rayado: agents. Tu PM dice "necesitamos un agente". Tu CTO dice "los agentes son el futuro". LinkedIn está inundado de posts filosóficos sobre agentes. Y tú ahí sentado pensando: "Ni siquiera sé qué significa eso".

Acá va la diferencia. Un chatbot espera a que escribas y te responde — como mandarle mensaje a un amigo inteligente. Un agente de IA es otra cosa. Un agente tiene un objetivo, elige sus propias herramientas, maneja errores y sigue trabajando hasta terminar el trabajo o decidir que no se puede. Nadie le agarra la mano. Es la diferencia entre preguntarle algo a alguien y contratar a alguien para que haga el trabajo.

Esta noche, cierras esa brecha. Vas a construir un agente real — en Python, desde cero, sin frameworks — que busca en la web, analiza información, toma decisiones y guarda un reporte de investigación en disco. Para cuando te vayas a dormir, vas a entender el patrón exacto que impulsa Claude Code, Codex, Devin y todos los demás productos de agentes que cobran $200/mes. El patrón en sí ocupa unas 30 líneas.

Qué vamos a construir

Un Research Agent que:

  1. Recibe un tema de tu parte
  2. Busca información relevante en la web
  3. Lee y analiza lo que encuentra
  4. Escribe un resumen estructurado de la investigación
  5. Guarda el resultado en un archivo

Esto no es un demo de juguete. Es la misma arquitectura que usan los agentes en producción — uso de herramientas, loops de razonamiento, salida estructurada. La única diferencia entre esto y un "agente de producción" es el manejo de errores y la escala.

Paso 1: Configurar el proyecto (10 minutos)

mkdir research-agent && cd research-agent
python3 -m venv venv
source venv/bin/activate

pip install anthropic httpx

Dos dependencias:

  • anthropic — el SDK de Python (kit de desarrollo de software — una biblioteca pre-construida para hablar con la API de Claude)
  • httpx — para hacer peticiones web desde Python

También necesitas una API key de Anthropic — básicamente una contraseña que permite que tu código hable con Claude. Consigue una en console.anthropic.com. Las cuentas nuevas reciben $5 en créditos gratis, suficiente para correr este agente cientos de veces.

export ANTHROPIC_API_KEY=sk-ant-...
touch agent.py

Paso 2: Definir las herramientas (15 minutos)

Un agente sin herramientas es solo un chatbot. Las herramientas son funciones que le permiten al agente interactuar con el mundo real — buscar en la web, leer archivos, llamar APIs (la forma en que los programas se gritan entre sí a través de internet — piensa en mensajes de texto entre máquinas).

Le vamos a dar a nuestro agente dos herramientas:

# agent.py

import anthropic
import httpx
import json
import os
from datetime import datetime

client = anthropic.Anthropic()
MODEL = "claude-haiku-4.5"

# Definiciones de herramientas — le dicen a Claude qué está disponible
tools = [
    {
        "name": "web_search",
        "description": "Search the web for information on a topic. Returns results with titles, URLs, and snippets.",
        "input_schema": {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "The search query"
                }
            },
            "required": ["query"]
        }
    },
    {
        "name": "save_file",
        "description": "Save text content to a file on disk.",
        "input_schema": {
            "type": "object",
            "properties": {
                "filename": {
                    "type": "string",
                    "description": "Name of the file to save"
                },
                "content": {
                    "type": "string",
                    "description": "Content to write to the file"
                }
            },
            "required": ["filename", "content"]
        }
    }
]

Estas definiciones funcionan como el menú de un restaurante. Claude lee las descripciones y decide cuándo usar cada herramienta — tú no programas el orden. La parte de input_schema usa JSON Schema — un formato estándar para describir cómo se ven los datos, para que Claude sepa exactamente qué parámetros espera cada herramienta. Sí, describes el formato de tus datos usando otro formato de datos. Bienvenido a la programación.

Paso 3: Implementar las herramientas (15 minutos)

Las definiciones le dicen a Claude qué existe. Ahora escribimos el código que realmente se ejecuta cuando Claude llama una herramienta. Aquí es donde la teoría se encuentra con la práctica — o, más exactamente, donde tus hermosas abstracciones se encuentran con la fea realidad de parsear HTML como si fuera 2003:

def execute_tool(tool_name: str, tool_input: dict) -> str:
    """Execute a tool and return the result as a string."""
    if tool_name == "web_search":
        return do_web_search(tool_input["query"])
    elif tool_name == "save_file":
        return do_save_file(tool_input["filename"], tool_input["content"])
    else:
        return f"Error: Unknown tool '{tool_name}'"


def do_web_search(query: str) -> str:
    """Search using DuckDuckGo's HTML endpoint. No API key needed."""
    try:
        response = httpx.get(
            "https://html.duckduckgo.com/html/",
            params={"q": query},
            headers={"User-Agent": "ResearchAgent/1.0"},
            timeout=10.0,
        )
        response.raise_for_status()

        text = response.text
        results = []
        parts = text.split('class="result__snippet"')
        for part in parts[1:6]:  # Grab up to 5 results
            snippet_end = part.find("</a>")
            if snippet_end > 0:
                snippet = part[:snippet_end]
                clean = snippet.replace("<b>", "").replace("</b>", "")
                clean = clean.split(">")[-1] if ">" in clean else clean
                if clean.strip():
                    results.append(clean.strip())

        if results:
            return "Search results:\n" + "\n".join(
                f"- {r}" for r in results
            )
        return f"Search completed but no clear results for: {query}"

    except Exception as e:
        return f"Search error: {str(e)}"


def do_save_file(filename: str, content: str) -> str:
    """Save content to the output directory."""
    os.makedirs("output", exist_ok=True)
    filepath = os.path.join("output", filename)
    try:
        with open(filepath, "w") as f:
            f.write(content)
        return f"File saved successfully: {filepath}"
    except Exception as e:
        return f"Error saving file: {str(e)}"

La búsqueda web usa el endpoint HTML de DuckDuckGo — sin API key, sin registro, sin costo. El parseo de HTML está sostenido con cinta adhesiva y optimismo (estamos raspando el markup crudo de la página en lugar de usar un feed de datos apropiado), pero funciona. Para producción, lo cambiarías por Brave Search API (2,000 consultas gratis al mes) o SearXNG auto-alojado.

Paso 4: Construir el loop del agente (20 minutos)

Este es el corazón de todo el asunto. Cada producto de agentes con landing page bonita y valuación de $50M corre alguna versión de estas 30 líneas:

def run_agent(topic: str, max_turns: int = 10) -> str:
    """Run the research agent on a topic."""
    print(f"\n{'='*60}")
    print(f"Research Agent — Topic: {topic}")
    print(f"{'='*60}\n")

    system_prompt = """You are a research agent. Your job is to research a topic
thoroughly and produce a well-structured summary.

Your process:
1. Search for information on the topic (multiple searches with different angles)
2. Analyze what you find
3. Write a comprehensive research summary
4. Save the summary to a file

Be thorough — do at least 3 different searches to cover the topic well.
Be critical — evaluate sources and note conflicting information.
When done, save the final summary as a markdown file.

Current date: """ + datetime.now().strftime("%Y-%m-%d")

    messages = [
        {
            "role": "user",
            "content": f"Research this topic and produce a detailed summary: {topic}"
        }
    ]

    # El loop del agente
    for turn in range(max_turns):
        print(f"--- Turn {turn + 1} ---")

        response = client.messages.create(
            model=MODEL,
            max_tokens=4096,
            system=system_prompt,
            tools=tools,
            messages=messages,
        )

        print(f"Stop reason: {response.stop_reason}")

        if response.stop_reason == "tool_use":
            tool_results = []

            for block in response.content:
                if block.type == "tool_use":
                    tool_name = block.name
                    tool_input = block.input
                    tool_id = block.id

                    print(f"  Tool: {tool_name}")
                    print(f"  Input: {json.dumps(tool_input, indent=2)[:200]}")

                    result = execute_tool(tool_name, tool_input)
                    print(f"  Result: {result[:200]}...")

                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": tool_id,
                        "content": result,
                    })

            messages.append({"role": "assistant", "content": response.content})
            messages.append({"role": "user", "content": tool_results})

        elif response.stop_reason == "end_turn":
            final_text = ""
            for block in response.content:
                if hasattr(block, "text"):
                    final_text += block.text

            print(f"\nAgent completed in {turn + 1} turns.")
            return final_text

    return "Agent did not complete within turn limit."

Desglosemos este loop, porque ahí está todo el truco de magia:

  1. Envías la tarea a Claude junto con las definiciones de herramientas
  2. Claude piensa y decide si usar una herramienta o responder con texto final
  3. Si es tool_use — ejecutamos la herramienta y mandamos el resultado de vuelta como un nuevo mensaje
  4. Claude ve el resultado y decide el siguiente movimiento
  5. Repetir hasta que Claude diga end_turn — o sea, que terminó

El insight crítico: tú no programaste "primero busca, luego analiza, luego escribe". Claude descifra el flujo de trabajo basándose en la tarea. Eso es lo que separa un agente de un script. Un script sigue tus instrucciones. Un agente sigue las suyas.

El campo stop_reason es la clave. La API de Claude devuelve o "tool_use" (quiero llamar una herramienta) o "end_turn" (ya terminé). Tu loop solo verifica cuál es y actúa en consecuencia.

Paso 5: Agregar el punto de entrada (5 minutos)

La parte aburrida. Pero hasta las partes aburridas necesitan existir, si no nada funciona — lección que la mitad de los repos de demos de IA en GitHub todavía no aprenden:

if __name__ == "__main__":
    import sys

    if len(sys.argv) > 1:
        topic = " ".join(sys.argv[1:])
    else:
        topic = input("Enter research topic: ")

    result = run_agent(topic)

    print(f"\n{'='*60}")
    print("Research complete. Check the output/ directory.")
    print(f"{'='*60}")

Paso 6: Córrelo (5 minutos)

python agent.py "current state of MCP protocol adoption in 2026"

Observa la terminal. Vas a ver al agente razonar por su cuenta:

============================================================
Research Agent — Topic: current state of MCP protocol adoption in 2026
============================================================

--- Turn 1 ---
Stop reason: tool_use
  Tool: web_search
  Input: {"query": "MCP model context protocol adoption 2026"}
  Result: Search results: - The MCP ecosystem has grown...

--- Turn 2 ---
Stop reason: tool_use
  Tool: web_search
  Input: {"query": "MCP servers enterprise production 2026"}
  Result: Search results: - Amazon Bedrock AgentCore...

--- Turn 3 ---
Stop reason: tool_use
  Tool: web_search
  Input: {"query": "MCP protocol limitations challenges 2026"}
  Result: Search results: - Stateful sessions fight with...

--- Turn 4 ---
Stop reason: tool_use
  Tool: save_file
  Input: {"filename": "mcp-research-2026.md", "content": "# MCP Protocol..."}
  Result: File saved successfully: output/mcp-research-2026.md

--- Turn 5 ---
Stop reason: end_turn

Agent completed in 5 turns.

Cinco turnos. Tres búsquedas, un guardado de archivo, un resumen final. Nadie le dijo que buscara desde diferentes ángulos — lo decidió solo. Eso es agencia, no scripting.

Paso 7: Hazlo más inteligente (30 minutos)

El agente básico funciona. Ahora agreguemos tres mejoras que lo convierten de un demo en algo que realmente vas a seguir usando.

Memoria entre sesiones

Ahora mismo, cada ejecución empieza de cero. Démosle al agente una memoria simple — un archivo JSON (un formato de texto estructurado que los programas pueden leer y escribir fácilmente) que almacena lo que investigó antes:

from pathlib import Path

MEMORY_FILE = "memory.json"

def load_memory() -> list:
    """Load previous research topics and findings."""
    if Path(MEMORY_FILE).exists():
        with open(MEMORY_FILE) as f:
            return json.load(f)
    return []

def save_memory(topic: str, summary: str):
    """Save this research session to memory."""
    memory = load_memory()
    memory.append({
        "date": datetime.now().isoformat(),
        "topic": topic,
        "summary": summary[:500],
    })
    memory = memory[-20:]  # Quedarse con las últimas 20 entradas
    with open(MEMORY_FILE, "w") as f:
        json.dump(memory, f, indent=2)

Inyecta la memoria en el system prompt — el texto de instrucciones que moldea cómo se comporta Claude:

memory = load_memory()
if memory:
    memory_context = "\n\nPrevious research sessions:\n"
    for m in memory[-5:]:
        memory_context += f"- [{m['date'][:10]}] {m['topic']}: {m['summary'][:100]}...\n"
    system_prompt += memory_context

Ahora el agente sabe lo que investigó antes. Puede referenciar hallazgos previos, evitar búsquedas duplicadas y construir sobre trabajo anterior.

Una herramienta para pensar

Esta es una trampa. Agrega una herramienta que literalmente no hace nada:

tools.append({
    "name": "think",
    "description": "Use this tool to think through your approach before acting. Write out your reasoning and what you need to find out next.",
    "input_schema": {
        "type": "object",
        "properties": {
            "thought": {
                "type": "string",
                "description": "Your reasoning and plan"
            }
        },
        "required": ["thought"]
    }
})

En el ejecutor de herramientas, solo devuelve una confirmación:

elif tool_name == "think":
    print(f"  Thinking: {tool_input['thought'][:300]}")
    return "Thought recorded. Continue with your plan."

¿Por qué agregar una herramienta que no hace nada? Porque le da al agente un espacio estructurado para razonar antes de actuar. Sin ella, Claude se lanza directo a llamar herramientas. Con ella, Claude hace una pausa, planifica y luego ejecuta — produciendo resultados notablemente mejores. Anthropic documenta esta técnica en su guía de uso de herramientas, y los agentes en producción dependen de ella.

Recuperación de errores

def execute_tool_safe(tool_name: str, tool_input: dict) -> str:
    """Execute a tool with automatic retries."""
    for attempt in range(3):
        try:
            result = execute_tool(tool_name, tool_input)
            if "error" in result.lower() and attempt < 2:
                print(f"  Retry {attempt + 1}...")
                continue
            return result
        except Exception as e:
            if attempt < 2:
                print(f"  Error, retrying: {e}")
                continue
            return f"Tool failed after 3 attempts: {str(e)}"

Las peticiones web fallan. Las APIs se caen. Los timeouts pasan. Un agente de producción reintenta antes de rendirse. Tres intentos con fallback es lo mínimo.

La estructura final

research-agent/
├── agent.py          # ~150 líneas de Python
├── memory.json       # Se crea solo, almacena historial de sesiones
├── output/           # Se crea solo, almacena reportes de investigación
│   └── *.md
└── requirements.txt  # anthropic, httpx

Menos de 200 líneas. Dos dependencias. Sin frameworks.

¿Por qué sin framework?

Capaz te estás preguntando: "¿Por qué no usar LangChain o LlamaIndex?" (Ambos son frameworks populares de Python que agregan abstracciones pre-construidas alrededor de llamadas a LLMs.)

Porque el loop del agente de arriba son 30 líneas. LangChain agregaría 15 dependencias y tres capas de abstracción para el mismo resultado.

Usa un framework cuando:

  • Necesitas 10+ herramientas con lógica de ruteo compleja
  • Necesitas memoria de conversación que escale a miles de usuarios
  • Necesitas múltiples agentes coordinándose en la misma tarea
  • Ya le quedó chico el Python simple y necesitas la arquitectura de alguien más

Sáltate el framework cuando:

  • Estás construyendo tu primer agente
  • Tu agente tiene 2–5 herramientas
  • Quieres entender cada línea de lo que se ejecuta
  • "Funciona y lo entiendo" le gana a "funciona y confío en la abstracción"

A marzo de 2026, la documentación del SDK de Anthropic muestra el mismo patrón de loop básico que acabamos de construir. La recomendación oficial es empezar sin framework.

Lo que construiste esta noche

Hagamos inventario:

  1. Un agente de IA funcional — recibe un objetivo, lo persigue de forma autónoma
  2. Uso de herramientas — el agente llama sistemas externos (búsqueda web, I/O de archivos)
  3. Un loop de razonamiento — Claude decide la siguiente acción basándose en resultados, no en un script programado
  4. Memoria — el agente recuerda sesiones pasadas y construye sobre ellas
  5. Manejo de errores — reintenta antes de fallar
  6. Persistencia de output — los resultados quedan en archivos reales en tu disco

Esta es la misma arquitectura central de Claude Code, Devin, OpenAI Codex y todos los demás productos de agentes. Ellos tienen mejores herramientas, más manejo de errores y ventanas de contexto más grandes — la cantidad de texto que el modelo puede "ver" a la vez, como su memoria de trabajo. Pero el loop es idéntico al que acabas de escribir.

Hacia dónde seguir

Ahora entiendes el patrón fundamental. Todo lo demás es ingeniería encima de él:

  • Más herramientas — una calculadora, un web scraper, un conector de base de datos, un ejecutor de código
  • Mejor memoria — bases de datos vectoriales (sistemas que almacenan texto por significado, no solo por palabras clave) para búsqueda semántica entre sesiones pasadas
  • Llamadas de herramientas en paralelo — correr múltiples búsquedas al mismo tiempo en lugar de secuencialmente
  • Sistemas multi-agente — un segundo agente que revisa el trabajo del primero, como un code review
  • Integración MCP — Model Context Protocol, un estándar para conectar agentes de IA a herramientas externas, como USB pero para fuentes de datos

No estás aprendiendo el framework de alguien que podría estar muerto en seis meses. Estás aprendiendo el patrón. El mismo patrón que funcionó en 2024, funciona en 2026, y va a funcionar en 2028 — porque la mecánica subyacente (el modelo decide → la herramienta ejecuta → el resultado regresa) es cómo funcionan todos los sistemas de agentes, sin importar qué nombre de marketing les pongan.

La industria de "agentes de IA" quiere que creas que construir agentes requiere un doctorado y una Serie B de $100M. Requiere entender un loop: llama al modelo, revisa si quiere una herramienta, ejecuta la herramienta, manda el resultado de vuelta, repite. Eso es todo. Todo lo demás es ingeniería alrededor de ese loop — y ahora sabes lo suficiente para hacer esa ingeniería tú mismo.